Source code for swh.web.auth.utils

# Copyright (C) 2020-2022  The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information

from __future__ import annotations

from base64 import urlsafe_b64encode
from typing import TYPE_CHECKING, List

from cryptography.fernet import Fernet
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC

from django.contrib.auth.decorators import user_passes_test
from django.http.request import HttpRequest

if TYPE_CHECKING:
    from django.contrib.auth.models import Permission

SWH_AMBASSADOR_PERMISSION = "swh.ambassador"
API_SAVE_ORIGIN_PERMISSION = "swh.web.api.save_origin"
ADMIN_LIST_DEPOSIT_PERMISSION = "swh.web.admin.list_deposits"
MAILMAP_PERMISSION = "swh.web.mailmap"
ADD_FORGE_MODERATOR_PERMISSION = "swh.web.add_forge_now.moderator"
MAILMAP_ADMIN_PERMISSION = "swh.web.admin.mailmap"
API_SAVE_BULK_PERMISSION = "swh.web.api.save_bulk"
API_PROVENANCE_PERMISSION = "swh.web.api.provenance"

WEBAPP_PERMISSIONS = [
    SWH_AMBASSADOR_PERMISSION,
    API_SAVE_ORIGIN_PERMISSION,
    ADMIN_LIST_DEPOSIT_PERMISSION,
    MAILMAP_PERMISSION,
    ADD_FORGE_MODERATOR_PERMISSION,
    MAILMAP_ADMIN_PERMISSION,
    API_PROVENANCE_PERMISSION,
]


def _get_fernet(password: bytes, salt: bytes) -> Fernet:
    """
    Instantiate a Fernet system from a password and a salt value
    (see https://cryptography.io/en/latest/fernet/).

    Args:
        password: user password that will be used to generate a Fernet key
            derivation function
        salt: value that will be used to generate a Fernet key
            derivation function

    Returns:
        The Fernet system
    """
    kdf = PBKDF2HMAC(
        algorithm=hashes.SHA256(),
        length=32,
        salt=salt,
        iterations=100000,
        backend=default_backend(),
    )
    key = urlsafe_b64encode(kdf.derive(password))
    return Fernet(key)


[docs] def encrypt_data(data: bytes, password: bytes, salt: bytes) -> bytes: """ Encrypt data using Fernet system (symmetric encryption). Args: data: input data to encrypt password: user password that will be used to generate a Fernet key derivation function salt: value that will be used to generate a Fernet key derivation function Returns: The encrypted data """ return _get_fernet(password, salt).encrypt(data)
[docs] def decrypt_data(data: bytes, password: bytes, salt: bytes) -> bytes: """ Decrypt data using Fernet system (symmetric encryption). Args: data: input data to decrypt password: user password that will be used to generate a Fernet key derivation function salt: value that will be used to generate a Fernet key derivation function Returns: The decrypted data """ return _get_fernet(password, salt).decrypt(data)
[docs] def privileged_user(request: HttpRequest, permissions: List[str] = []) -> bool: """Determine whether a user is authenticated and is a privileged one (e.g ambassador). This allows such user to have access to some more actions (e.g. bypass save code now review, access to 'archives' type...). A user is considered as privileged if he is a staff member or has any permission from those provided as parameters. Args: request: Input django HTTP request permissions: list of permission names to determine if user is privileged or not Returns: Whether the user is privileged or not. """ user = request.user return user.is_authenticated and ( user.is_staff or any([user.has_perm(perm) for perm in permissions]) )
[docs] def any_permission_required(*perms): """View decorator granting access to it if user has at least one permission among those passed as parameters. """ def check_perms(user): if any(user.has_perm(perm) for perm in perms): return True from swh.web.utils.exc import ForbiddenExc raise ForbiddenExc return user_passes_test(check_perms)
[docs] def is_add_forge_now_moderator(user) -> bool: """Is a user considered an add-forge-now moderator? Returns True if a user is staff or has add forge now moderator permission """ return user.is_staff or user.has_perm(ADD_FORGE_MODERATOR_PERMISSION)
[docs] def get_or_create_django_permission(perm_name: str) -> Permission: """Get or create a swh-web permission (different from django model permission) out of a permission name string. Args: perm_name: Permission name (e.g. swh.web.api.throttling_exempted, swh.ambassador, ...) Returns: The persisted permission """ from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType app_label, codename = perm_name.rsplit(".", maxsplit=1) content_type = ContentType.objects.get_or_create( app_label=app_label, model=codename, )[0] return Permission.objects.get_or_create( codename=codename, name=perm_name, content_type=content_type, )[0]