Skip to content
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

API V3: Don't allow leaking teams through expandable fields #11471

Merged
merged 5 commits into from
Aug 5, 2024
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
66 changes: 60 additions & 6 deletions docs/user/api/v3.rst
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,8 @@ Projects list

:Query Parameters:

* **expand** (*string*) -- with ``organization`` and ``teams``.
* **expand** (*string*) -- Add additional fields in the response.
Allowed values are: ``organization``.



Expand Down Expand Up @@ -294,7 +295,7 @@ Project details
}
}

:query string expand: allows to add/expand some extra fields in the response.
:query string expand: Add additional fields in the response.
Allowed values are ``active_versions``, ``active_versions.last_build`` and
``active_versions.last_build.config``. Multiple fields can be passed separated by commas.

Expand All @@ -314,7 +315,8 @@ Project details

:Query Parameters:

* **expand** (*string*) -- with ``organization`` and ``teams``.
* **expand** (*string*) -- Add additional fields in the response.
Allowed values are: ``organization``.

.. note::

Expand Down Expand Up @@ -620,7 +622,7 @@ Version detail
``null`` when it's not the stable version.
:>json boolean built: the version has at least one successful build.

:query string expand: allows to add/expand some extra fields in the response.
:query string expand: Add additional fields in the response.
Allowed values are ``last_build`` and ``last_build.config``.
Multiple fields can be passed separated by commas.

Expand Down Expand Up @@ -782,7 +784,7 @@ Build details
:>json string state: The state of the build (one of ``triggered``, ``building``, ``installing``, ``cloning``, ``finished`` or ``cancelled``)
:>json string error: An error message if the build was unsuccessful

:query string expand: allows to add/expand some extra fields in the response.
:query string expand: Add additional fields in the response.
Allowed value is ``config``.


Expand Down Expand Up @@ -1730,6 +1732,58 @@ Organization projects list
]
}

Organization teams list
+++++++++++++++++++++++

.. http:get:: /api/v3/organizations/(string:organization_slug)/teams/

Retrieve list of teams under an organization.

**Example request**:

.. tabs::

.. code-tab:: bash

$ curl -H "Authorization: Token <token>" https://readthedocs.com/api/v3/organizations/pypa/teams/

.. code-tab:: python

import requests
URL = 'https://readthedocs.com/api/v3/organizations/pypa/teams/'
TOKEN = '<token>'
HEADERS = {'Authorization': f'token {TOKEN}'}
response = requests.get(URL, headers=HEADERS)
print(response.json())

**Example response**:

.. sourcecode:: json

{
"count": 2,
"next": null,
"previous": null,
"results": [
{
"access": "admin",
"created": "2024-07-01T21:19:55.573819Z",
"modified": "2024-07-01T21:19:55.573837Z",
"name": "Admins",
"slug": "admins"
},
{
"access": "readonly",
"created": "2024-07-01T21:19:55.593815Z",
"modified": "2024-07-01T21:19:55.593829Z",
"name": "Read Only",
"slug": "read-only"
}
]
}

:query string expand: Add additional fields in the response.
Allowed values are: ``members``.

Remote organizations
~~~~~~~~~~~~~~~~~~~~
Expand Down Expand Up @@ -1916,7 +1970,7 @@ Remote repository listing
:query string full_name: return remote repositories containing the full name (it inclues the username/organization the project belongs to)
:query string vcs_provider: return remote repositories for specific vcs provider (``github``, ``gitlab`` or ``bitbucket``)
:query string organization: return remote repositories for specific remote organization (using remote organization ``slug``)
:query string expand: allows to add/expand some extra fields in the response.
:query string expand: Add additional fields in the response.
Allowed values are ``projects`` and ``remote_organization``.
Multiple fields can be passed separated by commas.

