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

QuerySets: filter permissions by organizations #8298

Merged
merged 10 commits into from Sep 1, 2021
52 changes: 30 additions & 22 deletions readthedocs/builds/querysets.py
Expand Up @@ -11,6 +11,7 @@
BUILD_STATE_TRIGGERED,
EXTERNAL,
)
from readthedocs.core.permissions import AdminPermission
from readthedocs.core.utils.extend import SettingsOverrideObject
from readthedocs.projects import constants
from readthedocs.projects.models import Project
Expand Down Expand Up @@ -47,16 +48,16 @@ def __init__(self, *args, internal_only=False, external_only=False, **kwargs):
super().__init__(*args, **kwargs)

def _add_from_user_projects(self, queryset, user, admin=False, member=False):
"""
Add related objects from projects where `user` is an `admin` or a `member`.

.. note::

In .org all users are admin and member of a project.
This will change with organizations soon.
"""
if user.is_authenticated:
projects_pk = user.projects.all().values_list('pk', flat=True)
"""Add related objects from projects where `user` is an `admin` or a `member`."""
if user and user.is_authenticated:
stsewd marked this conversation as resolved.
Show resolved Hide resolved
projects_pk = (
AdminPermission.projects(
user=user,
admin=admin,
member=member,
)
.values_list('pk', flat=True)
)
user_queryset = self.filter(project__in=projects_pk)
queryset = user_queryset | queryset
return queryset
Expand Down Expand Up @@ -131,16 +132,16 @@ class BuildQuerySetBase(models.QuerySet):
use_for_related_fields = True

def _add_from_user_projects(self, queryset, user, admin=False, member=False):
"""
Add related objects from projects where `user` is an `admin` or a `member`.

.. note::

In .org all users are admin and member of a project.
This will change with organizations soon.
"""
if user.is_authenticated:
projects_pk = user.projects.all().values_list('pk', flat=True)
"""Add related objects from projects where `user` is an `admin` or a `member`."""
if user and user.is_authenticated:
projects_pk = (
AdminPermission.projects(
user=user,
admin=admin,
member=member,
)
.values_list('pk', flat=True)
)
user_queryset = self.filter(project__in=projects_pk)
queryset = user_queryset | queryset
return queryset
Expand Down Expand Up @@ -258,8 +259,15 @@ class RelatedBuildQuerySet(models.QuerySet):
use_for_related_fields = True

def _add_from_user_projects(self, queryset, user):
if user.is_authenticated:
projects_pk = user.projects.all().values_list('pk', flat=True)
if user and user.is_authenticated:
projects_pk = (
AdminPermission.projects(
user=user,
admin=True,
member=True,
)
.values_list('pk', flat=True)
)
user_queryset = self.filter(build__project__in=projects_pk)
queryset = user_queryset | queryset
return queryset
Expand Down
73 changes: 72 additions & 1 deletion readthedocs/core/permissions.py
Expand Up @@ -4,10 +4,82 @@
from django.db.models import Q

from readthedocs.core.utils.extend import SettingsOverrideObject
from readthedocs.organizations.constants import ADMIN_ACCESS, READ_ONLY_ACCESS


class AdminPermissionBase:

@classmethod
def projects(cls, user, admin=False, member=False):
stsewd marked this conversation as resolved.
Show resolved Hide resolved
"""
Return all the projects the user has access to as ``admin`` or ``member``.

If `RTD_ALLOW_ORGANIZATIONS` is enabled
This function takes into consideration VCS SSO and Google SSO.
It includes:

- projects where the user has access to via VCS SSO.
- Projects where the user has access via Team,
if VCS SSO is not enabled for the organization of that team.

.. note::

SSO is taken into consideration,
but isn't implemented in .org yet.

:param user: user object to filter projects
:type user: django.contrib.admin.models.User
:param bool admin: include projects where the user has admin access to the project
:param bool member: include projects where the user has read access to the project
"""
from readthedocs.projects.models import Project
from readthedocs.sso.models import SSOIntegration

projects = Project.objects.none()
if not user or not user.is_authenticated:
return projects

if not settings.RTD_ALLOW_ORGANIZATIONS:
# All users are admin and member of a project
# when we aren't using organizations.
return user.projects.all()

if admin:
# Project Team Admin
admin_teams = user.teams.filter(access=ADMIN_ACCESS)
for team in admin_teams:
if not cls.has_sso_enabled(team.organization, SSOIntegration.PROVIDER_ALLAUTH):
projects |= team.projects.all()

