dynamicweb/cms/models/permissionmodels.py

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'