Skip to content

Commit

Permalink
QuerySets: filter permissions by organizations (#8298)
Browse files Browse the repository at this point in the history
* QuerySets: filter permissions by organizations

With the aim to bring logic from .com to .org more closer
I'm moving the organization's filters to .org.
They are going to be used if `RTD_ALLOW_ORGANIZATIONS` is True,
but they eventually will be merged with the current querysets
when organizations and normal projects are supported in .org.

In .com we are relying on user.projects having the
the projects where the user is member of and organization owner
(we are syncing this with signals).
We don't rely on that hack anymore and always check from the
organization models. The signals won't be removed for now,
but shouldn't be needed anymore.

To re-use more code I have brought the SSO concept here,
but isn't implemented yet, we can bring the SSO models and logic later
easily.

This could be seen as more queries,
but the main ones were already being executed in .com.
The other ones are executed only when using the API V2,
so I don't think that would be a problem.

I started this refactor using a mixin,
but then we would need to override every single
queryset in .com, using composition only requires
one override (already used),
and doesn't bring other methods into the class.
  • Loading branch information
stsewd committed Sep 1, 2021
1 parent a12a181 commit 7f4215f
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 41 deletions.
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:
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):
"""
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()

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

class AdminPermission(SettingsOverrideObject):
_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

0 comments on commit 7f4215f

Please sign in to comment.