# Org Admin
for org in user.owner_organizations.all():
if not cls.has_sso_enabled(org, SSOIntegration.PROVIDER_ALLAUTH):
# Do not grant admin access on projects for owners if the
# organization has SSO enabled with Authorization on the provider.
projects |= org.projects.all()

projects |= cls._get_projects_for_sso_user(user, admin=True)

if member:
# Project Team Member
member_teams = user.teams.filter(access=READ_ONLY_ACCESS)
for team in member_teams:
if not cls.has_sso_enabled(team.organization, SSOIntegration.PROVIDER_ALLAUTH):
projects |= team.projects.all()

projects |= cls._get_projects_for_sso_user(user, admin=False)

return projects

@classmethod
def has_sso_enabled(cls, obj, provider=None):
return False

@classmethod
def _get_projects_for_sso_user(cls, user, admin=False):
from readthedocs.projects.models import Project
return Project.objects.none()
Comment on lines +74 to +81
Copy link
Member Author

Choose a reason for hiding this comment

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

These two depend on the SSO implementation, they are overridden on .com.


@classmethod
def owners(cls, obj):
"""
Expand Down Expand Up @@ -65,4 +137,3 @@ def is_member(cls, user, obj):

class AdminPermission(SettingsOverrideObject):
Copy link
Member

Choose a reason for hiding this comment

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

nit: would be good to rename this to Permission instead of AdminPermission at some point since it's not related to admin anymore.

_default_class = AdminPermissionBase
_override_setting = 'ADMIN_PERMISSION'
31 changes: 17 additions & 14 deletions readthedocs/projects/querysets.py
Expand Up @@ -5,6 +5,7 @@
from django.db.models import OuterRef, Prefetch, Q, Subquery

from readthedocs.builds.constants import EXTERNAL
from readthedocs.core.permissions import AdminPermission
from readthedocs.core.utils.extend import SettingsOverrideObject
from readthedocs.projects import constants

Expand All @@ -16,18 +17,13 @@ class ProjectQuerySetBase(models.QuerySet):
use_for_related_fields = True

def _add_user_projects(self, queryset, user, admin=False, member=False):
"""
Add projects from where `user` is an `admin` or a `member`.

.. note::

In .org all users are admin and member of a project.
This will change with organizations soon.
"""
if user.is_authenticated:
user_queryset = user.projects.all()
queryset = user_queryset | queryset
return queryset
"""Add projects from where `user` is an `admin` or a `member`."""
projects = AdminPermission.projects(
user=user,
admin=admin,
member=member,
)
return queryset | projects

def for_user_and_viewer(self, user, viewer):
"""
Expand Down Expand Up @@ -172,8 +168,15 @@ class RelatedProjectQuerySet(models.QuerySet):
project_field = 'project'

def _add_from_user_projects(self, queryset, user):
if user.is_authenticated:
projects_pk = user.projects.all().values_list('pk', flat=True)
if user and user.is_authenticated:
projects_pk = (
AdminPermission.projects(
user=user,
admin=True,
member=True,
)
.values_list('pk', flat=True)
)
kwargs = {f'{self.project_field}__in': projects_pk}
user_queryset = self.filter(**kwargs)
queryset = user_queryset | queryset
Expand Down
12 changes: 10 additions & 2 deletions readthedocs/redirects/querysets.py
@@ -1,10 +1,11 @@
"""Queryset for the redirects app."""

import logging

from django.db import models
from django.db.models import CharField, F, Q, Value

from readthedocs.core.permissions import AdminPermission

log = logging.getLogger(__name__)


Expand All @@ -16,7 +17,14 @@ class RedirectQuerySet(models.QuerySet):

def _add_from_user_projects(self, queryset, user):
if user.is_authenticated:
projects_pk = user.projects.all().values_list('pk', flat=True)
projects_pk = (
AdminPermission.projects(
user=user,
admin=True,
member=True,
)
.values_list('pk', flat=True)
)
user_queryset = self.filter(project__in=projects_pk)
queryset = user_queryset | queryset
return queryset.distinct()
Expand Down
3 changes: 1 addition & 2 deletions readthedocs/sso/models.py
Expand Up @@ -4,7 +4,6 @@

from django.db import models

from readthedocs.organizations.models import Organization
from readthedocs.projects.validators import validate_domain_name


Expand All @@ -30,7 +29,7 @@ class SSOIntegration(models.Model):
# editable=False,
)
organization = models.OneToOneField(
Organization,
'organizations.Organization',
on_delete=models.CASCADE,
)
provider = models.CharField(
Expand Down