# Copyright (c) 2025 Ferrosoft GmbH. All rights reserved.
from dataclasses import dataclass, field
from enum import StrEnum, IntEnum
from typing import List, Any
from django.http import HttpRequest
[docs]
class Role(StrEnum):
"""Roles are identifiers for PermissionSets"""
EMIFLOW_USER = "emiflow_user"
EMIFLOW_MANAGER = "emiflow_manager"
USER_MANAGER = "user_manager"
[docs]
class Verb(IntEnum):
"""
Enumerate model actions.
"""
CREATE = 1
READ = 2
UPDATE = 3
DELETE = 4
READ_FILTERED = 5
READ_TABLE_FILTERED = 6
[docs]
class Permissions:
"""Utility for generating and checking Django permission strings.
Maps ``Verb`` enum values to the corresponding Django CRUD verbs
(``add``, ``view``, ``change``, ``delete``) and assembles fully qualified
``<app_label>.<verb>_<model_name>`` permission strings.
"""
_DJANGO_PERMISSIONS = {
Verb.CREATE: "add",
Verb.READ: "view",
Verb.UPDATE: "change",
Verb.DELETE: "delete",
Verb.READ_FILTERED: "view",
Verb.READ_TABLE_FILTERED: "view",
}
[docs]
@classmethod
def for_model(cls, model_class: Any, verb: Verb) -> str:
"""Build the Django permission string for a model and verb.
Args:
model_class: Model class or bare model name string. When a class
is provided the ``app_label`` is included in the result.
verb: The ``Verb`` action to translate.
Returns:
str: Permission string, e.g. ``"ferrobase.view_businesspartner"``.
"""
django_verb = cls._DJANGO_PERMISSIONS[verb]
if isinstance(model_class, str):
return f"{django_verb}_{model_class.lower()}"
# noinspection PyProtectedMember
m = model_class._meta
return f"{m.app_label}.{django_verb}_{m.model_name}"
[docs]
@classmethod
def allowed(cls, request: HttpRequest, model_class: Any, verb: Verb) -> bool:
"""Check whether the current request user has the given permission.
Args:
request: The current HTTP request.
model_class: Model class or name string.
verb: The action to check.
Returns:
bool: ``True`` if the user holds the corresponding Django permission.
"""
return request.user.has_perm(cls.for_model(model_class, verb))
[docs]
@dataclass
class PermissionSet:
"""Associates a Django group name with a list of model-level permissions.
``group`` holds the display name of the Django ``Group`` this set maps to;
``permissions`` accumulates the individual Django permission strings.
"""
group: str
permissions: List[str] = field(default_factory=list)
[docs]
def with_model(self, model_class: str, *verbs: Verb) -> "PermissionSet":
"""Append permissions for the given model and verbs and return self.
Args:
model_class: Model name string (lower-case) or model class.
*verbs: One or more ``Verb`` values whose Django equivalents are
appended to ``permissions``.
Returns:
PermissionSet: ``self``, enabling a fluent builder pattern.
"""
self.permissions += [Permissions.for_model(model_class, verb) for verb in verbs]
return self
[docs]
class Registry:
"""Static registry mapping each ``Role`` to its ``PermissionSet``.
Provides the single source of truth for which Django permissions belong
to each built-in role. The mapping is consumed when setting up tenant
groups and when assigning roles to users.
"""
_PERMISSION_SETS: dict[Role, PermissionSet] = {
Role.EMIFLOW_USER: (
PermissionSet("Emiflow User")
.with_model(
"businesspartner",
Verb.READ,
)
.with_model(
"companysite",
Verb.READ,
)
.with_model(
"consignment",
Verb.CREATE,
Verb.UPDATE,
Verb.DELETE,
Verb.READ,
)
.with_model(
"emissionbookingrecord",
Verb.CREATE,
Verb.READ,
)
.with_model(
"emissionfactor",
Verb.CREATE,
Verb.UPDATE,
Verb.DELETE,
Verb.READ,
)
.with_model(
"emissionintensity",
Verb.CREATE,
Verb.UPDATE,
Verb.DELETE,
Verb.READ,
)
.with_model(
"emissionscatalog",
Verb.CREATE,
Verb.UPDATE,
Verb.DELETE,
Verb.READ,
)
.with_model(
"emitter",
Verb.READ,
)
.with_model(
"energycarrier",
Verb.READ,
)
.with_model(
"freightitem",
Verb.CREATE,
Verb.UPDATE,
Verb.DELETE,
Verb.READ,
)
.with_model(
"importjob",
Verb.CREATE,
Verb.UPDATE,
Verb.DELETE,
Verb.READ,
)
.with_model(
"importjobtemplate",
Verb.READ,
)
.with_model(
"lifecyclecategory",
Verb.READ,
)
.with_model(
"logentry",
Verb.READ,
)
.with_model(
"material",
Verb.READ,
)
.with_model(
"multilinebookingrecord",
Verb.CREATE,
Verb.UPDATE,
Verb.DELETE,
Verb.READ,
)
.with_model(
"multilineimportline",
Verb.CREATE,
Verb.UPDATE,
Verb.DELETE,
Verb.READ,
)
.with_model(
"report",
Verb.CREATE,
Verb.UPDATE,
Verb.DELETE,
Verb.READ,
)
.with_model(
"shipment",
Verb.CREATE,
Verb.UPDATE,
Verb.DELETE,
Verb.READ,
)
.with_model(
"simpleimportline",
Verb.CREATE,
Verb.UPDATE,
Verb.DELETE,
Verb.READ,
)
.with_model(
"tcegeodestination",
Verb.CREATE,
Verb.UPDATE,
Verb.DELETE,
Verb.READ,
)
.with_model(
"tcegeoorigin",
Verb.CREATE,
Verb.UPDATE,
Verb.DELETE,
Verb.READ,
)
.with_model(
"transportchain",
Verb.CREATE,
Verb.UPDATE,
Verb.READ,
Verb.DELETE,
)
.with_model(
"transportchaindefaults",
Verb.READ,
)
.with_model(
"transportchainelement",
Verb.CREATE,
Verb.UPDATE,
Verb.DELETE,
Verb.READ,
)
.with_model(
"transportlccvalue",
Verb.CREATE,
Verb.READ,
Verb.UPDATE,
Verb.DELETE,
)
.with_model(
"transportoperation",
Verb.READ,
)
.with_model(
"treatment",
Verb.READ,
Verb.CREATE,
Verb.UPDATE,
)
.with_model(
"treatmentoperation",
Verb.READ,
)
.with_model(
"treatmentinputline",
Verb.READ,
Verb.CREATE,
Verb.UPDATE,
)
.with_model(
"treatmentlccvalue",
Verb.READ,
Verb.CREATE,
Verb.UPDATE,
Verb.DELETE,
)
.with_model(
"treatmentoutputline",
Verb.READ,
Verb.CREATE,
Verb.UPDATE,
)
.with_model(
"treatmentoperationinputline",
Verb.READ,
)
.with_model(
"treatmentoperationoutputline",
Verb.READ,
)
),
Role.EMIFLOW_MANAGER: (
PermissionSet("Emiflow Manager")
.with_model(
"businesscentralsettings",
Verb.READ,
Verb.UPDATE,
)
.with_model(
"businesspartner",
Verb.CREATE,
Verb.UPDATE,
Verb.DELETE,
)
.with_model(
"companysite",
Verb.CREATE,
Verb.UPDATE,
Verb.DELETE,
)
.with_model(
"emitter",
Verb.CREATE,
Verb.UPDATE,
Verb.DELETE,
)
.with_model(
"energycarrier",
Verb.CREATE,
Verb.UPDATE,
Verb.DELETE,
)
.with_model(
"idsequenceconfig",
Verb.CREATE,
Verb.READ,
Verb.UPDATE,
)
.with_model(
"lifecyclecategory",
Verb.CREATE,
Verb.UPDATE,
Verb.DELETE,
)
.with_model(
"material",
Verb.CREATE,
Verb.UPDATE,
Verb.DELETE,
)
.with_model(
"settings",
Verb.CREATE,
Verb.UPDATE,
Verb.DELETE,
)
.with_model(
"transportchaindefaults",
Verb.CREATE,
Verb.UPDATE,
Verb.DELETE,
)
.with_model(
"transportoperation",
Verb.CREATE,
Verb.UPDATE,
Verb.DELETE,
)
.with_model(
"treatmentoperation",
Verb.CREATE,
Verb.UPDATE,
Verb.DELETE,
)
.with_model(
"treatmentoperationinputline",
Verb.CREATE,
Verb.UPDATE,
Verb.DELETE,
)
.with_model(
"treatmentoperationoutputline",
Verb.CREATE,
Verb.UPDATE,
Verb.DELETE,
)
),
Role.USER_MANAGER: (
PermissionSet("User Manager")
.with_model(
"user",
Verb.CREATE,
Verb.UPDATE,
Verb.DELETE,
Verb.READ,
)
.with_model(
"apitoken",
Verb.CREATE,
Verb.UPDATE,
Verb.DELETE,
Verb.READ,
)
),
}
[docs]
@classmethod
def get_permission_sets(cls) -> dict[Role, PermissionSet]:
"""Return the full role-to-permission-set mapping.
Returns:
dict[Role, PermissionSet]: Mapping of every defined role to its
associated ``PermissionSet``.
"""
return cls._PERMISSION_SETS