Expand Down
5 changes: 5 additions & 0 deletions readthedocs/api/v3/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,12 +188,17 @@ class OrganizationQuerySetMixin(NestedParentObjectMixin):
"""

def has_admin_permission(self, user, organization):
"""Check if user is an owner of the organization."""
if self.admin_organizations(user).filter(pk=organization.pk).exists():
return True

return False

def is_admin_member(self, user, organization):
"""Check if user is an owner or belongs to a team with admin permissions of the organization."""
if self.has_admin_permission(user, organization):
return True
Comment on lines +199 to +200
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the owner didn't belong to any admin teams, we were not granting permissions :_


return (
Project.objects.for_admin_user(user=user)
.filter(organizations__in=[organization])
Expand Down
25 changes: 2 additions & 23 deletions readthedocs/api/v3/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -802,20 +802,10 @@ class Meta:
# NOTE: we use a serializer without expandable fields to avoid
# leaking information about the organization through the project.
"organization": (
"readthedocs.api.v3.serializers.OrganizationSerializerWithoutExpandableFields",
"readthedocs.api.v3.serializers.OrganizationSerializer",
# NOTE: we cannot have a Project with multiple organizations.
{"source": "organizations.first"},
),
# NOTE: we are leaking the slugs of all teams linked to this project
# to anyone with access to this prtoject. It's only the slug, but still.
"teams": (
serializers.SlugRelatedField,
{
"slug_field": "slug",
"many": True,
"read_only": True,
},
),
}

def __init__(self, *args, **kwargs):
Expand Down Expand Up @@ -1183,7 +1173,7 @@ class Meta:
}


class OrganizationSerializerWithoutExpandableFields(FlexFieldsModelSerializer):
class OrganizationSerializer(serializers.ModelSerializer):
created = serializers.DateTimeField(source="pub_date")
modified = serializers.DateTimeField(source="modified_date")
owners = UserSerializer(many=True)
Expand All @@ -1206,17 +1196,6 @@ class Meta:
)


class OrganizationSerializerWithExpandableFields(
OrganizationSerializerWithoutExpandableFields
):
class Meta(OrganizationSerializerWithoutExpandableFields.Meta):
expandable_fields = {
# TODO: we are leaking all teams and its members to anyone who is
# an admin member of the organization.
"teams": (TeamSerializer, {"many": True}),
}


class RemoteOrganizationSerializer(serializers.ModelSerializer):
class Meta:
model = RemoteOrganization
Expand Down
8 changes: 8 additions & 0 deletions readthedocs/api/v3/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
NotificationsOrganizationViewSet,
NotificationsProjectViewSet,
NotificationsUserViewSet,
OrganizationsTeamsViewSet,
OrganizationsViewSet,
ProjectsViewSet,
RedirectsViewSet,
Expand Down Expand Up @@ -141,6 +142,13 @@
parents_query_lookups=["organization__slug"],
)

organizations.register(
"teams",
OrganizationsTeamsViewSet,
basename="organizations-teams",
parents_query_lookups=["organization__slug"],
)

# allows /api/v3/notifications/
router.register(
r"notifications",
Expand Down
25 changes: 22 additions & 3 deletions readthedocs/api/v3/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
RemoteRepository,
RemoteRepositoryRelation,
)
from readthedocs.organizations.models import Organization
from readthedocs.organizations.models import Organization, Team
from readthedocs.projects.models import (
EnvironmentVariable,
Project,
Expand Down Expand Up @@ -72,7 +72,7 @@
BuildSerializer,
EnvironmentVariableSerializer,
NotificationSerializer,
OrganizationSerializerWithExpandableFields,
OrganizationSerializer,
ProjectCreateSerializer,
ProjectSerializer,
ProjectUpdateSerializer,
Expand All @@ -83,6 +83,7 @@
SubprojectCreateSerializer,
SubprojectDestroySerializer,
SubprojectSerializer,
TeamSerializer,
UserSerializer,
VersionSerializer,
VersionUpdateSerializer,
Expand Down Expand Up @@ -692,7 +693,7 @@ class OrganizationsViewSetBase(
# Also note that Read the Docs for Business expose this endpoint already.

model = Organization
serializer_class = OrganizationSerializerWithExpandableFields
serializer_class = OrganizationSerializer
queryset = Organization.objects.none()
permission_classes = (IsAuthenticated,)

Expand Down Expand Up @@ -721,6 +722,24 @@ def get_view_name(self):
return f"Organizations Projects {self.suffix}"


class OrganizationsTeamsViewSet(
APIv3Settings,
NestedViewSetMixin,
OrganizationQuerySetMixin,
FlexFieldsMixin,
ListModelMixin,
GenericViewSet,
):
model = Team
serializer_class = TeamSerializer
permission_classes = [IsAuthenticated & IsOrganizationAdmin]
permit_list_expands = ["members"]

def get_queryset(self):
organization = self._get_parent_organization()
return organization.teams.all()


class NotificationsOrganizationViewSet(
APIv3Settings,
NestedViewSetMixin,
Expand Down