Skip to content

API: use restricted serializer for related projects #11820

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/user/api/v3.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
++++++++++++++

Expand Down
40 changes: 32 additions & 8 deletions readthedocs/api/v3/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):

"""
Expand Down Expand Up @@ -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 = [
Expand Down Expand Up @@ -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"},
),
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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.
Expand All @@ -1244,7 +1269,6 @@ class RestrictedOrganizationSerializer(serializers.ModelSerializer):
class Meta:
model = Organization
fields = (
"name",
"slug",
"_links",
)
Expand Down
4 changes: 2 additions & 2 deletions readthedocs/proxito/tests/test_hosting.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
{
Expand All @@ -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"),
{
Expand Down
7 changes: 7 additions & 0 deletions readthedocs/proxito/views/hosting.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from readthedocs.api.v3.serializers import (
BuildSerializer,
ProjectSerializer,
RelatedProjectSerializer,
VersionSerializer,
)
from readthedocs.builds.constants import BUILD_STATE_FINISHED, LATEST
Expand Down Expand Up @@ -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__(
Expand Down