diff --git a/docs/user/api/v3.rst b/docs/user/api/v3.rst index dab6bf10e19..a76858498fe 100644 --- a/docs/user/api/v3.rst +++ b/docs/user/api/v3.rst @@ -308,12 +308,21 @@ Project details * **expand** (*string*) -- Add additional fields in the response. Allowed values are: ``organization``. + We used to return a full organization object in the response, + due to privacy concerns, now we return only the slug of the organization. + If you need to get the full organization object, you can use the organization endpoint with the slug. .. note:: The ``single_version`` attribute is deprecated, use ``versioning_scheme`` instead. + .. note:: + + Previously, ``translation_of`` and ``subproject_of`` returned a full project object, + due to privacy concerns, now they return only the slug of the project. + If you need to get the full project object, you can use the project endpoint with the slug. + Project create ++++++++++++++ diff --git a/readthedocs/api/v3/serializers.py b/readthedocs/api/v3/serializers.py index b271cf255fd..c0452ef99df 100644 --- a/readthedocs/api/v3/serializers.py +++ b/readthedocs/api/v3/serializers.py @@ -736,6 +736,26 @@ def get_admin(self, obj): return AdminPermission.is_admin(user, obj) +class RelatedProjectSerializer(serializers.ModelSerializer): + + """ + Stripped version of the ProjectSerializer to be used when including related projects. + + This serializer is used to avoid leaking information about a private project through + a public project. Instead of checking if user has access to the project, + we just show the slug. + """ + + _links = ProjectLinksSerializer(source="*") + + class Meta: + model = Project + fields = [ + "slug", + "_links", + ] + + class ProjectSerializer(FlexFieldsModelSerializer): """ @@ -766,6 +786,8 @@ class ProjectSerializer(FlexFieldsModelSerializer): created = serializers.DateTimeField(source="pub_date") modified = serializers.DateTimeField(source="modified_date") + related_project_serializer = RelatedProjectSerializer + class Meta: model = Project fields = [ @@ -812,7 +834,7 @@ class Meta: # Users can use the /api/v3/organizations/ endpoint to get more information # about the organization. "organization": ( - "readthedocs.api.v3.serializers.RestrictedOrganizationSerializer", + "readthedocs.api.v3.serializers.RelatedOrganizationSerializer", # NOTE: we cannot have a Project with multiple organizations. {"source": "organizations.first"}, ), @@ -846,13 +868,16 @@ def get_homepage(self, obj): def get_translation_of(self, obj): if obj.main_language_project: - return self.__class__(obj.main_language_project).data + # Since the related project can be private, we use a restricted serializer. + return self.related_project_serializer(obj.main_language_project).data + return None def get_subproject_of(self, obj): - try: - return self.__class__(obj.superprojects.first().parent).data - except Exception: - return None + parent_relationship = obj.superprojects.first() + if parent_relationship: + # Since the related project can be private, we use a restricted serializer. + return self.related_project_serializer(parent_relationship.parent).data + return None class SubprojectCreateSerializer(FlexFieldsModelSerializer): @@ -1230,7 +1255,7 @@ class Meta: ) -class RestrictedOrganizationSerializer(serializers.ModelSerializer): +class RelatedOrganizationSerializer(serializers.ModelSerializer): """ Stripped version of the OrganizationSerializer to be used when listing projects. @@ -1244,7 +1269,6 @@ class RestrictedOrganizationSerializer(serializers.ModelSerializer): class Meta: model = Organization fields = ( - "name", "slug", "_links", ) diff --git a/readthedocs/proxito/tests/test_hosting.py b/readthedocs/proxito/tests/test_hosting.py index a990fe2a984..3b23f3b71db 100644 --- a/readthedocs/proxito/tests/test_hosting.py +++ b/readthedocs/proxito/tests/test_hosting.py @@ -843,7 +843,7 @@ def test_number_of_queries_url_subproject(self): active=True, ) - with self.assertNumQueries(31): + with self.assertNumQueries(26): r = self.client.get( reverse("proxito_readthedocs_docs_addons"), { @@ -869,7 +869,7 @@ def test_number_of_queries_url_translations(self): language=language, ) - with self.assertNumQueries(58): + with self.assertNumQueries(42): r = self.client.get( reverse("proxito_readthedocs_docs_addons"), { diff --git a/readthedocs/proxito/views/hosting.py b/readthedocs/proxito/views/hosting.py index 933f359fbec..3299419c991 100644 --- a/readthedocs/proxito/views/hosting.py +++ b/readthedocs/proxito/views/hosting.py @@ -16,6 +16,7 @@ from readthedocs.api.v3.serializers import ( BuildSerializer, ProjectSerializer, + RelatedProjectSerializer, VersionSerializer, ) from readthedocs.builds.constants import BUILD_STATE_FINISHED, LATEST @@ -245,7 +246,13 @@ def __init__(self, *args, **kwargs): # on El Proxito. # # See https://github.com/readthedocs/readthedocs-ops/issues/1323 +class RelatedProjectSerializerNoLinks(NoLinksMixin, RelatedProjectSerializer): + pass + + class ProjectSerializerNoLinks(NoLinksMixin, ProjectSerializer): + related_project_serializer = RelatedProjectSerializerNoLinks + def __init__(self, *args, **kwargs): resolver = kwargs.pop("resolver", Resolver()) super().__init__(