From bb2a1b9b9b44c7b829670ccad2c8ca8858122957 Mon Sep 17 00:00:00 2001 From: Anthony Johnson Date: Fri, 16 Jun 2017 16:10:08 -0700 Subject: [PATCH] Remove privacy app The privacy app was a strange mixture of various application models managers, querysets, and syncers. Instead, logic is moved to where we should be using it, inside the other applications --- readthedocs/builds/managers.py | 63 ++++ readthedocs/builds/models.py | 23 +- readthedocs/builds/querysets.py | 140 ++++++++ .../{privacy/backends => builds}/syncers.py | 15 +- readthedocs/core/permissions.py | 21 ++ .../templatetags/privacy_tags.py | 5 +- readthedocs/core/views/serve.py | 4 +- readthedocs/oauth/managers.py | 12 - readthedocs/oauth/models.py | 2 +- readthedocs/oauth/querysets.py | 31 ++ readthedocs/privacy/__init__.py | 0 readthedocs/privacy/backend.py | 326 ------------------ readthedocs/privacy/backends/__init__.py | 0 readthedocs/privacy/backends/managers.py | 0 readthedocs/privacy/backends/permissions.py | 0 readthedocs/privacy/loader.py | 62 ---- readthedocs/privacy/models.py | 0 readthedocs/privacy/templatetags/__init__.py | 0 readthedocs/projects/forms.py | 2 +- readthedocs/projects/models.py | 7 +- readthedocs/projects/querysets.py | 150 ++++++++ readthedocs/projects/tasks.py | 13 +- readthedocs/restapi/permissions.py | 3 +- .../rtd_tests/tests/test_project_querysets.py | 29 ++ readthedocs/rtd_tests/tests/test_views.py | 2 +- readthedocs/settings/base.py | 1 - readthedocs/settings/dev.py | 2 +- 27 files changed, 479 insertions(+), 434 deletions(-) create mode 100644 readthedocs/builds/managers.py create mode 100644 readthedocs/builds/querysets.py rename readthedocs/{privacy/backends => builds}/syncers.py (95%) create mode 100644 readthedocs/core/permissions.py rename readthedocs/{privacy => core}/templatetags/privacy_tags.py (90%) delete mode 100644 readthedocs/oauth/managers.py create mode 100644 readthedocs/oauth/querysets.py delete mode 100644 readthedocs/privacy/__init__.py delete mode 100644 readthedocs/privacy/backend.py delete mode 100644 readthedocs/privacy/backends/__init__.py delete mode 100644 readthedocs/privacy/backends/managers.py delete mode 100644 readthedocs/privacy/backends/permissions.py delete mode 100644 readthedocs/privacy/loader.py delete mode 100644 readthedocs/privacy/models.py delete mode 100644 readthedocs/privacy/templatetags/__init__.py create mode 100644 readthedocs/projects/querysets.py create mode 100644 readthedocs/rtd_tests/tests/test_project_querysets.py diff --git a/readthedocs/builds/managers.py b/readthedocs/builds/managers.py new file mode 100644 index 00000000000..716dad6dde6 --- /dev/null +++ b/readthedocs/builds/managers.py @@ -0,0 +1,63 @@ +"""Build and Version class model Managers""" + +from __future__ import absolute_import + +from django.db import models + +from .constants import (BRANCH, TAG, LATEST, LATEST_VERBOSE_NAME, STABLE, + STABLE_VERBOSE_NAME) +from .querysets import VersionQuerySet +from readthedocs.core.utils.extend import (SettingsOverrideObject, + get_override_class) + + +__all__ = ['VersionManager'] + + +class VersionManagerBase(models.Manager): + + """Version manager for manager only queries + + For queries not suitable for the :py:cls:`VersionQuerySet`, such as create + queries. + """ + + @classmethod + def from_queryset(cls, queryset_class, class_name=None): + # This is overridden because :py:meth:`models.Manager.from_queryset` + # uses `inspect` to retrieve the class methods, and the proxy class has + # no direct members. + queryset_class = get_override_class( + VersionQuerySet, + VersionQuerySet._default_class # pylint: disable=protected-access + ) + return super(VersionManagerBase, cls).from_queryset(queryset_class, class_name) + + def create_stable(self, **kwargs): + defaults = { + 'slug': STABLE, + 'verbose_name': STABLE_VERBOSE_NAME, + 'machine': True, + 'active': True, + 'identifier': STABLE, + 'type': TAG, + } + defaults.update(kwargs) + return self.create(**defaults) + + def create_latest(self, **kwargs): + defaults = { + 'slug': LATEST, + 'verbose_name': LATEST_VERBOSE_NAME, + 'machine': True, + 'active': True, + 'identifier': LATEST, + 'type': BRANCH, + } + defaults.update(kwargs) + return self.create(**defaults) + + +class VersionManager(SettingsOverrideObject): + _default_class = VersionManagerBase + _override_setting = 'VERSION_MANAGER' diff --git a/readthedocs/builds/models.py b/readthedocs/builds/models.py index b456124c657..a91222594cf 100644 --- a/readthedocs/builds/models.py +++ b/readthedocs/builds/models.py @@ -1,33 +1,32 @@ """Models for the builds app.""" from __future__ import absolute_import -from builtins import object + import logging -import re import os.path +import re from shutil import rmtree -from django.core.urlresolvers import reverse +from builtins import object from django.conf import settings +from django.core.urlresolvers import reverse from django.db import models from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _, ugettext - from guardian.shortcuts import assign from taggit.managers import TaggableManager -from readthedocs.core.utils import broadcast -from readthedocs.privacy.backend import VersionQuerySet, VersionManager -from readthedocs.privacy.loader import RelatedBuildQuerySet, BuildQuerySet -from readthedocs.projects.models import Project -from readthedocs.projects.constants import (PRIVACY_CHOICES, GITHUB_URL, - GITHUB_REGEXS, BITBUCKET_URL, - BITBUCKET_REGEXS, PRIVATE) - from .constants import (BUILD_STATE, BUILD_TYPES, VERSION_TYPES, LATEST, NON_REPOSITORY_VERSIONS, STABLE, BUILD_STATE_FINISHED, BRANCH, TAG) +from .managers import VersionManager +from .querysets import BuildQuerySet, RelatedBuildQuerySet, VersionQuerySet from .version_slug import VersionSlugField +from readthedocs.core.utils import broadcast +from readthedocs.projects.constants import (PRIVACY_CHOICES, GITHUB_URL, + GITHUB_REGEXS, BITBUCKET_URL, + BITBUCKET_REGEXS, PRIVATE) +from readthedocs.projects.models import Project DEFAULT_VERSION_PRIVACY_LEVEL = getattr(settings, 'DEFAULT_VERSION_PRIVACY_LEVEL', 'public') diff --git a/readthedocs/builds/querysets.py b/readthedocs/builds/querysets.py new file mode 100644 index 00000000000..5b1628a6e73 --- /dev/null +++ b/readthedocs/builds/querysets.py @@ -0,0 +1,140 @@ +"""Build and Version QuerySet classes""" + +from __future__ import absolute_import + +from django.db import models +from guardian.shortcuts import get_objects_for_user + +from readthedocs.core.utils.extend import SettingsOverrideObject +from readthedocs.projects import constants + + +__all__ = ['VersionQuerySet', 'BuildQuerySet', 'RelatedBuildQuerySet'] + + +class VersionQuerySetBase(models.QuerySet): + + """Versions take into account their own privacy_level setting.""" + + use_for_related_fields = True + + def _add_user_repos(self, queryset, user): + if user.has_perm('builds.view_version'): + return self.all().distinct() + if user.is_authenticated(): + user_queryset = get_objects_for_user(user, 'builds.view_version') + queryset = user_queryset | queryset + return queryset.distinct() + + def public(self, user=None, project=None, only_active=True): + queryset = self.filter(privacy_level=constants.PUBLIC) + if user: + queryset = self._add_user_repos(queryset, user) + if project: + queryset = queryset.filter(project=project) + if only_active: + queryset = queryset.filter(active=True) + return queryset + + def protected(self, user=None, project=None, only_active=True): + queryset = self.filter(privacy_level__in=[constants.PUBLIC, constants.PROTECTED]) + if user: + queryset = self._add_user_repos(queryset, user) + if project: + queryset = queryset.filter(project=project) + if only_active: + queryset = queryset.filter(active=True) + return queryset + + def private(self, user=None, project=None, only_active=True): + queryset = self.filter(privacy_level__in=[constants.PRIVATE]) + if user: + queryset = self._add_user_repos(queryset, user) + if project: + queryset = queryset.filter(project=project) + if only_active: + queryset = queryset.filter(active=True) + return queryset + + def api(self, user=None): + return self.public(user, only_active=False) + + def for_project(self, project): + """Return all versions for a project, including translations""" + return self.filter( + models.Q(project=project) | + models.Q(project__main_language_project=project) + ) + + +class VersionQuerySet(SettingsOverrideObject): + _default_class = VersionQuerySetBase + + +class BuildQuerySetBase(models.QuerySet): + + """ + Build objects that are privacy aware. + + i.e. they take into account the privacy of the Version that they relate to. + """ + + use_for_related_fields = True + + def _add_user_repos(self, queryset, user=None): + if user.has_perm('builds.view_version'): + return self.all().distinct() + if user.is_authenticated(): + user_queryset = get_objects_for_user(user, 'builds.view_version') + pks = [p.pk for p in user_queryset] + queryset = self.filter(version__pk__in=pks) | queryset + return queryset.distinct() + + def public(self, user=None, project=None): + queryset = self.filter(version__privacy_level=constants.PUBLIC) + if user: + queryset = self._add_user_repos(queryset, user) + if project: + queryset = queryset.filter(project=project) + return queryset + + def api(self, user=None): + return self.public(user) + + +class BuildQuerySet(SettingsOverrideObject): + _default_class = BuildQuerySetBase + _override_setting = 'BUILD_MANAGER' + + +class RelatedBuildQuerySetBase(models.QuerySet): + + """For models with association to a project through :py:class:`Build`""" + + use_for_related_fields = True + + def _add_user_repos(self, queryset, user=None): + if user.has_perm('builds.view_version'): + return self.all().distinct() + if user.is_authenticated(): + user_queryset = get_objects_for_user(user, 'builds.view_version') + pks = [p.pk for p in user_queryset] + queryset = self.filter( + build__version__pk__in=pks) | queryset + return queryset.distinct() + + def public(self, user=None, project=None): + queryset = self.filter(build__version__privacy_level=constants.PUBLIC) + if user: + queryset = self._add_user_repos(queryset, user) + if project: + queryset = queryset.filter(build__project=project) + return queryset + + def api(self, user=None): + return self.public(user) + + +class RelatedBuildQuerySet(SettingsOverrideObject): + _default_class = RelatedBuildQuerySetBase + _override_setting = 'RELATED_BUILD_MANAGER' diff --git a/readthedocs/privacy/backends/syncers.py b/readthedocs/builds/syncers.py similarity index 95% rename from readthedocs/privacy/backends/syncers.py rename to readthedocs/builds/syncers.py index 23650fbb52c..4c2b5f2da93 100644 --- a/readthedocs/privacy/backends/syncers.py +++ b/readthedocs/builds/syncers.py @@ -1,18 +1,22 @@ -"""Classes allowing copying files around. +"""Classes to copy files between build and web servers "Syncers" copy files from the local machine, while "Pullers" copy files to the local machine. - """ + from __future__ import absolute_import -from builtins import object + import getpass import logging import os import shutil +from builtins import object from django.conf import settings +from readthedocs.core.utils.extend import SettingsOverrideObject + + log = logging.getLogger(__name__) @@ -135,3 +139,8 @@ def copy(cls, path, target, host, is_file=False, **__): ret = os.system(sync_cmd) if ret != 0: log.info("COPY ERROR to app servers.") + + +class Syncer(SettingsOverrideObject): + _default_class = LocalSyncer + _override_setting = 'FILE_SYNCER' diff --git a/readthedocs/core/permissions.py b/readthedocs/core/permissions.py new file mode 100644 index 00000000000..9f23eeeff51 --- /dev/null +++ b/readthedocs/core/permissions.py @@ -0,0 +1,21 @@ +"""Objects for User permission checks""" + +from __future__ import absolute_import + +from readthedocs.core.utils.extend import SettingsOverrideObject + + +class AdminPermissionBase(object): + + @classmethod + def is_admin(cls, user, project): + return user in project.users.all() + + @classmethod + def is_member(cls, user, obj): + return user in obj.users.all() + + +class AdminPermission(SettingsOverrideObject): + _default_class = AdminPermissionBase + _override_setting = 'ADMIN_PERMISSION' diff --git a/readthedocs/privacy/templatetags/privacy_tags.py b/readthedocs/core/templatetags/privacy_tags.py similarity index 90% rename from readthedocs/privacy/templatetags/privacy_tags.py rename to readthedocs/core/templatetags/privacy_tags.py index b79662b3f25..d18778778f6 100644 --- a/readthedocs/privacy/templatetags/privacy_tags.py +++ b/readthedocs/core/templatetags/privacy_tags.py @@ -1,12 +1,13 @@ """Template tags to query projects by privacy.""" from __future__ import absolute_import -from django import template -from ..loader import AdminPermission +from django import template +from readthedocs.core.permissions import AdminPermission from readthedocs.projects.models import Project + register = template.Library() diff --git a/readthedocs/core/views/serve.py b/readthedocs/core/views/serve.py index 677d8646707..a011e33bc11 100644 --- a/readthedocs/core/views/serve.py +++ b/readthedocs/core/views/serve.py @@ -34,9 +34,9 @@ from readthedocs.builds.models import Version from readthedocs.projects import constants from readthedocs.projects.models import Project, ProjectRelationship -from readthedocs.core.symlink import PrivateSymlink, PublicSymlink +from readthedocs.core.permissions import AdminPermission from readthedocs.core.resolver import resolve, resolve_path -from readthedocs.privacy.loader import AdminPermission +from readthedocs.core.symlink import PrivateSymlink, PublicSymlink import mimetypes import os diff --git a/readthedocs/oauth/managers.py b/readthedocs/oauth/managers.py deleted file mode 100644 index 41223c252a8..00000000000 --- a/readthedocs/oauth/managers.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Managers for OAuth models""" - -from __future__ import absolute_import -from readthedocs.privacy.loader import RelatedUserQuerySet - - -class RemoteRepositoryQuerySet(RelatedUserQuerySet): - pass - - -class RemoteOrganizationQuerySet(RelatedUserQuerySet): - pass diff --git a/readthedocs/oauth/models.py b/readthedocs/oauth/models.py index c847633ab8a..e5ab447c4cc 100644 --- a/readthedocs/oauth/models.py +++ b/readthedocs/oauth/models.py @@ -17,7 +17,7 @@ from readthedocs.projects.constants import REPO_CHOICES from readthedocs.projects.models import Project -from .managers import RemoteRepositoryQuerySet, RemoteOrganizationQuerySet +from .querysets import RemoteRepositoryQuerySet, RemoteOrganizationQuerySet DEFAULT_PRIVACY_LEVEL = getattr(settings, 'DEFAULT_PRIVACY_LEVEL', 'public') diff --git a/readthedocs/oauth/querysets.py b/readthedocs/oauth/querysets.py new file mode 100644 index 00000000000..f916889d469 --- /dev/null +++ b/readthedocs/oauth/querysets.py @@ -0,0 +1,31 @@ +"""Managers for OAuth models""" + +from __future__ import absolute_import + +from django.db import models + +from readthedocs.core.utils.extend import SettingsOverrideObject + + +class RelatedUserQuerySetBase(models.QuerySet): + + """For models with relations through :py:class:`User`""" + + def api(self, user=None): + """Return objects for user""" + if not user.is_authenticated(): + return self.none() + return self.filter(users=user) + + +class RelatedUserQuerySet(SettingsOverrideObject): + _default_class = RelatedUserQuerySetBase + _override_setting = 'RELATED_USER_MANAGER' + + +class RemoteRepositoryQuerySet(RelatedUserQuerySet): + pass + + +class RemoteOrganizationQuerySet(RelatedUserQuerySet): + pass diff --git a/readthedocs/privacy/__init__.py b/readthedocs/privacy/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/readthedocs/privacy/backend.py b/readthedocs/privacy/backend.py deleted file mode 100644 index 273de441221..00000000000 --- a/readthedocs/privacy/backend.py +++ /dev/null @@ -1,326 +0,0 @@ -"""Django Managers and Querysets to apply project privacy restrictions.""" -from __future__ import absolute_import -from builtins import object -from django.db import models - -from guardian.shortcuts import get_objects_for_user - -from readthedocs.builds.constants import BRANCH -from readthedocs.builds.constants import TAG -from readthedocs.builds.constants import LATEST -from readthedocs.builds.constants import LATEST_VERBOSE_NAME -from readthedocs.builds.constants import STABLE -from readthedocs.builds.constants import STABLE_VERBOSE_NAME -from readthedocs.core.utils.extend import (SettingsOverrideObject, - get_override_class) -from readthedocs.projects import constants - - -class ProjectQuerySet(models.QuerySet): - - """Projects take into account their own privacy_level setting.""" - - use_for_related_fields = True - - def _add_user_repos(self, queryset, user): - if user.has_perm('projects.view_project'): - return self.all().distinct() - if user.is_authenticated(): - user_queryset = get_objects_for_user(user, 'projects.view_project') - queryset = user_queryset | queryset - return queryset.distinct() - - def for_user_and_viewer(self, user, viewer): - """Show projects that a user owns, that another user can see.""" - queryset = self.filter(privacy_level=constants.PUBLIC) - queryset = self._add_user_repos(queryset, viewer) - queryset = queryset.filter(users__in=[user]) - return queryset - - def for_admin_user(self, user=None): - if user.is_authenticated(): - return self.filter(users__in=[user]) - return self.none() - - def public(self, user=None): - queryset = self.filter(privacy_level=constants.PUBLIC) - if user: - return self._add_user_repos(queryset, user) - return queryset - - def protected(self, user=None): - queryset = self.filter(privacy_level__in=[constants.PUBLIC, constants.PROTECTED]) - if user: - return self._add_user_repos(queryset, user) - return queryset - - def private(self, user=None): - queryset = self.filter(privacy_level=constants.PRIVATE) - if user: - return self._add_user_repos(queryset, user) - return queryset - - # Aliases - - def dashboard(self, user=None): - return self.for_admin_user(user) - - def api(self, user=None): - return self.public(user) - - -class VersionManager(models.Manager): - - """Version manager for manager only queries - - For queries not suitable for the :py:cls:`VersionQuerySet`, such as create - queries. - """ - - @classmethod - def from_queryset(cls, queryset_class, class_name=None): - # This is overridden because :py:meth:`models.Manager.from_queryset` - # uses `inspect` to retrieve the class methods, and the proxy class has - # no direct members. - queryset_class = get_override_class( - VersionQuerySet, - VersionQuerySet._default_class # pylint: disable=protected-access - ) - return super(VersionManager, cls).from_queryset(queryset_class, class_name) - - def create_stable(self, **kwargs): - defaults = { - 'slug': STABLE, - 'verbose_name': STABLE_VERBOSE_NAME, - 'machine': True, - 'active': True, - 'identifier': STABLE, - 'type': TAG, - } - defaults.update(kwargs) - return self.create(**defaults) - - def create_latest(self, **kwargs): - defaults = { - 'slug': LATEST, - 'verbose_name': LATEST_VERBOSE_NAME, - 'machine': True, - 'active': True, - 'identifier': LATEST, - 'type': BRANCH, - } - defaults.update(kwargs) - return self.create(**defaults) - - -class VersionQuerySetBase(models.QuerySet): - - """Versions take into account their own privacy_level setting.""" - - use_for_related_fields = True - - def _add_user_repos(self, queryset, user): - if user.has_perm('builds.view_version'): - return self.all().distinct() - if user.is_authenticated(): - user_queryset = get_objects_for_user(user, 'builds.view_version') - queryset = user_queryset | queryset - return queryset.distinct() - - def public(self, user=None, project=None, only_active=True): - queryset = self.filter(privacy_level=constants.PUBLIC) - if user: - queryset = self._add_user_repos(queryset, user) - if project: - queryset = queryset.filter(project=project) - if only_active: - queryset = queryset.filter(active=True) - return queryset - - def protected(self, user=None, project=None, only_active=True): - queryset = self.filter(privacy_level__in=[constants.PUBLIC, constants.PROTECTED]) - if user: - queryset = self._add_user_repos(queryset, user) - if project: - queryset = queryset.filter(project=project) - if only_active: - queryset = queryset.filter(active=True) - return queryset - - def private(self, user=None, project=None, only_active=True): - queryset = self.filter(privacy_level__in=[constants.PRIVATE]) - if user: - queryset = self._add_user_repos(queryset, user) - if project: - queryset = queryset.filter(project=project) - if only_active: - queryset = queryset.filter(active=True) - return queryset - - def api(self, user=None): - return self.public(user, only_active=False) - - def for_project(self, project): - """Return all versions for a project, including translations""" - return self.filter( - models.Q(project=project) | - models.Q(project__main_language_project=project) - ) - - -class VersionQuerySet(SettingsOverrideObject): - - _default_class = VersionQuerySetBase - - -class BuildQuerySet(models.QuerySet): - - """ - Build objects that are privacy aware. - - i.e. they take into account the privacy of the Version that they relate to. - """ - - use_for_related_fields = True - - def _add_user_repos(self, queryset, user=None): - if user.has_perm('builds.view_version'): - return self.all().distinct() - if user.is_authenticated(): - user_queryset = get_objects_for_user(user, 'builds.view_version') - pks = [p.pk for p in user_queryset] - queryset = self.filter(version__pk__in=pks) | queryset - return queryset.distinct() - - def public(self, user=None, project=None): - queryset = self.filter(version__privacy_level=constants.PUBLIC) - if user: - queryset = self._add_user_repos(queryset, user) - if project: - queryset = queryset.filter(project=project) - return queryset - - def api(self, user=None): - return self.public(user) - - -class RelatedProjectQuerySet(models.QuerySet): - - """ - A manager for things that relate to Project and need to get their perms from the project. - - This shouldn't be used as a subclass. - """ - - use_for_related_fields = True - project_field = 'project' - - def _add_user_repos(self, queryset, user=None): - # Hack around get_objects_for_user not supporting global perms - if user.has_perm('projects.view_project'): - return self.all().distinct() - if user.is_authenticated(): - # Add in possible user-specific views - project_qs = get_objects_for_user(user, 'projects.view_project') - pks = [p.pk for p in project_qs] - kwargs = {'%s__pk__in' % self.project_field: pks} - queryset = self.filter(**kwargs) | queryset - return queryset.distinct() - - def public(self, user=None, project=None): - kwargs = {'%s__privacy_level' % self.project_field: constants.PUBLIC} - queryset = self.filter(**kwargs) - if user: - queryset = self._add_user_repos(queryset, user) - if project: - queryset = queryset.filter(project=project) - return queryset - - def protected(self, user=None, project=None): - kwargs = { - '%s__privacy_level__in' % self.project_field: [constants.PUBLIC, constants.PROTECTED] - } - queryset = self.filter(**kwargs) - if user: - queryset = self._add_user_repos(queryset, user) - if project: - queryset = queryset.filter(project=project) - return queryset - - def private(self, user=None, project=None): - kwargs = { - '%s__privacy_level' % self.project_field: constants.PRIVATE, - } - queryset = self.filter(**kwargs) - if user: - queryset = self._add_user_repos(queryset, user) - if project: - queryset = queryset.filter(project=project) - return queryset - - def api(self, user=None): - return self.public(user) - - -class ParentRelatedProjectQuerySet(RelatedProjectQuerySet): - project_field = 'parent' - use_for_related_fields = True - - -class ChildRelatedProjectQuerySet(RelatedProjectQuerySet): - project_field = 'child' - use_for_related_fields = True - - -class RelatedBuildQuerySet(models.QuerySet): - - """For models with association to a project through :py:class:`Build`""" - - use_for_related_fields = True - - def _add_user_repos(self, queryset, user=None): - if user.has_perm('builds.view_version'): - return self.all().distinct() - if user.is_authenticated(): - user_queryset = get_objects_for_user(user, 'builds.view_version') - pks = [p.pk for p in user_queryset] - queryset = self.filter( - build__version__pk__in=pks) | queryset - return queryset.distinct() - - def public(self, user=None, project=None): - queryset = self.filter(build__version__privacy_level=constants.PUBLIC) - if user: - queryset = self._add_user_repos(queryset, user) - if project: - queryset = queryset.filter(build__project=project) - return queryset - - def api(self, user=None): - return self.public(user) - - -class RelatedUserQuerySet(models.QuerySet): - - """For models with relations through :py:class:`User`""" - - def api(self, user=None): - """Return objects for user""" - if not user.is_authenticated(): - return self.none() - return self.filter(users=user) - - -class AdminPermission(object): - - @classmethod - def is_admin(cls, user, project): - return user in project.users.all() - - @classmethod - def is_member(cls, user, obj): - return user in obj.users.all() - - -class AdminNotAuthorized(ValueError): - pass diff --git a/readthedocs/privacy/backends/__init__.py b/readthedocs/privacy/backends/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/readthedocs/privacy/backends/managers.py b/readthedocs/privacy/backends/managers.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/readthedocs/privacy/backends/permissions.py b/readthedocs/privacy/backends/permissions.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/readthedocs/privacy/loader.py b/readthedocs/privacy/loader.py deleted file mode 100644 index 7e71ee6dc7c..00000000000 --- a/readthedocs/privacy/loader.py +++ /dev/null @@ -1,62 +0,0 @@ -"""A namespace of privacy classes configured by settings. - -Importing classes from this module allows the classes used to be overridden -using Django settings. - -""" - -from __future__ import absolute_import -from readthedocs.core.utils.extend import SettingsOverrideObject -from readthedocs.privacy import backend - - -# Managers -from readthedocs.privacy.backends import syncers - - -class ProjectQuerySet(SettingsOverrideObject): - _default_class = backend.ProjectQuerySet - _override_setting = 'PROJECT_MANAGER' - - -# VersionQuerySet was replaced by SettingsOverrideObject -class VersionManager(SettingsOverrideObject): - _default_class = backend.VersionManager - _override_setting = 'VERSION_MANAGER' - - -class BuildQuerySet(SettingsOverrideObject): - _default_class = backend.BuildQuerySet - _override_setting = 'BUILD_MANAGER' - - -class RelatedProjectQuerySet(SettingsOverrideObject): - _default_class = backend.RelatedProjectQuerySet - _override_setting = 'RELATED_PROJECT_MANAGER' - - -class RelatedBuildQuerySet(SettingsOverrideObject): - _default_class = backend.RelatedBuildQuerySet - _override_setting = 'RELATED_BUILD_MANAGER' - - -class RelatedUserQuerySet(SettingsOverrideObject): - _default_class = backend.RelatedUserQuerySet - _override_setting = 'RELATED_USER_MANAGER' - - -class ChildRelatedProjectQuerySet(SettingsOverrideObject): - _default_class = backend.ChildRelatedProjectQuerySet - _override_setting = 'CHILD_RELATED_PROJECT_MANAGER' - - -# Permissions -class AdminPermission(SettingsOverrideObject): - _default_class = backend.AdminPermission - _override_setting = 'ADMIN_PERMISSION' - - -# Syncers -class Syncer(SettingsOverrideObject): - _default_class = syncers.LocalSyncer - _override_setting = 'FILE_SYNCER' diff --git a/readthedocs/privacy/models.py b/readthedocs/privacy/models.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/readthedocs/privacy/templatetags/__init__.py b/readthedocs/privacy/templatetags/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/readthedocs/projects/forms.py b/readthedocs/projects/forms.py index 4d3cd3131ee..910b475cbd0 100644 --- a/readthedocs/projects/forms.py +++ b/readthedocs/projects/forms.py @@ -15,13 +15,13 @@ from textclassifier.validators import ClassifierValidator from readthedocs.builds.constants import TAG +from readthedocs.core.permissions import AdminPermission from readthedocs.core.utils import trigger_build, slugify from readthedocs.integrations.models import Integration from readthedocs.oauth.models import RemoteRepository from readthedocs.projects import constants from readthedocs.projects.exceptions import ProjectSpamError from readthedocs.projects.models import Project, EmailHook, WebHook, Domain -from readthedocs.privacy.loader import AdminPermission from readthedocs.redirects.models import Redirect from future import standard_library diff --git a/readthedocs/projects/models.py b/readthedocs/projects/models.py index 1eff7b84cff..6b6540043d7 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -20,10 +20,13 @@ from readthedocs.core.utils import broadcast, slugify from readthedocs.restapi.client import api as apiv2 from readthedocs.builds.constants import LATEST, LATEST_VERBOSE_NAME, STABLE -from readthedocs.privacy.loader import (RelatedProjectQuerySet, ProjectQuerySet, - ChildRelatedProjectQuerySet) from readthedocs.projects import constants from readthedocs.projects.exceptions import ProjectImportError +from readthedocs.projects.querysets import ( + ProjectQuerySet, + RelatedProjectQuerySet, + ChildRelatedProjectQuerySet +) from readthedocs.projects.templatetags.projects_tags import sort_version_aware from readthedocs.projects.utils import make_api_version from readthedocs.projects.version_handling import determine_stable_version diff --git a/readthedocs/projects/querysets.py b/readthedocs/projects/querysets.py new file mode 100644 index 00000000000..5f7688d5529 --- /dev/null +++ b/readthedocs/projects/querysets.py @@ -0,0 +1,150 @@ +"""Project model QuerySet classes""" + +from __future__ import absolute_import + +from django.db import models +from guardian.shortcuts import get_objects_for_user + +from . import constants +from readthedocs.core.utils.extend import SettingsOverrideObject + + +class ProjectQuerySetBase(models.QuerySet): + + """Projects take into account their own privacy_level setting.""" + + use_for_related_fields = True + + def _add_user_repos(self, queryset, user): + if user.has_perm('projects.view_project'): + return self.all().distinct() + if user.is_authenticated(): + user_queryset = get_objects_for_user(user, 'projects.view_project') + queryset = user_queryset | queryset + return queryset.distinct() + + def for_user_and_viewer(self, user, viewer): + """Show projects that a user owns, that another user can see.""" + queryset = self.filter(privacy_level=constants.PUBLIC) + queryset = self._add_user_repos(queryset, viewer) + queryset = queryset.filter(users__in=[user]) + return queryset + + def for_admin_user(self, user=None): + if user.is_authenticated(): + return self.filter(users__in=[user]) + return self.none() + + def public(self, user=None): + queryset = self.filter(privacy_level=constants.PUBLIC) + if user: + return self._add_user_repos(queryset, user) + return queryset + + def protected(self, user=None): + queryset = self.filter(privacy_level__in=[constants.PUBLIC, constants.PROTECTED]) + if user: + return self._add_user_repos(queryset, user) + return queryset + + def private(self, user=None): + queryset = self.filter(privacy_level=constants.PRIVATE) + if user: + return self._add_user_repos(queryset, user) + return queryset + + # Aliases + + def dashboard(self, user=None): + return self.for_admin_user(user) + + def api(self, user=None): + return self.public(user) + + +class ProjectQuerySet(SettingsOverrideObject): + _default_class = ProjectQuerySetBase + _override_setting = 'PROJECT_MANAGER' + + +class RelatedProjectQuerySetBase(models.QuerySet): + + """ + A manager for things that relate to Project and need to get their perms from the project. + + This shouldn't be used as a subclass. + """ + + use_for_related_fields = True + project_field = 'project' + + def _add_user_repos(self, queryset, user=None): + # Hack around get_objects_for_user not supporting global perms + if user.has_perm('projects.view_project'): + return self.all().distinct() + if user.is_authenticated(): + # Add in possible user-specific views + project_qs = get_objects_for_user(user, 'projects.view_project') + pks = [p.pk for p in project_qs] + kwargs = {'%s__pk__in' % self.project_field: pks} + queryset = self.filter(**kwargs) | queryset + return queryset.distinct() + + def public(self, user=None, project=None): + kwargs = {'%s__privacy_level' % self.project_field: constants.PUBLIC} + queryset = self.filter(**kwargs) + if user: + queryset = self._add_user_repos(queryset, user) + if project: + queryset = queryset.filter(project=project) + return queryset + + def protected(self, user=None, project=None): + kwargs = { + '%s__privacy_level__in' % self.project_field: [constants.PUBLIC, constants.PROTECTED] + } + queryset = self.filter(**kwargs) + if user: + queryset = self._add_user_repos(queryset, user) + if project: + queryset = queryset.filter(project=project) + return queryset + + def private(self, user=None, project=None): + kwargs = { + '%s__privacy_level' % self.project_field: constants.PRIVATE, + } + queryset = self.filter(**kwargs) + if user: + queryset = self._add_user_repos(queryset, user) + if project: + queryset = queryset.filter(project=project) + return queryset + + def api(self, user=None): + return self.public(user) + + +class RelatedProjectQuerySet(SettingsOverrideObject): + _default_class = RelatedProjectQuerySetBase + _override_setting = 'RELATED_PROJECT_MANAGER' + + +class ParentRelatedProjectQuerySetBase(RelatedProjectQuerySetBase): + project_field = 'parent' + use_for_related_fields = True + + +class ParentRelatedProjectQuerySet(SettingsOverrideObject): + _default_class = ParentRelatedProjectQuerySetBase + _override_setting = 'RELATED_PROJECT_MANAGER' + + +class ChildRelatedProjectQuerySetBase(RelatedProjectQuerySetBase): + project_field = 'child' + use_for_related_fields = True + + +class ChildRelatedProjectQuerySet(SettingsOverrideObject): + _default_class = ChildRelatedProjectQuerySetBase + _override_setting = 'RELATED_PROJECT_MANAGER' diff --git a/readthedocs/projects/tasks.py b/readthedocs/projects/tasks.py index c32c1c50314..6d64d8f95cc 100644 --- a/readthedocs/projects/tasks.py +++ b/readthedocs/projects/tasks.py @@ -23,8 +23,12 @@ from django.utils.translation import ugettext_lazy as _ from slumber.exceptions import HttpClientError +from .constants import LOG_TEMPLATE +from .exceptions import ProjectImportError +from .models import ImportedFile, Project, Domain +from .signals import before_vcs, after_vcs, before_build, after_build +from .utils import make_api_version, make_api_project from readthedocs_build.config import ConfigError - from readthedocs.builds.constants import (LATEST, BUILD_STATE_CLONING, BUILD_STATE_INSTALLING, @@ -41,18 +45,13 @@ DockerEnvironment) from readthedocs.doc_builder.exceptions import BuildEnvironmentError from readthedocs.doc_builder.python_environments import Virtualenv, Conda -from readthedocs.projects.exceptions import ProjectImportError -from readthedocs.projects.models import ImportedFile, Project, Domain -from readthedocs.projects.utils import make_api_version, make_api_project -from readthedocs.projects.constants import LOG_TEMPLATE -from readthedocs.privacy.loader import Syncer +from readthedocs.builds.syncers import Syncer from readthedocs.search.parse_json import process_all_json_files from readthedocs.search.utils import process_mkdocs_json from readthedocs.restapi.utils import index_search_request from readthedocs.vcs_support import utils as vcs_support_utils from readthedocs.api.client import api as api_v1 from readthedocs.restapi.client import api as api_v2 -from readthedocs.projects.signals import before_vcs, after_vcs, before_build, after_build from readthedocs.core.resolver import resolve_path diff --git a/readthedocs/restapi/permissions.py b/readthedocs/restapi/permissions.py index a532e203229..a6a5abc58c9 100644 --- a/readthedocs/restapi/permissions.py +++ b/readthedocs/restapi/permissions.py @@ -3,7 +3,8 @@ from __future__ import absolute_import from rest_framework import permissions -from readthedocs.privacy.backend import AdminPermission + +from readthedocs.core.permissions import AdminPermission class IsOwner(permissions.BasePermission): diff --git a/readthedocs/rtd_tests/tests/test_project_querysets.py b/readthedocs/rtd_tests/tests/test_project_querysets.py new file mode 100644 index 00000000000..237af53c3d4 --- /dev/null +++ b/readthedocs/rtd_tests/tests/test_project_querysets.py @@ -0,0 +1,29 @@ +from __future__ import absolute_import + +import django_dynamic_fixture as fixture +from django.test import TestCase + +from readthedocs.projects.models import Project, ProjectRelationship +from readthedocs.projects.querysets import (ParentRelatedProjectQuerySet, + ChildRelatedProjectQuerySet) + + +class ProjectQuerySetTests(TestCase): + + def test_subproject_queryset_attributes(self): + self.assertEqual(ParentRelatedProjectQuerySet.project_field, 'parent') + self.assertTrue(ParentRelatedProjectQuerySet.use_for_related_fields) + self.assertEqual(ChildRelatedProjectQuerySet.project_field, 'child') + self.assertTrue(ChildRelatedProjectQuerySet.use_for_related_fields) + + def test_subproject_queryset_as_manager_gets_correct_class(self): + mgr = ChildRelatedProjectQuerySet.as_manager() + self.assertEqual( + mgr.__class__.__name__, + 'ManagerFromChildRelatedProjectQuerySetBase' + ) + mgr = ParentRelatedProjectQuerySet.as_manager() + self.assertEqual( + mgr.__class__.__name__, + 'ManagerFromParentRelatedProjectQuerySetBase' + ) diff --git a/readthedocs/rtd_tests/tests/test_views.py b/readthedocs/rtd_tests/tests/test_views.py index 480934aefc9..8d9256c74da 100644 --- a/readthedocs/rtd_tests/tests/test_views.py +++ b/readthedocs/rtd_tests/tests/test_views.py @@ -7,10 +7,10 @@ from django_dynamic_fixture import new from readthedocs.builds.constants import LATEST +from readthedocs.core.permissions import AdminPermission from readthedocs.projects.models import ImportedFile from readthedocs.projects.models import Project from readthedocs.projects.forms import UpdateProjectForm -from readthedocs.privacy.loader import AdminPermission class Testmaker(TestCase): diff --git a/readthedocs/settings/base.py b/readthedocs/settings/base.py index 4d51f8ade7c..c7473fc496a 100644 --- a/readthedocs/settings/base.py +++ b/readthedocs/settings/base.py @@ -98,7 +98,6 @@ def INSTALLED_APPS(self): # noqa 'readthedocs.redirects', 'readthedocs.rtd_tests', 'readthedocs.restapi', - 'readthedocs.privacy', 'readthedocs.gold', 'readthedocs.donate', 'readthedocs.payments', diff --git a/readthedocs/settings/dev.py b/readthedocs/settings/dev.py index ccb85e20b58..d711b3a3c09 100644 --- a/readthedocs/settings/dev.py +++ b/readthedocs/settings/dev.py @@ -41,7 +41,7 @@ def DATABASES(self): # noqa } EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' - FILE_SYNCER = 'readthedocs.privacy.backends.syncers.LocalSyncer' + FILE_SYNCER = 'readthedocs.builds.syncers.LocalSyncer' NGINX_X_ACCEL_REDIRECT = True