ambient-innovation / ambient-toolbox

Helpers and tools for Python and web-development with Django
https://pypi.org/project/ambient-toolbox/
MIT License
8 stars 4 forks source link

feat: add static_role_permissions #32

Closed robinzachmann closed 2 months ago

robinzachmann commented 2 months ago

Static Role Permissions

This library provides a highly simplified permission system based on a static (hardcoded) definition of roles and permissions.

Installation

  1. Add StaticRolePermissionBackend to your AUTHENTICATION_BACKENDS in the settings.py:
AUTHENTICATION_BACKENDS = [
    # ... other backends
    # 'django.contrib.auth.backends.ModelBackend',  <-- replace the default ModelBackend
    "ambient_toolbox.static_role_permissions.auth_backend.StaticRolePermissionBackend",
]
  1. Set STATIC_ROLE_PERMISSIONS_PATH as dotted path, pointing to a dictionary, which defines your permissions per role.

Example:

settings.py

STATIC_ROLE_PERMISSIONS_PATH = "my_project.conf.permisions.PERMISSIONS_DICT"

my_project.conf.permisions.py

PERMISSIONS_DICT: dict[str, set[str]] = {
    "role_1": {"my_app.permission_1", "my_app.permission_2", ...},
    "role_2": {...},
}

The roles (keys) and permissions (values) are simple strings. By default, a system check will match the values against the Django model permission (e.g. polls.view_poll) (see System check for more information).

In theory, roles and permissions could be any value. Just make sure that the return value of User.role matches the keys of the dictionary.

Simple example

PERMISSIONS_DICT: dict[str, set[str]] = {
    "admin": {
        "auth.view_user",
        "auth.edit_user",
    },
    "customer": {
        "auth.view_user",
    },
}

class User(AbstractUser):
    @property
    def role(self) -> str:
        return "admin" if self.is_staff else "customer"

User(is_staff=True).has_perm("auth.edit_user")  # --> True (without any db query)

Example with role field

This library does not require a role field on the user model, but here is an example how you could implement it:

class UserRoleChoices(models.TextChoices):
    ADMIN = "ADMIN", "Admin"
    EMPLOYEE = "EMPLOYEE", "Employee"
    USER = "USER", "User"

class User(AbstractUser):
    role = models.CharField(
        max_length=100,
        choices=UserRoleChoices.choices,
        default=UserRoleChoices.USER,
    )

PERMISSIONS_DICT: dict[str, set[str]] = {
    UserRoleChoices.ADMIN: {...},
    UserRoleChoices.EMPLOYEE: {...},
    UserRoleChoices.USER: {...},
}

System check

By default, this library will register a system check if STATIC_ROLE_PERMISSIONS_PATH is set. The check will go through all the defined permissions and compare them with the Django model permissions. If any permission does not exist in the Django permission system, a warning will be raised.

This is useful to ensure compatibility with the Django ecosystem and to avoid typos in the permissions.

The system check can be disabled by setting STATIC_ROLE_PERMISSIONS_DISABLE_SYSTEM_CHECK to True in the settings.

Why use this library?

Django comes by default with a quite sophisticated permission system. It is very flexible and covers a lot of use cases for user-based permissions.

Django permission system in a nutshell:

However, in many cases this adds of lot of complexity to the application, which is not needed.

This library provides a much simpler way to manage permissions, which is based roles and a hardcoded set of permissions per role.

Limitations

This library comes with a simple approach for roles / permissions. If your reality is more complex than that, you should not use it.

E.g when: