From 21ff6fe7d05821b71373ce5eaca77602262a9cd1 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Thu, 20 Mar 2025 16:50:56 +0100 Subject: [PATCH] Implement organization rules --- src/servala/core/models/mixins.py | 3 ++- src/servala/core/models/organization.py | 10 ++++++- src/servala/core/models/user.py | 36 +++++-------------------- src/servala/core/rules.py | 23 ++++++++++++++++ src/servala/settings.py | 2 +- 5 files changed, 41 insertions(+), 33 deletions(-) create mode 100644 src/servala/core/rules.py diff --git a/src/servala/core/models/mixins.py b/src/servala/core/models/mixins.py index ac8c3b0..5a2ed94 100644 --- a/src/servala/core/models/mixins.py +++ b/src/servala/core/models/mixins.py @@ -1,8 +1,9 @@ from django.db import models from django.utils.translation import gettext_lazy as _ +from rules.contrib.models import RulesModelBase, RulesModelMixin -class ServalaModelMixin(models.Model): +class ServalaModelMixin(RulesModelMixin, models.Model, metaclass=RulesModelBase): created_at = models.DateTimeField( auto_now_add=True, editable=False, verbose_name=_("Created") ) diff --git a/src/servala/core/models/organization.py b/src/servala/core/models/organization.py index bcba9a2..d59057b 100644 --- a/src/servala/core/models/organization.py +++ b/src/servala/core/models/organization.py @@ -1,3 +1,4 @@ +import rules import urlman from django.conf import settings from django.db import models @@ -6,7 +7,8 @@ from django.utils.text import slugify from django.utils.translation import gettext_lazy as _ from django_scopes import ScopedManager -from .mixins import ServalaModelMixin +from servala.core import rules as perms +from servala.core.models.mixins import ServalaModelMixin class Organization(ServalaModelMixin, models.Model): @@ -65,6 +67,12 @@ class Organization(ServalaModelMixin, models.Model): class Meta: verbose_name = _("Organization") verbose_name_plural = _("Organizations") + rules_permissions = { + "view": rules.is_staff | perms.is_organization_member, + "change": rules.is_staff | perms.is_organization_admin, + "delete": rules.is_staff | perms.is_organization_owner, + "add": rules.is_authenticated, + } def __str__(self): return self.name diff --git a/src/servala/core/models/user.py b/src/servala/core/models/user.py index 12545a8..8fa3b88 100644 --- a/src/servala/core/models/user.py +++ b/src/servala/core/models/user.py @@ -1,4 +1,8 @@ -from django.contrib.auth.models import AbstractBaseUser, BaseUserManager +from django.contrib.auth.models import ( + AbstractBaseUser, + BaseUserManager, + PermissionsMixin, +) from django.db import models from django.utils.translation import gettext_lazy as _ @@ -32,7 +36,7 @@ class UserManager(BaseUserManager): return self.create_user(email, password, **extra_fields) -class User(ServalaModelMixin, AbstractBaseUser): +class User(ServalaModelMixin, PermissionsMixin, AbstractBaseUser): """The Django model provides a password and last_login field.""" objects = UserManager() @@ -71,31 +75,3 @@ class User(ServalaModelMixin, AbstractBaseUser): def normalize_username(self, username): return super().normalize_username(username).strip().lower() - - def has_perm(self, perm, obj=None): - """ - Return True if the user has the specified permission. - Superusers automatically have all permissions. - """ - return self.is_superuser - - def has_module_perms(self, app_label): - """ - Return True if the user has any permissions in the given app label. - Superusers automatically have all permissions. - """ - return self.is_superuser - - def get_all_permissions(self, obj=None): - """ - Return a set of permission strings that the user has. - Superusers have all permissions. - """ - if self.is_superuser: - from django.contrib.auth.models import Permission - - return { - f"{perm.content_type.app_label}.{perm.codename}" - for perm in Permission.objects.all() - } - return set() diff --git a/src/servala/core/rules.py b/src/servala/core/rules.py new file mode 100644 index 0000000..2d51145 --- /dev/null +++ b/src/servala/core/rules.py @@ -0,0 +1,23 @@ +import rules + + +def has_organization_role(user, org, roles): + memberships = org.memberships.all().filter(user=user) + if roles: + memberships = memberships.filter(role__in=roles) + return memberships.exists() + + +@rules.predicate +def is_organization_owner(user, org): + return has_organization_role(user, org, ["owner"]) + + +@rules.predicate +def is_organization_admin(user, org): + return has_organization_role(user, org, ["owner", "admin"]) + + +@rules.predicate +def is_organization_member(user, org): + return has_organization_role(user, org, None) diff --git a/src/servala/settings.py b/src/servala/settings.py index c1682a7..ef9932d 100644 --- a/src/servala/settings.py +++ b/src/servala/settings.py @@ -97,7 +97,7 @@ INSTALLED_APPS = [ "django.contrib.staticfiles", "django.forms", "template_partials", - "rules", + "rules.apps.AutodiscoverRulesConfig", # The frontend app is loaded early in order to supersede some allauth views/behaviour "servala.frontend", "allauth",