digitalfabrik/integreat-cms

View on GitHub
integreat_cms/cms/views/pages/page_permission_actions.py

Summary

Maintainability
A
0 mins
Test Coverage
B
88%
import json
import logging
from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import StrEnum
from typing import Any

from django.contrib.auth import get_user_model
from django.core.exceptions import PermissionDenied
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render
from django.utils.translation import gettext as _
from django.views.decorators.http import require_POST

from ...decorators import permission_required
from ...forms import PageForm
from ...models import Page, Region, User

logger = logging.getLogger(__name__)


@require_POST
@permission_required("cms.change_page")
@permission_required("cms.grant_page_permissions")
# pylint: disable=unused-argument
def grant_page_permission_ajax(request: HttpRequest, region_slug: str) -> HttpResponse:
    """
    Grant a user editing or publishing permissions on a specific page object

    :param request: The current request
    :param region_slug: The slug of the current region

    :raises ~django.core.exceptions.PermissionDenied: If page permissions are disabled for this region or the user does
                                                      not have the permission to grant page permissions

    :return: The rendered page permission table
    """

    try:
        data = json.loads(request.body.decode("utf-8"))
        permission = get_permission(data)
        page = Page.objects.get(id=data.get("page_id"))
        user = get_user_model().objects.get(id=data.get("user_id"))

        log_permission_request(request, user, permission, page, grant=True)

        ensure_page_specific_permissions_enabled(page.region)
        ensure_user_has_correct_permissions(request, page, user)

        permission_message = permission.grant_permission(user, page)
    except PermissionDenied as e:
        logger.exception(e)
        permission_message = PermissionMessage(
            "An error has occurred. Please contact an administrator.",
            MessageLevel.ERROR,
        )
    if permission_message:
        logger.debug(permission_message.message)
    return __render_response(request, page, permission_message)


@require_POST
@permission_required("cms.change_page")
@permission_required("cms.grant_page_permissions")
# pylint: disable=unused-argument
def revoke_page_permission_ajax(request: HttpRequest, region_slug: str) -> HttpResponse:
    """
    Remove a page permission for a given user and page

    :param request: The current request
    :param region_slug: The slug of the current region

    :raises ~django.core.exceptions.PermissionDenied: If page permissions are disabled for this region or the user does
                                                      not have the permission to revoke page permissions

    :return: The rendered page permission table
    """

    try:
        data = json.loads(request.body.decode("utf-8"))
        permission = get_permission(data)
        page = Page.objects.get(id=data.get("page_id"))
        user = get_user_model().objects.get(id=data.get("user_id"))

        log_permission_request(request, user, permission, page, grant=False)

        ensure_page_specific_permissions_enabled(page.region)
        ensure_user_has_correct_permissions(request, page, user)

        permission_message = permission.revoke_permission(user, page)
    except PermissionDenied as e:
        logger.exception(e)
        permission_message = PermissionMessage(
            _("An error has occurred. Please contact an administrator."),
            MessageLevel.ERROR,
        )
    logger.debug(permission_message)

    return __render_response(request, page, permission_message)


class MessageLevel(StrEnum):
    """
    Enum for different level tags of a message
    """

    WARNING = "warning"
    INFO = "info"
    SUCCESS = "success"
    ERROR = "error"


@dataclass
class PermissionMessage:
    """
    Saves message and level_tag of the return message
    """

    def __init__(
        self,
        message: str,
        level_tag: MessageLevel,
    ):
        self.message = message
        self.level_tag = level_tag


class AbstractPagePermission(ABC):
    """
    An abstract class to handle page permissions
    """

    def __init__(self, permission: str):
        self.permission = permission

    @property
    @abstractmethod
    def grant_success_message(self) -> str:
        """
        Success message for granting a permission

        :return: success message
        """

    @property
    @abstractmethod
    def revoke_success_message(self) -> str:
        """
        Success message for revoking a permission

        :return: success message
        """

    @property
    @abstractmethod
    def revoke_with_no_effect_success_message(self) -> str:
        """
        Success message for granting a permission without effect

        :return: success message
        """

    def grant_permission(self, user: User, page: Page) -> PermissionMessage:
        """
        Grant the permission

        :param user: User the permission is changed for
        :param page: Page the permission is changed for

        :return: Response message
        """
        if not user.has_perm("cms.view_page"):
            return PermissionMessage(
                _(
                    "Page-specific permissions cannot be granted to users "
                    "who do not have permission to view pages (e.g event managers)."
                ),
                MessageLevel.WARNING,
            )

        message = self.build_grant_message(user, page)
        self.save_grant_permission(user, page)
        return message

    def revoke_permission(self, user: User, page: Page) -> PermissionMessage:
        """
        Revoke the permission

        :param user: User the permission is changed for
        :param page: Page the permission is changed for

        :return: Response message
        """
        self.save_revoke_permission(user, page)
        return self.build_revoke_message(user, page)

    def build_grant_message(self, user: User, page: Page) -> PermissionMessage:
        """
        Build the success message when granting a permission

        :param user: user that is granted a permission
        :param page: page the user is granted the permission for

        :return: message with level_tag
        """
        if user.has_perm(self.permission, page):
            message = _(
                'Information: The user "{}" has this permission already.'
            ).format(user.full_user_name)
            level_tag = MessageLevel.INFO
        else:
            message = self.grant_success_message.format(user.full_user_name)
            level_tag = MessageLevel.SUCCESS
        return PermissionMessage(message, level_tag)

    def build_revoke_message(self, user: User, page: Page) -> PermissionMessage:
        """
        Build the response message after revoking

        :param user: User the permission is changed for
        :param page: Page the permission is changed for

        :return: permission message
        """
        if user.has_perm(self.permission, page):
            message = self.revoke_with_no_effect_success_message.format(
                user.full_user_name
            )
            level_tag = MessageLevel.INFO
        else:
            message = self.revoke_success_message.format(user.full_user_name)
            level_tag = MessageLevel.SUCCESS
        return PermissionMessage(message, level_tag)

    @abstractmethod
    def save_grant_permission(self, user: User, page: Page) -> None:
        """
        Grant the permission and save changes

        :param user: User the permission should be changed for
        :param page: Page the permission should be changed for
        """

    @abstractmethod
    def save_revoke_permission(self, user: User, page: Page) -> None:
        """
        Revoke the permission and save changes

        :param user: User the permission should be changed for
        :param page: Page the permission should be changed for
        """


