from copy import deepcopy from django.contrib import admin from django.contrib.admin import site from django.contrib.auth import get_user_model from django.contrib.auth.admin import UserAdmin from django.contrib.sites.models import Site from django.db import OperationalError, ProgrammingError from django.utils.translation import gettext_lazy as _ from cms.admin.forms import ( GlobalPagePermissionAdminForm, PagePermissionInlineAdminForm, ViewRestrictionInlineAdminForm, ) from cms.exceptions import NoPermissionsException from cms.models import GlobalPagePermission, PagePermission from cms.utils import page_permissions, permissions from cms.utils.conf import get_cms_setting from cms.utils.helpers import classproperty PERMISSION_ADMIN_INLINES = [] user_model = get_user_model() admin_class = UserAdmin for model, admin_instance in site._registry.items(): if model == user_model: admin_class = admin_instance.__class__ class TabularInline(admin.TabularInline): pass class PagePermissionInlineAdmin(TabularInline): model = PagePermission # use special form, so we can override of user and group field form = PagePermissionInlineAdminForm extra = 0 # edit page load time boost show_with_view_permissions = False def has_change_permission(self, request, obj=None): if not obj: return False return page_permissions.user_can_change_page_permissions( request.user, page=obj, site=obj.site, ) def has_add_permission(self, request, obj=None): return self.has_change_permission(request, obj) @classproperty def raw_id_fields(cls): # Dynamically set raw_id_fields based on settings threshold = get_cms_setting('RAW_ID_USERS') # Given a fresh django-cms install and a django settings with the # CMS_RAW_ID_USERS = CMS_PERMISSION = True # django throws an OperationalError (sqlite) or ProgrammingError # (postgres) when running ./manage migrate because the user table # doesn't exist yet. try: threshold = threshold and get_user_model().objects.count() > threshold except (OperationalError, ProgrammingError): threshold = False return ['user'] if threshold else [] def get_queryset(self, request): """ Queryset change, so user with global change permissions can see all permissions. Otherwise user can see only permissions for peoples which are under him (he can't see his permissions, because this will lead to violation, when he can add more power to himself) """ site = Site.objects.get_current(request) try: # can see only permissions for users which are under him in tree qs = self.model.objects.subordinate_to_user(request.user, site) except NoPermissionsException: return self.model.objects.none() return qs.filter(can_view=self.show_with_view_permissions) def get_formset(self, request, obj=None, **kwargs): """ Some fields may be excluded here. User can change only permissions which are available for him. E.g. if user does not haves can_change flag, he can't change assign can_change permissions. """ exclude = self.exclude or [] if obj: user = request.user if not obj.has_add_permission(user): exclude.append('can_add') if not obj.has_delete_permission(user): exclude.append('can_delete') if not obj.has_publish_permission(user): exclude.append('can_publish') if not obj.has_advanced_settings_permission(user): exclude.append('can_change_advanced_settings') if not obj.has_move_page_permission(user): exclude.append('can_move_page') kwargs['exclude'] = exclude self.form._page = obj formset_cls = super().get_formset(request, obj=obj, **kwargs) qs = self.get_queryset(request) if obj is not None: qs = qs.filter(page=obj) formset_cls._queryset = qs return formset_cls class ViewRestrictionInlineAdmin(PagePermissionInlineAdmin): extra = 0 # edit page load time boost form = ViewRestrictionInlineAdminForm verbose_name = _("View restriction") verbose_name_plural = _("View restrictions") show_with_view_permissions = True class GlobalPagePermissionAdmin(admin.ModelAdmin): list_display = ['user', 'group', 'can_change', 'can_delete', 'can_publish', 'can_change_permissions'] list_filter = ['user', 'group', 'can_change', 'can_delete', 'can_publish', 'can_change_permissions'] form = GlobalPagePermissionAdminForm search_fields = [] for field in admin_class.search_fields: search_fields.append("user__%s" % field) search_fields.append('group__name') list_display.append('can_change_advanced_settings') list_filter.append('can_change_advanced_settings') def get_list_filter(self, request): threshold = get_cms_setting('RAW_ID_USERS') try: threshold = threshold and get_user_model().objects.count() > threshold except (OperationalError, ProgrammingError): threshold = False filter_copy = deepcopy(self.list_filter) if threshold: filter_copy.remove('user') return filter_copy def get_exclude(self, request, obj=None): """Offer only the ``can_*`` flags the acting user may hand out. Mirrors ``PagePermissionInlineAdmin.get_formset``: a manager must not be able to grant -- to themselves or anyone else -- a right they do not hold (CWE-269). Excluding the field keeps it out of the rendered form *and* out of a crafted POST. Whether a flag the manager holds on some site may be granted for the sites actually picked is settled in the form's ``clean()``, once ``sites`` is known. """ exclude = list(super().get_exclude(request, obj) or []) grantable = permissions.get_grantable_global_permissions(request.user) exclude.extend( field for field in GlobalPagePermission.get_all_permissions() if field not in grantable and field not in exclude ) return exclude def get_form(self, request, obj=None, **kwargs): form_class = super().get_form(request, obj, **kwargs) # The form caps each granted flag to the sites the manager holds it on, # so it needs to know who is acting. ``modelform_factory`` returns a # fresh subclass per call, so this is per-request state rather than # shared mutation of the declared form class. form_class._current_user = request.user return form_class def has_add_permission(self, request): site = Site.objects.get_current(request) return permissions.user_can_add_global_permissions(request.user, site) def has_change_permission(self, request, obj=None): site = Site.objects.get_current(request) return ( permissions.user_can_change_global_permissions(request.user, site) and self._can_manage_grant(request, obj) ) def has_delete_permission(self, request, obj=None): site = Site.objects.get_current(request) return ( permissions.user_can_delete_global_permissions(request.user, site) and self._can_manage_grant(request, obj) ) def _can_manage_grant(self, request, obj): """Only let a manager edit or delete a grant they could have created. A grant for a site the manager may not manage permissions on, or holding flags they may not hand out on its sites, is out of reach: its ``user``, ``group`` and ``sites`` would otherwise let them move those flags to themselves or to other sites (CWE-269). """ if obj is None: return True held = {flag for flag in GlobalPagePermission.get_all_permissions() if getattr(obj, flag)} site_ids = list(obj.sites.values_list("pk", flat=True)) grantable = permissions.get_grantable_global_permissions(request.user, site_ids) return "can_change_permissions" in grantable and held <= grantable @classproperty def raw_id_fields(cls): # Dynamically set raw_id_fields based on settings threshold = get_cms_setting('RAW_ID_USERS') # Given a fresh django-cms install and a django settings with the # CMS_RAW_ID_USERS = CMS_PERMISSION = True # django throws an OperationalError (sqlite) or ProgrammingError # (postgres) when running ./manage migrate because the user table # doesn't exist yet. try: threshold = threshold and get_user_model().objects.count() > threshold except (OperationalError, ProgrammingError): threshold = False return ['user'] if threshold else [] if get_cms_setting('PERMISSION'): admin.site.register(GlobalPagePermission, GlobalPagePermissionAdmin) PERMISSION_ADMIN_INLINES.extend([ ViewRestrictionInlineAdmin, PagePermissionInlineAdmin, ])