forked from ungleich-public/dynamicweb
382 lines
14 KiB
Python
382 lines
14 KiB
Python
from django.apps import apps
|
|
from django.conf import settings
|
|
from django.contrib.auth.models import Group, UserManager
|
|
from django.contrib.sites.models import Site
|
|
from django.core.exceptions import ImproperlyConfigured, ValidationError
|
|
from django.db import models
|
|
from django.db.models import Q
|
|
from django.utils.encoding import force_str
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
from cms.models import Page
|
|
from cms.models.managers import (
|
|
GlobalPagePermissionManager,
|
|
PagePermissionManager,
|
|
)
|
|
|
|
# Cannot use contrib.auth.get_user_model() at compile time.
|
|
user_app_name, user_model_name = settings.AUTH_USER_MODEL.rsplit('.', 1)
|
|
User = None
|
|
try:
|
|
User = apps.get_registered_model(user_app_name, user_model_name)
|
|
except KeyError:
|
|
pass
|
|
if User is None:
|
|
raise ImproperlyConfigured(
|
|
"You have defined a custom user model %s, but the app %s is not "
|
|
"in settings.INSTALLED_APPS" % (settings.AUTH_USER_MODEL, user_app_name)
|
|
)
|
|
|
|
|
|
# NOTE: those are not just numbers!! we will do binary AND on them,
|
|
# so pay attention when adding/changing them, or MASKs..
|
|
|
|
#: Access to the page itself
|
|
ACCESS_PAGE = 1
|
|
|
|
#: Access to immediate children (1 level)
|
|
ACCESS_CHILDREN = 2
|
|
|
|
#: Access to page itself and immediate children (1 level)
|
|
ACCESS_PAGE_AND_CHILDREN = 3
|
|
|
|
#: Access to all children (first level and also their children)
|
|
ACCESS_DESCENDANTS = 4
|
|
|
|
#: Access to page itself and all children (first level and also their children)
|
|
ACCESS_PAGE_AND_DESCENDANTS = 5
|
|
|
|
# binary masks for ACCESS permissions
|
|
MASK_PAGE = 1
|
|
MASK_CHILDREN = 2
|
|
MASK_DESCENDANTS = 4
|
|
|
|
ACCESS_CHOICES = (
|
|
(ACCESS_PAGE, _('Current page')),
|
|
(ACCESS_CHILDREN, _('Page children (immediate)')),
|
|
(ACCESS_PAGE_AND_CHILDREN, _('Page and children (immediate)')),
|
|
(ACCESS_DESCENDANTS, _('Page descendants')),
|
|
(ACCESS_PAGE_AND_DESCENDANTS, _('Page and descendants')),
|
|
)
|
|
|
|
#: Validation messages raised when a permission is granted without the
|
|
#: prerequisite "can edit" (``can_change``) permission. Defined once here so the
|
|
#: model's ``clean()`` and the admin ``PagePermissionInlineAdminForm`` cannot
|
|
#: drift apart. Each value is a complete sentence (rather than assembled from
|
|
#: translated fragments) so translators receive the whole string and per-language
|
|
#: word order is preserved. Keys are the permission flags that trigger them.
|
|
EDIT_PERMISSION_REQUIRED_MESSAGES = {
|
|
"can_add": _(
|
|
"Users can't create a page without also being able to change it. "
|
|
"Please also enable 'Can edit'."
|
|
),
|
|
"can_delete": _(
|
|
"Users can't delete a page without also being able to change it. "
|
|
"Please also enable 'Can edit'."
|
|
),
|
|
"can_publish": _(
|
|
"Users can't publish a page without also being able to change it. "
|
|
"Please also enable 'Can edit'."
|
|
),
|
|
"can_change_advanced_settings": _(
|
|
"Users can't change a page's advanced settings without also being able "
|
|
"to change it. Please also enable 'Can edit'."
|
|
),
|
|
"can_change_permissions": _(
|
|
"Users can't change a page's permissions without also being able to "
|
|
"change it. Please also enable 'Can edit'."
|
|
),
|
|
"can_move_page": _(
|
|
"Users can't move a page without also being able to change it. "
|
|
"Please also enable 'Can edit'."
|
|
),
|
|
}
|
|
|
|
|
|
class AbstractPagePermission(models.Model):
|
|
"""Abstract page permissions
|
|
"""
|
|
|
|
# who:
|
|
user = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.CASCADE,
|
|
verbose_name=_("user"),
|
|
blank=True,
|
|
null=True,
|
|
)
|
|
group = models.ForeignKey(
|
|
Group,
|
|
on_delete=models.CASCADE,
|
|
verbose_name=_("group"),
|
|
blank=True,
|
|
null=True,
|
|
)
|
|
|
|
# what:
|
|
can_change = models.BooleanField(_("can edit"), default=True)
|
|
can_add = models.BooleanField(_("can add"), default=True)
|
|
can_delete = models.BooleanField(_("can delete"), default=True)
|
|
can_publish = models.BooleanField(_("can publish"), default=True)
|
|
can_change_advanced_settings = models.BooleanField(_("can change advanced settings"), default=False)
|
|
can_change_permissions = models.BooleanField(
|
|
_("can change permissions"),
|
|
default=False,
|
|
help_text=_("Allows this user to grant and revoke page permissions for other users."),
|
|
)
|
|
can_move_page = models.BooleanField(_("can move"), default=True)
|
|
can_view = models.BooleanField(
|
|
_("can view restricted pages"),
|
|
default=False,
|
|
help_text=_(
|
|
"Grants frontend view access. Note: as soon as any user or group is "
|
|
"given view access to a page, that page becomes restricted — only "
|
|
"users/groups with view access can then see it on the frontend."
|
|
),
|
|
)
|
|
|
|
class Meta:
|
|
abstract = True
|
|
app_label = 'cms'
|
|
|
|
def clean(self):
|
|
super().clean()
|
|
|
|
if not self.user and not self.group:
|
|
raise ValidationError(_('Please select user or group.'))
|
|
|
|
if self.can_change:
|
|
return
|
|
|
|
for flag, message in EDIT_PERMISSION_REQUIRED_MESSAGES.items():
|
|
if getattr(self, flag):
|
|
raise ValidationError(message)
|
|
|
|
@property
|
|
def audience(self):
|
|
"""Return audience by priority, so: All or User, Group
|
|
"""
|
|
targets = filter(lambda item: item, (self.user, self.group,))
|
|
return ", ".join([force_str(t) for t in targets]) or 'No one'
|
|
|
|
def save(self, *args, **kwargs):
|
|
if not self.user and not self.group:
|
|
# don't allow `empty` objects
|
|
raise TypeError(_('Please select user or group.'))
|
|
return super().save(*args, **kwargs)
|
|
|
|
def get_configured_actions(self):
|
|
actions = [action for action in self.get_permissions_by_action()
|
|
if self.has_configured_action(action)]
|
|
return actions
|
|
|
|
def has_configured_action(self, action):
|
|
permissions = self.get_permissions_by_action()[action]
|
|
return all(getattr(self, perm) for perm in permissions)
|
|
|
|
@classmethod
|
|
def get_all_permissions(cls):
|
|
perms = [
|
|
'can_add',
|
|
'can_change',
|
|
'can_delete',
|
|
'can_publish',
|
|
'can_change_advanced_settings',
|
|
'can_change_permissions',
|
|
'can_move_page',
|
|
'can_view',
|
|
]
|
|
return perms
|
|
|
|
@classmethod
|
|
def get_permissions_by_action(cls):
|
|
# Maps an action to the required flags on the
|
|
# PagePermission model or GlobalPagePermission model
|
|
permissions_by_action = {
|
|
'add_page': ['can_add', 'can_change'],
|
|
'change_page': ['can_change'],
|
|
'change_page_advanced_settings': ['can_change', 'can_change_advanced_settings'],
|
|
'change_page_permissions': ['can_change', 'can_change_permissions'],
|
|
'delete_page': ['can_change', 'can_delete'],
|
|
'delete_page_translation': ['can_change', 'can_delete'],
|
|
'publish_page': ['can_change', 'can_publish'],
|
|
'move_page': ['can_change', 'can_move_page'],
|
|
'view_page': ['can_view'],
|
|
}
|
|
return permissions_by_action
|
|
|
|
|
|
class GlobalPagePermission(AbstractPagePermission):
|
|
"""Permissions for all pages (global).
|
|
"""
|
|
can_recover_page = models.BooleanField(
|
|
verbose_name=_("can recover pages"),
|
|
default=True,
|
|
help_text=_("can recover any deleted page"),
|
|
)
|
|
sites = models.ManyToManyField(
|
|
to=Site,
|
|
blank=True,
|
|
help_text=_('If none selected, user haves granted permissions to all sites.'),
|
|
verbose_name=_('sites'),
|
|
)
|
|
|
|
objects = GlobalPagePermissionManager()
|
|
|
|
class Meta:
|
|
verbose_name = _('Page global permission')
|
|
verbose_name_plural = _('Pages global permissions')
|
|
app_label = 'cms'
|
|
|
|
def __str__(self):
|
|
return "%s :: GLOBAL" % self.audience
|
|
|
|
|
|
class PermissionTuple(tuple):
|
|
"""Represents a permission tuple containing grant level and permission path.
|
|
|
|
This tuple subclass provides utility methods for checking if a page path
|
|
matches the permission scope defined by the grant level and path.
|
|
|
|
:ivar [0] grant_on: The grant level (ACCESS_PAGE, ACCESS_CHILDREN, etc.)
|
|
:ivar [1] perm_path: The page path associated with this permission
|
|
"""
|
|
def contains(self, path: str, steplen: int = Page.steplen) -> bool:
|
|
"""Check if the given path is contained within this permission's scope.
|
|
|
|
:param path: The page path to check
|
|
:type path: str
|
|
:param steplen: The step length used for path hierarchy (default: Page.steplen)
|
|
:type steplen: int
|
|
:return: True if the path is contained within this permission's scope
|
|
:rtype: bool
|
|
"""
|
|
grant_on, perm_path = self
|
|
if grant_on == ACCESS_PAGE:
|
|
return path == perm_path
|
|
elif grant_on == ACCESS_CHILDREN:
|
|
return path.startswith(perm_path) and len(path) == len(perm_path) + steplen
|
|
elif grant_on == ACCESS_DESCENDANTS:
|
|
return path.startswith(perm_path) and len(path) > len(perm_path)
|
|
elif grant_on == ACCESS_PAGE_AND_DESCENDANTS:
|
|
return path.startswith(perm_path)
|
|
elif grant_on == ACCESS_PAGE_AND_CHILDREN:
|
|
return path.startswith(perm_path) and len(path) <= len(perm_path) + steplen
|
|
return False
|
|
|
|
def allow_list(self, filter: str = "", steplen: int = Page.steplen) -> Q:
|
|
"""Generate a Django Q object for filtering pages based on permission scope.
|
|
|
|
:param filter: The field name prefix for the query filter (default: "")
|
|
:type filter: str
|
|
:param steplen: The step length used for path hierarchy (default: Page.steplen)
|
|
:type steplen: int
|
|
:return: A Q object representing the page filter for this permission's scope
|
|
:rtype: Q
|
|
"""
|
|
if filter != "":
|
|
filter = f"{filter}__"
|
|
grant_on, path = self
|
|
depth = len(path) // steplen
|
|
if grant_on == ACCESS_PAGE:
|
|
return Q(**{f"{filter}path": path})
|
|
elif grant_on == ACCESS_CHILDREN:
|
|
return Q(**{f"{filter}path__startswith": path, f"{filter}depth": depth + 1})
|
|
elif grant_on == ACCESS_DESCENDANTS:
|
|
return Q(**{f"{filter}path__startswith": path, f"{filter}depth__gt": depth})
|
|
elif grant_on == ACCESS_PAGE_AND_DESCENDANTS:
|
|
return Q(**{f"{filter}path__startswith": path})
|
|
elif grant_on == ACCESS_PAGE_AND_CHILDREN:
|
|
return Q(**{f"{filter}path__startswith": path, f"{filter}depth__lte": depth + 1})
|
|
return Q()
|
|
|
|
|
|
class PagePermission(AbstractPagePermission):
|
|
"""Page permissions for a single page
|
|
"""
|
|
grant_on = models.IntegerField(_("Grant on"), choices=ACCESS_CHOICES, default=ACCESS_PAGE_AND_DESCENDANTS)
|
|
page = models.ForeignKey(Page, on_delete=models.CASCADE, null=True, blank=True, verbose_name=_("page"))
|
|
|
|
objects = PagePermissionManager()
|
|
|
|
class Meta:
|
|
verbose_name = _('Page permission')
|
|
verbose_name_plural = _('Page permissions')
|
|
app_label = 'cms'
|
|
|
|
def __str__(self):
|
|
page = self.page_id and force_str(self.page) or "None"
|
|
return f"{page} :: {self.audience} has: {force_str(self.get_grant_on_display())}"
|
|
|
|
def clean(self):
|
|
super().clean()
|
|
|
|
if self.can_add and self.grant_on == ACCESS_PAGE:
|
|
# this is a misconfiguration - user can add/move page to current
|
|
# page, but after he does this, he will not have permissions to
|
|
# access this page anymore, so avoid this.
|
|
message = _("Add page permission requires also access to children, "
|
|
"or descendants, otherwise added page can't be changed "
|
|
"by its creator.")
|
|
raise ValidationError(message)
|
|
|
|
def get_page_permission_tuple(self):
|
|
return PermissionTuple((self.grant_on, self.page.path))
|
|
|
|
def get_page_ids(self):
|
|
import warnings
|
|
|
|
from cms.utils.compat.warnings import RemovedInDjangoCMS60Warning
|
|
warnings.warn("get_page_ids is deprecated, "
|
|
"use get_page_permission_tuple instead", RemovedInDjangoCMS60Warning, stacklevel=2)
|
|
|
|
return self._get_page_ids()
|
|
|
|
def _get_page_ids(self):
|
|
if self.grant_on & MASK_PAGE:
|
|
yield self.page_id
|
|
|
|
if self.grant_on & MASK_CHILDREN:
|
|
children = self.page.get_child_pages().values_list('pk', flat=True)
|
|
|
|
yield from children
|
|
elif self.grant_on & MASK_DESCENDANTS:
|
|
if self.page._has_cached_hierarchy():
|
|
descendants = (page.pk for page in self.page.get_cached_descendants())
|
|
else:
|
|
descendants = self.page.get_descendant_pages().values_list('pk', flat=True).iterator()
|
|
|
|
yield from descendants
|
|
|
|
|
|
class PageUserManager(UserManager):
|
|
use_in_migrations = False
|
|
|
|
|
|
class PageUser(User):
|
|
"""Cms specific user data, required for permission system
|
|
"""
|
|
created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, related_name="created_users")
|
|
|
|
objects = PageUserManager()
|
|
|
|
class Meta:
|
|
verbose_name = _('User (page)')
|
|
verbose_name_plural = _('Users (page)')
|
|
app_label = 'cms'
|
|
|
|
|
|
class PageUserGroup(Group):
|
|
"""Cms specific group data, required for permission system
|
|
"""
|
|
created_by = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
related_name="created_usergroups"
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = _('User group (page)')
|
|
verbose_name_plural = _('User groups (page)')
|
|
app_label = 'cms'
|