class EditPagePermission(AbstractPagePermission):
    """
    Implementation of AbstractPagePermission for editing page permissions
    """

    def __init__(self) -> None:
        super().__init__("cms.change_page_object")

    grant_success_message = _('Success: The user "{}" can now edit this page.')
    revoke_success_message = _('Success: The user "{}" cannot edit this page anymore.')
    revoke_with_no_effect_success_message = _(
        'Information: The user "{}" has been removed from the authors of '
        "this page, but has the implicit permission to edit this page anyway."
    )

    def save_grant_permission(self, user: User, page: Page) -> None:
        page.authors.add(user)
        page.save()

    def save_revoke_permission(self, user: User, page: Page) -> None:
        if user in page.authors.all():
            page.authors.remove(user)
            page.save()


class PublishPagePermission(AbstractPagePermission):
    """
    Implementation of AbstractPagePermission for publishing page permissions
    """

    def __init__(self) -> None:
        super().__init__("cms.publish_page_object")

    grant_success_message = _('Success: The user "{}" can now publish this page.')
    revoke_success_message = _(
        'Success: The user "{}" cannot publish this page anymore.'
    )
    revoke_with_no_effect_success_message = _(
        'Information: The user "{}" has been removed from the editors of '
        "this page, but has the implicit permission to publish this page anyway."
    )

    def save_grant_permission(self, user: User, page: Page) -> None:
        page.editors.add(user)
        page.save()

    def save_revoke_permission(self, user: User, page: Page) -> None:
        if user in page.editors.all():
            page.editors.remove(user)
            page.save()


def __render_response(
    request: HttpRequest, page: Page, permission_message: PermissionMessage
) -> HttpResponse:
    return render(
        request,
        "pages/_page_permission_table.html",
        {
            "page": page,
            "page_form": PageForm(
                instance=page,
                additional_instance_attributes={
                    "region": page.region,
                },
            ),
            "permission_message": permission_message,
        },
    )


def log_permission_request(
    request: HttpRequest,
    user: User,
    permission: AbstractPagePermission,
    page: Page,
    grant: bool,
) -> None:
    """
    Logging for page permission request

    :param request: Request
    :param user: user that should be granted or revoked a permission
    :param permission: permission that should be granted or revoked
    :param page: page for which the permission should be granted or revoked
    :param grant: if the permission is granted or revoked
    """
    logger.debug(
        "[AJAX] %r wants to %r %r the permission to %s %r",
        request.user,
        "grant" if grant else "revoke",
        user,
        permission,
        page,
    )


def ensure_page_specific_permissions_enabled(region: Region) -> None:
    """
    Ensure the page permission is enabled

    :param region: The region the page permission should be enabled for

    :raises ~django.core.exceptions.PermissionDenied: If page permissions are disabled for this region
    """
    if not region.page_permissions_enabled:
        raise PermissionDenied(f"Page permissions are not activated for {region!r}")


def ensure_user_has_correct_permissions(
    request: HttpRequest, page: Page, user: User
) -> None:
    """
    Ensure the user has correct permissions

    :param request: request
    :param page: page the permission should be changed for
    :param user: user the permission should be changed for

    :raises ~django.core.exceptions.PermissionDenied: If the user does not have the permission to grant page permissions
    """
    if not (request.user.is_superuser or request.user.is_staff):
        if page.region not in request.user.regions.all():
            logger.warning(
                "Error: %r cannot grant permissions for %r",
                request.user,
                page.region,
            )
            raise PermissionDenied
        if page.region not in user.regions.all():
            logger.warning(
                "Error: %r cannot receive permissions for %r",
                user,
                page.region,
            )
            raise PermissionDenied


def get_permission(data: Any) -> AbstractPagePermission:
    """
    Gets the Permission object for the requested permission

    :param data: Request data

    :raises ~django.core.exceptions.PermissionDenied: If unknown page permissions should be changed

    :return: Permission object
    """
    permission = data.get("permission")
    if permission == "edit":
        return EditPagePermission()
    if permission == "publish":
        return PublishPagePermission()
    logger.warning("Error: The permission %r is not supported", permission)
    raise PermissionDenied