Skip to content

Commit 1ab641a

Browse files
authored
API: use restricted serializer for related projects (#11820)
Ref readthedocs/readthedocs-corporate#1845
1 parent 1788629 commit 1ab641a

File tree

4 files changed

+50
-10
lines changed

4 files changed

+50
-10
lines changed

docs/user/api/v3.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,12 +308,21 @@ Project details
308308

309309
* **expand** (*string*) -- Add additional fields in the response.
310310
Allowed values are: ``organization``.
311+
We used to return a full organization object in the response,
312+
due to privacy concerns, now we return only the slug of the organization.
313+
If you need to get the full organization object, you can use the organization endpoint with the slug.
311314

312315
.. note::
313316

314317
The ``single_version`` attribute is deprecated,
315318
use ``versioning_scheme`` instead.
316319

320+
.. note::
321+
322+
Previously, ``translation_of`` and ``subproject_of`` returned a full project object,
323+
due to privacy concerns, now they return only the slug of the project.
324+
If you need to get the full project object, you can use the project endpoint with the slug.
325+
317326
Project create
318327
++++++++++++++
319328

readthedocs/api/v3/serializers.py

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,26 @@ def get_admin(self, obj):
736736
return AdminPermission.is_admin(user, obj)
737737

738738

739+
class RelatedProjectSerializer(serializers.ModelSerializer):
740+
741+
"""
742+
Stripped version of the ProjectSerializer to be used when including related projects.
743+
744+
This serializer is used to avoid leaking information about a private project through
745+
a public project. Instead of checking if user has access to the project,
746+
we just show the slug.
747+
"""
748+
749+
_links = ProjectLinksSerializer(source="*")
750+
751+
class Meta:
752+
model = Project
753+
fields = [
754+
"slug",
755+
"_links",
756+
]
757+
758+
739759
class ProjectSerializer(FlexFieldsModelSerializer):
740760

741761
"""
@@ -766,6 +786,8 @@ class ProjectSerializer(FlexFieldsModelSerializer):
766786
created = serializers.DateTimeField(source="pub_date")
767787
modified = serializers.DateTimeField(source="modified_date")
768788

789+
related_project_serializer = RelatedProjectSerializer
790+
769791
class Meta:
770792
model = Project
771793
fields = [
@@ -812,7 +834,7 @@ class Meta:
812834
# Users can use the /api/v3/organizations/ endpoint to get more information
813835
# about the organization.
814836
"organization": (
815-
"readthedocs.api.v3.serializers.RestrictedOrganizationSerializer",
837+
"readthedocs.api.v3.serializers.RelatedOrganizationSerializer",
816838
# NOTE: we cannot have a Project with multiple organizations.
817839
{"source": "organizations.first"},
818840
),
@@ -846,13 +868,16 @@ def get_homepage(self, obj):
846868

847869
def get_translation_of(self, obj):
848870
if obj.main_language_project:
849-
return self.__class__(obj.main_language_project).data
871+
# Since the related project can be private, we use a restricted serializer.
872+
return self.related_project_serializer(obj.main_language_project).data
873+
return None
850874

851875
def get_subproject_of(self, obj):
852-
try:
853-
return self.__class__(obj.superprojects.first().parent).data
854-
except Exception:
855-
return None
876+
parent_relationship = obj.superprojects.first()
877+
if parent_relationship:
878+
# Since the related project can be private, we use a restricted serializer.
879+
return self.related_project_serializer(parent_relationship.parent).data
880+
return None
856881

857882

858883
class SubprojectCreateSerializer(FlexFieldsModelSerializer):
@@ -1230,7 +1255,7 @@ class Meta:
12301255
)
12311256

12321257

1233-
class RestrictedOrganizationSerializer(serializers.ModelSerializer):
1258+
class RelatedOrganizationSerializer(serializers.ModelSerializer):
12341259

12351260
"""
12361261
Stripped version of the OrganizationSerializer to be used when listing projects.
@@ -1244,7 +1269,6 @@ class RestrictedOrganizationSerializer(serializers.ModelSerializer):
12441269
class Meta:
12451270
model = Organization
12461271
fields = (
1247-
"name",
12481272
"slug",
12491273
"_links",
12501274
)

readthedocs/proxito/tests/test_hosting.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -843,7 +843,7 @@ def test_number_of_queries_url_subproject(self):
843843
active=True,
844844
)
845845

846-
with self.assertNumQueries(31):
846+
with self.assertNumQueries(26):
847847
r = self.client.get(
848848
reverse("proxito_readthedocs_docs_addons"),
849849
{
@@ -869,7 +869,7 @@ def test_number_of_queries_url_translations(self):
869869
language=language,
870870
)
871871

872-
with self.assertNumQueries(58):
872+
with self.assertNumQueries(42):
873873
r = self.client.get(
874874
reverse("proxito_readthedocs_docs_addons"),
875875
{

readthedocs/proxito/views/hosting.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from readthedocs.api.v3.serializers import (
1717
BuildSerializer,
1818
ProjectSerializer,
19+
RelatedProjectSerializer,
1920
VersionSerializer,
2021
)
2122
from readthedocs.builds.constants import BUILD_STATE_FINISHED, LATEST
@@ -245,7 +246,13 @@ def __init__(self, *args, **kwargs):
245246
# on El Proxito.
246247
#
247248
# See https://github.com/readthedocs/readthedocs-ops/issues/1323
249+
class RelatedProjectSerializerNoLinks(NoLinksMixin, RelatedProjectSerializer):
250+
pass
251+
252+
248253
class ProjectSerializerNoLinks(NoLinksMixin, ProjectSerializer):
254+
related_project_serializer = RelatedProjectSerializerNoLinks
255+
249256
def __init__(self, *args, **kwargs):
250257
resolver = kwargs.pop("resolver", Resolver())
251258
super().__init__(

0 commit comments

Comments
 (0)