From e1939c4aa7b589be11fe44915740619fa32a9ea7 Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Tue, 3 Dec 2024 16:42:48 -0500 Subject: [PATCH 1/5] API: use restricted serializer for related projects Ref https://github.com/readthedocs/readthedocs-corporate/issues/1845 --- readthedocs/api/v3/serializers.py | 35 ++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/readthedocs/api/v3/serializers.py b/readthedocs/api/v3/serializers.py index b271cf255fd..2dc6a337d12 100644 --- a/readthedocs/api/v3/serializers.py +++ b/readthedocs/api/v3/serializers.py @@ -846,13 +846,38 @@ 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 RestrictedProjectSerializer(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_relationshipt = obj.superprojects.first() + if parent_relationshipt: + # Since the related project can be private, we use a restricted serializer. + return RestrictedProjectSerializer(parent_relationshipt.parent).data + return None + + +class RestrictedProjectSerializer(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 name and slug. + """ + + _links = ProjectLinksSerializer(source="*") + + class Meta: + model = Project + fields = [ + "id", + "name", + "slug", + "_links", + ] class SubprojectCreateSerializer(FlexFieldsModelSerializer): From b1c3834bf171f232403085a03e54259b8537f366 Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Tue, 3 Dec 2024 18:44:06 -0500 Subject: [PATCH 2/5] Fix tests --- readthedocs/api/v3/serializers.py | 54 ++++++++++++----------- readthedocs/proxito/tests/test_hosting.py | 4 +- readthedocs/proxito/views/hosting.py | 7 +++ 3 files changed, 37 insertions(+), 28 deletions(-) diff --git a/readthedocs/api/v3/serializers.py b/readthedocs/api/v3/serializers.py index 2dc6a337d12..8c8b61be4af 100644 --- a/readthedocs/api/v3/serializers.py +++ b/readthedocs/api/v3/serializers.py @@ -736,6 +736,28 @@ def get_admin(self, obj): return AdminPermission.is_admin(user, obj) +class RestrictedProjectSerializer(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 name and slug. + """ + + _links = ProjectLinksSerializer(source="*") + + class Meta: + model = Project + fields = [ + "id", + "name", + "slug", + "_links", + ] + + class ProjectSerializer(FlexFieldsModelSerializer): """ @@ -766,6 +788,8 @@ class ProjectSerializer(FlexFieldsModelSerializer): created = serializers.DateTimeField(source="pub_date") modified = serializers.DateTimeField(source="modified_date") + related_project_serializer = RestrictedProjectSerializer + class Meta: model = Project fields = [ @@ -847,39 +871,17 @@ def get_homepage(self, obj): def get_translation_of(self, obj): if obj.main_language_project: # Since the related project can be private, we use a restricted serializer. - return RestrictedProjectSerializer(obj.main_language_project).data + return self.related_project_serializer(obj.main_language_project).data return None def get_subproject_of(self, obj): - parent_relationshipt = obj.superprojects.first() - if parent_relationshipt: + parent_relationship = obj.superprojects.first() + if parent_relationship: # Since the related project can be private, we use a restricted serializer. - return RestrictedProjectSerializer(parent_relationshipt.parent).data + return self.related_project_serializer(parent_relationship.parent).data return None -class RestrictedProjectSerializer(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 name and slug. - """ - - _links = ProjectLinksSerializer(source="*") - - class Meta: - model = Project - fields = [ - "id", - "name", - "slug", - "_links", - ] - - class SubprojectCreateSerializer(FlexFieldsModelSerializer): """Serializer used to define a Project as subproject of another Project.""" 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..6c10f18d1a9 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, + RestrictedProjectSerializer, 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 RestrictedProjectSerializerNoLinks(NoLinksMixin, RestrictedProjectSerializer): + pass + + class ProjectSerializerNoLinks(NoLinksMixin, ProjectSerializer): + related_project_serializer = RestrictedProjectSerializerNoLinks + def __init__(self, *args, **kwargs): resolver = kwargs.pop("resolver", Resolver()) super().__init__( From 137bd11ca48c30940ea464b78d32953a9ad0d984 Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Mon, 13 Jan 2025 10:32:19 -0500 Subject: [PATCH 3/5] Rename --- readthedocs/api/v3/serializers.py | 13 +++++-------- readthedocs/proxito/views/hosting.py | 6 +++--- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/readthedocs/api/v3/serializers.py b/readthedocs/api/v3/serializers.py index 8c8b61be4af..c0452ef99df 100644 --- a/readthedocs/api/v3/serializers.py +++ b/readthedocs/api/v3/serializers.py @@ -736,14 +736,14 @@ def get_admin(self, obj): return AdminPermission.is_admin(user, obj) -class RestrictedProjectSerializer(serializers.ModelSerializer): +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 name and slug. + we just show the slug. """ _links = ProjectLinksSerializer(source="*") @@ -751,8 +751,6 @@ class RestrictedProjectSerializer(serializers.ModelSerializer): class Meta: model = Project fields = [ - "id", - "name", "slug", "_links", ] @@ -788,7 +786,7 @@ class ProjectSerializer(FlexFieldsModelSerializer): created = serializers.DateTimeField(source="pub_date") modified = serializers.DateTimeField(source="modified_date") - related_project_serializer = RestrictedProjectSerializer + related_project_serializer = RelatedProjectSerializer class Meta: model = Project @@ -836,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"}, ), @@ -1257,7 +1255,7 @@ class Meta: ) -class RestrictedOrganizationSerializer(serializers.ModelSerializer): +class RelatedOrganizationSerializer(serializers.ModelSerializer): """ Stripped version of the OrganizationSerializer to be used when listing projects. @@ -1271,7 +1269,6 @@ class RestrictedOrganizationSerializer(serializers.ModelSerializer): class Meta: model = Organization fields = ( - "name", "slug", "_links", ) diff --git a/readthedocs/proxito/views/hosting.py b/readthedocs/proxito/views/hosting.py index 6c10f18d1a9..3299419c991 100644 --- a/readthedocs/proxito/views/hosting.py +++ b/readthedocs/proxito/views/hosting.py @@ -16,7 +16,7 @@ from readthedocs.api.v3.serializers import ( BuildSerializer, ProjectSerializer, - RestrictedProjectSerializer, + RelatedProjectSerializer, VersionSerializer, ) from readthedocs.builds.constants import BUILD_STATE_FINISHED, LATEST @@ -246,12 +246,12 @@ def __init__(self, *args, **kwargs): # on El Proxito. # # See https://github.com/readthedocs/readthedocs-ops/issues/1323 -class RestrictedProjectSerializerNoLinks(NoLinksMixin, RestrictedProjectSerializer): +class RelatedProjectSerializerNoLinks(NoLinksMixin, RelatedProjectSerializer): pass class ProjectSerializerNoLinks(NoLinksMixin, ProjectSerializer): - related_project_serializer = RestrictedProjectSerializerNoLinks + related_project_serializer = RelatedProjectSerializerNoLinks def __init__(self, *args, **kwargs): resolver = kwargs.pop("resolver", Resolver()) From 4909493b44c00548f95faee881349eedc8fda737 Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Mon, 13 Jan 2025 10:40:02 -0500 Subject: [PATCH 4/5] Mention previous behavior in docs --- docs/user/api/v3.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/user/api/v3.rst b/docs/user/api/v3.rst index dab6bf10e19..10997b67a61 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 ++++++++++++++ From f3343ca87f0d93cd1a87699664c0ad2759836827 Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Mon, 13 Jan 2025 10:43:57 -0500 Subject: [PATCH 5/5] Lint --- docs/user/api/v3.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user/api/v3.rst b/docs/user/api/v3.rst index 10997b67a61..a76858498fe 100644 --- a/docs/user/api/v3.rst +++ b/docs/user/api/v3.rst @@ -318,7 +318,7 @@ Project details 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.