mirror of
https://github.com/goauthentik/authentik.git
synced 2026-08-30 18:51:39 -07:00
rbac: clean up roles and permissions (#19588)
* clean up roles and permissions This was purposefully not included in `2025.12` to split the changes up. The main content of this patch is in the migrations. Everything else follows more or less automatically. * add breaking change warning to release notes * add `ak_groups` --> `groups` deprecated proxy * fixup! add `ak_groups` --> `groups` deprecated proxy * fixup! add `ak_groups` --> `groups` deprecated proxy * fixup! add `ak_groups` --> `groups` deprecated proxy * add configuration warning to default notifications blueprint * add rudimentary tests for User.ak_groups * remove no longer used permissions * clarify deprecation Co-authored-by: Jens L. <jens@goauthentik.io> Signed-off-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com> * remove integration changes These will be included in a separate PR once this is released. --------- Signed-off-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com> Co-authored-by: Jens L. <jens@goauthentik.io>
This commit is contained in:
@@ -18,7 +18,7 @@ entries:
|
||||
name: foo
|
||||
title: foo
|
||||
permissions:
|
||||
- permission: view_flow
|
||||
- permission: authentik_flows.view_flow
|
||||
user: !KeyOf user
|
||||
- permission: view_flow
|
||||
- permission: authentik_flows.view_flow
|
||||
role: !KeyOf role
|
||||
|
||||
@@ -15,7 +15,7 @@ from django.db.models import Model
|
||||
from django.db.models.query_utils import Q
|
||||
from django.db.transaction import atomic
|
||||
from django.db.utils import IntegrityError
|
||||
from guardian.models import RoleObjectPermission, UserObjectPermission
|
||||
from guardian.models import RoleObjectPermission
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.serializers import BaseSerializer, Serializer
|
||||
from structlog.stdlib import BoundLogger, get_logger
|
||||
@@ -71,7 +71,6 @@ def excluded_models() -> list[type[Model]]:
|
||||
ContentType,
|
||||
Permission,
|
||||
RoleObjectPermission,
|
||||
UserObjectPermission,
|
||||
# Base classes
|
||||
Provider,
|
||||
Source,
|
||||
|
||||
@@ -66,7 +66,7 @@ class ApplicationSerializer(ModelSerializer):
|
||||
user = self.context["request"].user
|
||||
|
||||
# Cache serialized user data to avoid N+1 when formatting launch URLs
|
||||
# for multiple applications. UserSerializer accesses user.ak_groups which
|
||||
# for multiple applications. UserSerializer accesses user.groups which
|
||||
# would otherwise trigger a query for each application.
|
||||
if user is not None:
|
||||
if "_cached_user_data" not in self.context:
|
||||
|
||||
@@ -129,7 +129,6 @@ class UserSerializer(ModelSerializer):
|
||||
groups = PrimaryKeyRelatedField(
|
||||
allow_empty=True,
|
||||
many=True,
|
||||
source="ak_groups",
|
||||
queryset=Group.objects.all().order_by("name"),
|
||||
default=list,
|
||||
)
|
||||
@@ -165,7 +164,7 @@ class UserSerializer(ModelSerializer):
|
||||
def get_groups_obj(self, instance: User) -> list[PartialGroupSerializer] | None:
|
||||
if not self._should_include_groups:
|
||||
return None
|
||||
return PartialGroupSerializer(instance.ak_groups, many=True).data
|
||||
return PartialGroupSerializer(instance.groups, many=True).data
|
||||
|
||||
@extend_schema_field(RoleSerializer(many=True))
|
||||
def get_roles_obj(self, instance: User) -> list[RoleSerializer] | None:
|
||||
@@ -421,7 +420,7 @@ class UsersFilter(FilterSet):
|
||||
last_login__gt = IsoDateTimeFilter(field_name="last_login", lookup_expr="gt")
|
||||
last_login__isnull = BooleanFilter(field_name="last_login", lookup_expr="isnull")
|
||||
|
||||
is_superuser = BooleanFilter(field_name="ak_groups", method="filter_is_superuser")
|
||||
is_superuser = BooleanFilter(field_name="groups", method="filter_is_superuser")
|
||||
uuid = UUIDFilter(field_name="uuid")
|
||||
|
||||
path = CharFilter(field_name="path")
|
||||
@@ -430,12 +429,12 @@ class UsersFilter(FilterSet):
|
||||
type = MultipleChoiceFilter(choices=UserTypes.choices, field_name="type")
|
||||
|
||||
groups_by_name = ModelMultipleChoiceFilter(
|
||||
field_name="ak_groups__name",
|
||||
field_name="groups__name",
|
||||
to_field_name="name",
|
||||
queryset=Group.objects.all().order_by("name"),
|
||||
)
|
||||
groups_by_pk = ModelMultipleChoiceFilter(
|
||||
field_name="ak_groups",
|
||||
field_name="groups",
|
||||
queryset=Group.objects.all().order_by("name"),
|
||||
)
|
||||
|
||||
@@ -451,8 +450,8 @@ class UsersFilter(FilterSet):
|
||||
|
||||
def filter_is_superuser(self, queryset, name, value):
|
||||
if value:
|
||||
return queryset.filter(ak_groups__is_superuser=True).distinct()
|
||||
return queryset.exclude(ak_groups__is_superuser=True).distinct()
|
||||
return queryset.filter(groups__is_superuser=True).distinct()
|
||||
return queryset.exclude(groups__is_superuser=True).distinct()
|
||||
|
||||
def filter_attributes(self, queryset, name, value):
|
||||
"""Filter attributes by query args"""
|
||||
@@ -530,7 +529,7 @@ class UserViewSet(
|
||||
def get_queryset(self):
|
||||
base_qs = User.objects.all().exclude_anonymous()
|
||||
if self.serializer_class(context={"request": self.request})._should_include_groups:
|
||||
base_qs = base_qs.prefetch_related("ak_groups")
|
||||
base_qs = base_qs.prefetch_related("groups")
|
||||
if self.serializer_class(context={"request": self.request})._should_include_roles:
|
||||
base_qs = base_qs.prefetch_related("roles")
|
||||
return base_qs
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# Generated by Django 5.2.10 on 2026-01-19 21:46
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0056_user_roles"),
|
||||
("authentik_rbac", "0010_remove_role_group_alter_role_name"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="user_permissions",
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="group",
|
||||
name="roles",
|
||||
field=models.ManyToManyField(
|
||||
blank=True, related_name="groups", to="authentik_rbac.role"
|
||||
),
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="groups",
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="user",
|
||||
old_name="ak_groups",
|
||||
new_name="groups",
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="user",
|
||||
options={
|
||||
"permissions": [
|
||||
("reset_user_password", "Reset Password"),
|
||||
("impersonate", "Can impersonate other users"),
|
||||
("preview_user", "Can preview user data sent to providers"),
|
||||
("view_user_applications", "View applications the user has access to"),
|
||||
],
|
||||
"verbose_name": "User",
|
||||
"verbose_name_plural": "Users",
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,6 +1,6 @@
|
||||
"""authentik core models"""
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from enum import StrEnum
|
||||
from hashlib import sha256
|
||||
from typing import Any, Self
|
||||
@@ -183,7 +183,7 @@ class Group(SerializerModel, AttributesMixin):
|
||||
default=False, help_text=_("Users added to this group will be superusers.")
|
||||
)
|
||||
|
||||
roles = models.ManyToManyField("authentik_rbac.Role", related_name="ak_groups", blank=True)
|
||||
roles = models.ManyToManyField("authentik_rbac.Role", related_name="groups", blank=True)
|
||||
|
||||
parents = models.ManyToManyField(
|
||||
"Group",
|
||||
@@ -232,7 +232,7 @@ class Group(SerializerModel, AttributesMixin):
|
||||
def all_roles(self) -> QuerySet[Role]:
|
||||
"""Get all roles of this group and all of its ancestors."""
|
||||
return Role.objects.filter(
|
||||
ak_groups__in=Group.objects.filter(pk=self.pk).with_ancestors()
|
||||
groups__in=Group.objects.filter(pk=self.pk).with_ancestors()
|
||||
).distinct()
|
||||
|
||||
def get_managed_role(self, create=False):
|
||||
@@ -240,7 +240,7 @@ class Group(SerializerModel, AttributesMixin):
|
||||
name = managed_role_name(self)
|
||||
role, created = Role.objects.get_or_create(name=name, managed=name)
|
||||
if created:
|
||||
role.ak_groups.add(self)
|
||||
role.groups.add(self)
|
||||
return role
|
||||
else:
|
||||
return Role.objects.filter(name=managed_role_name(self)).first()
|
||||
@@ -355,13 +355,17 @@ class UserManager(DjangoUserManager):
|
||||
class User(SerializerModel, AttributesMixin, AbstractUser):
|
||||
"""authentik User model, based on django's contrib auth user model."""
|
||||
|
||||
# Overwriting PermissionsMixin: permissions are handled by roles.
|
||||
# (This knowingly violates the Liskov substitution principle. It is better to fail loudly.)
|
||||
user_permissions = None
|
||||
|
||||
uuid = models.UUIDField(default=uuid4, editable=False, unique=True)
|
||||
name = models.TextField(help_text=_("User's display name."))
|
||||
path = models.TextField(default="users")
|
||||
type = models.TextField(choices=UserTypes.choices, default=UserTypes.INTERNAL)
|
||||
|
||||
sources = models.ManyToManyField("Source", through="UserSourceConnection")
|
||||
ak_groups = models.ManyToManyField("Group", related_name="users")
|
||||
groups = models.ManyToManyField("Group", related_name="users")
|
||||
roles = models.ManyToManyField("authentik_rbac.Role", related_name="users", blank=True)
|
||||
password_change_date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
@@ -375,8 +379,6 @@ class User(SerializerModel, AttributesMixin, AbstractUser):
|
||||
permissions = [
|
||||
("reset_user_password", _("Reset Password")),
|
||||
("impersonate", _("Can impersonate other users")),
|
||||
("assign_user_permissions", _("Can assign permissions to users")),
|
||||
("unassign_user_permissions", _("Can unassign permissions from users")),
|
||||
("preview_user", _("Can preview user data sent to providers")),
|
||||
("view_user_applications", _("View applications the user has access to")),
|
||||
]
|
||||
@@ -400,11 +402,11 @@ class User(SerializerModel, AttributesMixin, AbstractUser):
|
||||
|
||||
def all_groups(self) -> QuerySet[Group]:
|
||||
"""Recursively get all groups this user is a member of."""
|
||||
return self.ak_groups.all().with_ancestors()
|
||||
return self.groups.all().with_ancestors()
|
||||
|
||||
def all_roles(self) -> QuerySet[Role]:
|
||||
"""Get all roles of this user and all of its groups (recursively)."""
|
||||
return Role.objects.filter(Q(users=self) | Q(ak_groups__in=self.all_groups())).distinct()
|
||||
return Role.objects.filter(Q(users=self) | Q(groups__in=self.all_groups())).distinct()
|
||||
|
||||
def get_managed_role(self, create=False):
|
||||
if create:
|
||||
@@ -508,6 +510,42 @@ class User(SerializerModel, AttributesMixin, AbstractUser):
|
||||
"""superuser == staff user"""
|
||||
return self.is_superuser # type: ignore
|
||||
|
||||
# TODO: remove this after 2026.
|
||||
@property
|
||||
def ak_groups(self):
|
||||
"""This is a proxy for a renamed, deprecated field."""
|
||||
from authentik.events.models import Event, EventAction
|
||||
|
||||
deprecation = "authentik.core.models.User.ak_groups"
|
||||
replacement = "authentik.core.models.User.groups"
|
||||
message_logger = (
|
||||
f"{deprecation} is deprecated and will be removed in a future version of "
|
||||
f"authentik. Please use {replacement} instead."
|
||||
)
|
||||
message_event = (
|
||||
f"{message_logger} This event will not be repeated until it expires (by "
|
||||
"default: in 30 days). See authentik logs for every will invocation of this "
|
||||
"deprecation."
|
||||
)
|
||||
LOGGER.warning(
|
||||
"deprecation used",
|
||||
message=message_logger,
|
||||
deprecation=deprecation,
|
||||
replacement=replacement,
|
||||
)
|
||||
if not Event.filter_not_expired(
|
||||
action=EventAction.CONFIGURATION_WARNING, context__deprecation=deprecation
|
||||
).exists():
|
||||
event = Event.new(
|
||||
EventAction.CONFIGURATION_WARNING,
|
||||
deprecation=deprecation,
|
||||
replacement=replacement,
|
||||
message=message_event,
|
||||
)
|
||||
event.expires = datetime.now() + timedelta(days=30)
|
||||
event.save()
|
||||
return self.groups
|
||||
|
||||
def set_password(self, raw_password, signal=True, sender=None, request=None):
|
||||
if self.pk and signal:
|
||||
from authentik.core.signals import password_changed
|
||||
|
||||
@@ -392,10 +392,10 @@ class GroupUpdateStage(StageView):
|
||||
groups.append(group)
|
||||
|
||||
with transaction.atomic():
|
||||
self.user.ak_groups.remove(
|
||||
*self.user.ak_groups.filter(groupsourceconnection__source=self.source)
|
||||
self.user.groups.remove(
|
||||
*self.user.groups.filter(groupsourceconnection__source=self.source)
|
||||
)
|
||||
self.user.ak_groups.add(*groups)
|
||||
self.user.groups.add(*groups)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ class TestApplicationEntitlements(APITestCase):
|
||||
def test_group(self):
|
||||
"""Test direct group"""
|
||||
group = Group.objects.create(name=generate_id())
|
||||
self.user.ak_groups.add(group)
|
||||
self.user.groups.add(group)
|
||||
ent = ApplicationEntitlement.objects.create(app=self.app, name=generate_id())
|
||||
PolicyBinding.objects.create(target=ent, group=group, order=0)
|
||||
ents = self.user.app_entitlements(self.app)
|
||||
@@ -50,7 +50,7 @@ class TestApplicationEntitlements(APITestCase):
|
||||
parent = Group.objects.create(name=generate_id())
|
||||
group = Group.objects.create(name=generate_id())
|
||||
group.parents.add(parent)
|
||||
self.user.ak_groups.add(group)
|
||||
self.user.groups.add(group)
|
||||
ent = ApplicationEntitlement.objects.create(app=self.app, name=generate_id())
|
||||
PolicyBinding.objects.create(target=ent, group=parent, order=0)
|
||||
ents = self.user.app_entitlements(self.app)
|
||||
|
||||
@@ -122,8 +122,8 @@ class TestGroupsAPI(APITestCase):
|
||||
def test_superuser_update_no_perm(self):
|
||||
"""Test updating a superuser group without permission"""
|
||||
group = Group.objects.create(name=generate_id(), is_superuser=True)
|
||||
self.login_user.assign_perms_to_managed_role("view_group", group)
|
||||
self.login_user.assign_perms_to_managed_role("change_group", group)
|
||||
self.login_user.assign_perms_to_managed_role("authentik_core.view_group", group)
|
||||
self.login_user.assign_perms_to_managed_role("authentik_core.change_group", group)
|
||||
self.client.force_login(self.login_user)
|
||||
res = self.client.patch(
|
||||
reverse("authentik_api:group-detail", kwargs={"pk": group.pk}),
|
||||
@@ -139,8 +139,8 @@ class TestGroupsAPI(APITestCase):
|
||||
"""Test updating a superuser group without permission
|
||||
and without changing the superuser status"""
|
||||
group = Group.objects.create(name=generate_id(), is_superuser=True)
|
||||
self.login_user.assign_perms_to_managed_role("view_group", group)
|
||||
self.login_user.assign_perms_to_managed_role("change_group", group)
|
||||
self.login_user.assign_perms_to_managed_role("authentik_core.view_group", group)
|
||||
self.login_user.assign_perms_to_managed_role("authentik_core.change_group", group)
|
||||
self.client.force_login(self.login_user)
|
||||
res = self.client.patch(
|
||||
reverse("authentik_api:group-detail", kwargs={"pk": group.pk}),
|
||||
|
||||
@@ -54,7 +54,7 @@ class TestSourceFlowManager(FlowTestCase):
|
||||
)
|
||||
self.assertTrue(stage.handle_groups())
|
||||
self.assertTrue(Group.objects.filter(name="group 1").exists())
|
||||
self.assertTrue(self.user.ak_groups.filter(name="group 1").exists())
|
||||
self.assertTrue(self.user.groups.filter(name="group 1").exists())
|
||||
self.assertTrue(
|
||||
GroupOAuthSourceConnection.objects.filter(
|
||||
group=Group.objects.get(name="group 1"), source=self.source
|
||||
@@ -88,7 +88,7 @@ class TestSourceFlowManager(FlowTestCase):
|
||||
)
|
||||
self.assertTrue(stage.handle_groups())
|
||||
self.assertTrue(Group.objects.filter(name="group 1").exists())
|
||||
self.assertTrue(self.user.ak_groups.filter(name="group 1").exists())
|
||||
self.assertTrue(self.user.groups.filter(name="group 1").exists())
|
||||
self.assertTrue(
|
||||
GroupOAuthSourceConnection.objects.filter(
|
||||
group=Group.objects.get(name="group 1"), source=self.source
|
||||
@@ -123,7 +123,7 @@ class TestSourceFlowManager(FlowTestCase):
|
||||
)
|
||||
self.assertTrue(stage.handle_groups())
|
||||
self.assertTrue(Group.objects.filter(name="group 1").exists())
|
||||
self.assertTrue(self.user.ak_groups.filter(name="group 1").exists())
|
||||
self.assertTrue(self.user.groups.filter(name="group 1").exists())
|
||||
self.assertTrue(
|
||||
GroupOAuthSourceConnection.objects.filter(group=group, source=self.source).exists()
|
||||
)
|
||||
@@ -155,7 +155,7 @@ class TestSourceFlowManager(FlowTestCase):
|
||||
)
|
||||
self.assertTrue(stage.handle_groups())
|
||||
self.assertTrue(Group.objects.filter(name="group 1").exists())
|
||||
self.assertTrue(self.user.ak_groups.filter(name="group 1").exists())
|
||||
self.assertTrue(self.user.groups.filter(name="group 1").exists())
|
||||
self.assertTrue(
|
||||
GroupOAuthSourceConnection.objects.filter(
|
||||
group=Group.objects.get(name="group 1"), source=self.source
|
||||
@@ -189,7 +189,7 @@ class TestSourceFlowManager(FlowTestCase):
|
||||
request=request,
|
||||
)
|
||||
self.assertFalse(stage.handle_groups())
|
||||
self.assertFalse(self.user.ak_groups.filter(name="group 1").exists())
|
||||
self.assertFalse(self.user.groups.filter(name="group 1").exists())
|
||||
self.assertFalse(
|
||||
GroupOAuthSourceConnection.objects.filter(group=group, source=self.source).exists()
|
||||
)
|
||||
@@ -201,7 +201,7 @@ class TestSourceFlowManager(FlowTestCase):
|
||||
other_group = Group.objects.create(name="other group")
|
||||
old_group = Group.objects.create(name="old group")
|
||||
new_group = Group.objects.create(name="new group")
|
||||
self.user.ak_groups.set([other_group, old_group])
|
||||
self.user.groups.set([other_group, old_group])
|
||||
GroupOAuthSourceConnection.objects.create(
|
||||
group=old_group, source=self.source, identifier=old_group.name
|
||||
)
|
||||
@@ -231,7 +231,7 @@ class TestSourceFlowManager(FlowTestCase):
|
||||
request=request,
|
||||
)
|
||||
self.assertTrue(stage.handle_groups())
|
||||
self.assertFalse(self.user.ak_groups.filter(name="old group").exists())
|
||||
self.assertTrue(self.user.ak_groups.filter(name="other group").exists())
|
||||
self.assertTrue(self.user.ak_groups.filter(name="new group").exists())
|
||||
self.assertEqual(self.user.ak_groups.count(), 2)
|
||||
self.assertFalse(self.user.groups.filter(name="old group").exists())
|
||||
self.assertTrue(self.user.groups.filter(name="other group").exists())
|
||||
self.assertTrue(self.user.groups.filter(name="new group").exists())
|
||||
self.assertEqual(self.user.groups.count(), 2)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from django.test.testcases import TestCase
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.events.models import Event
|
||||
from authentik.lib.generators import generate_id
|
||||
|
||||
|
||||
@@ -18,3 +19,17 @@ class TestUsers(TestCase):
|
||||
self.assertTrue(user.has_perm(perm))
|
||||
user.remove_perms_from_managed_role(perm)
|
||||
self.assertFalse(user.has_perm(perm))
|
||||
|
||||
def test_user_ak_groups(self):
|
||||
"""Test user.ak_groups is a proxy for user.groups"""
|
||||
user = User.objects.create(username=generate_id())
|
||||
self.assertEqual(user.ak_groups, user.groups)
|
||||
|
||||
def test_user_ak_groups_event(self):
|
||||
"""Test user.ak_groups creates exactly one event"""
|
||||
user = User.objects.create(username=generate_id())
|
||||
self.assertEqual(Event.objects.count(), 0)
|
||||
user.ak_groups.all()
|
||||
self.assertEqual(Event.objects.count(), 1)
|
||||
user.ak_groups.all()
|
||||
self.assertEqual(Event.objects.count(), 1)
|
||||
|
||||
@@ -196,8 +196,10 @@ class TestCrypto(APITestCase):
|
||||
"""Test certificate export (download)"""
|
||||
keypair = create_test_cert()
|
||||
user = create_test_user()
|
||||
user.assign_perms_to_managed_role("view_certificatekeypair", keypair)
|
||||
user.assign_perms_to_managed_role("view_certificatekeypair_certificate", keypair)
|
||||
user.assign_perms_to_managed_role("authentik_crypto.view_certificatekeypair", keypair)
|
||||
user.assign_perms_to_managed_role(
|
||||
"authentik_crypto.view_certificatekeypair_certificate", keypair
|
||||
)
|
||||
self.client.force_login(user)
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
@@ -220,8 +222,8 @@ class TestCrypto(APITestCase):
|
||||
"""Test private_key export (download)"""
|
||||
keypair = create_test_cert()
|
||||
user = create_test_user()
|
||||
user.assign_perms_to_managed_role("view_certificatekeypair", keypair)
|
||||
user.assign_perms_to_managed_role("view_certificatekeypair_key", keypair)
|
||||
user.assign_perms_to_managed_role("authentik_crypto.view_certificatekeypair", keypair)
|
||||
user.assign_perms_to_managed_role("authentik_crypto.view_certificatekeypair_key", keypair)
|
||||
self.client.force_login(user)
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
|
||||
@@ -145,7 +145,7 @@ class GoogleWorkspaceProvider(OutgoingSyncProvider, BackchannelProvider):
|
||||
type=UserTypes.INTERNAL_SERVICE_ACCOUNT
|
||||
)
|
||||
if self.filter_group:
|
||||
base = base.filter(ak_groups__in=[self.filter_group])
|
||||
base = base.filter(groups__in=[self.filter_group])
|
||||
return base.order_by("pk")
|
||||
if type == Group:
|
||||
# Get queryset of all groups with consistent ordering
|
||||
|
||||
@@ -134,7 +134,7 @@ class MicrosoftEntraProvider(OutgoingSyncProvider, BackchannelProvider):
|
||||
type=UserTypes.INTERNAL_SERVICE_ACCOUNT
|
||||
)
|
||||
if self.filter_group:
|
||||
base = base.filter(ak_groups__in=[self.filter_group])
|
||||
base = base.filter(groups__in=[self.filter_group])
|
||||
return base.order_by("pk")
|
||||
if type == Group:
|
||||
# Get queryset of all groups with consistent ordering
|
||||
|
||||
@@ -43,7 +43,7 @@ def ssf_providers_post_save(sender: type[Model], instance: SSFProvider, created:
|
||||
"path": USER_PATH_PROVIDERS_SSF,
|
||||
},
|
||||
)
|
||||
user.assign_perms_to_managed_role("add_stream", instance)
|
||||
user.assign_perms_to_managed_role("authentik_providers_ssf.add_stream", instance)
|
||||
token, token_created = Token.objects.update_or_create(
|
||||
identifier=identifier,
|
||||
defaults={
|
||||
|
||||
@@ -11,7 +11,7 @@ class ExportUserSerializer(UserSerializer):
|
||||
groups = SerializerMethodField(source="get_groups")
|
||||
|
||||
def get_groups(self, instance: User) -> str:
|
||||
return ",".join([group.name for group in instance.ak_groups.all()])
|
||||
return ",".join([group.name for group in instance.groups.all()])
|
||||
|
||||
class Meta(UserSerializer.Meta):
|
||||
fields = [f for f in UserSerializer.Meta.fields if f != "groups_obj"] + ["groups"]
|
||||
|
||||
@@ -91,7 +91,9 @@ class MTLSStageTests(FlowTestCase):
|
||||
def test_parse_outpost_object(self):
|
||||
"""Test outposts's format"""
|
||||
outpost = Outpost.objects.create(name=generate_id(), type=OutpostType.PROXY)
|
||||
outpost.user.assign_perms_to_managed_role("pass_outpost_certificate", self.stage)
|
||||
outpost.user.assign_perms_to_managed_role(
|
||||
"authentik_stages_mtls.pass_outpost_certificate", self.stage
|
||||
)
|
||||
with patch(
|
||||
"authentik.root.middleware.ClientIPMiddleware.get_outpost_user",
|
||||
MagicMock(return_value=outpost.user),
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# Generated by Django 5.2.10 on 2026-01-20 18:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_events", "0014_notification_hyperlink_notification_hyperlink_label_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="event",
|
||||
name="action",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("login", "Login"),
|
||||
("login_failed", "Login Failed"),
|
||||
("logout", "Logout"),
|
||||
("user_write", "User Write"),
|
||||
("suspicious_request", "Suspicious Request"),
|
||||
("password_set", "Password Set"),
|
||||
("secret_view", "Secret View"),
|
||||
("secret_rotate", "Secret Rotate"),
|
||||
("invitation_used", "Invite Used"),
|
||||
("authorize_application", "Authorize Application"),
|
||||
("source_linked", "Source Linked"),
|
||||
("impersonation_started", "Impersonation Started"),
|
||||
("impersonation_ended", "Impersonation Ended"),
|
||||
("flow_execution", "Flow Execution"),
|
||||
("policy_execution", "Policy Execution"),
|
||||
("policy_exception", "Policy Exception"),
|
||||
("property_mapping_exception", "Property Mapping Exception"),
|
||||
("system_task_execution", "System Task Execution"),
|
||||
("system_task_exception", "System Task Exception"),
|
||||
("system_exception", "System Exception"),
|
||||
("configuration_error", "Configuration Error"),
|
||||
("configuration_warning", "Configuration Warning"),
|
||||
("model_created", "Model Created"),
|
||||
("model_updated", "Model Updated"),
|
||||
("model_deleted", "Model Deleted"),
|
||||
("email_sent", "Email Sent"),
|
||||
("update_available", "Update Available"),
|
||||
("export_ready", "Export Ready"),
|
||||
("custom_", "Custom Prefix"),
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -112,6 +112,7 @@ class EventAction(models.TextChoices):
|
||||
SYSTEM_EXCEPTION = "system_exception"
|
||||
|
||||
CONFIGURATION_ERROR = "configuration_error"
|
||||
CONFIGURATION_WARNING = "configuration_warning"
|
||||
|
||||
MODEL_CREATED = "model_created"
|
||||
MODEL_UPDATED = "model_updated"
|
||||
|
||||
@@ -620,7 +620,7 @@ class TestFlowExecutor(FlowTestCase):
|
||||
user_other = create_test_user()
|
||||
|
||||
group_a = Group.objects.create(name=generate_id())
|
||||
user_group_membership.ak_groups.add(group_a)
|
||||
user_group_membership.groups.add(group_a)
|
||||
|
||||
# Stage 0 is an identification stage
|
||||
ident_stage = IdentificationStage.objects.create(
|
||||
|
||||
@@ -87,4 +87,4 @@ def register_signals(
|
||||
return
|
||||
task_sync_m2m_dispatch.send(instance.pk, action, list(pk_set), reverse)
|
||||
|
||||
m2m_changed.connect(model_m2m_changed, User.ak_groups.through, dispatch_uid=uid, weak=False)
|
||||
m2m_changed.connect(model_m2m_changed, User.groups.through, dispatch_uid=uid, weak=False)
|
||||
|
||||
@@ -439,9 +439,13 @@ class Outpost(ScheduledModel, SerializerModel, ManagedModel):
|
||||
if self.managed:
|
||||
for brand in Brand.objects.filter(web_certificate__isnull=False):
|
||||
objects.append(brand)
|
||||
objects.append(("view_certificatekeypair", brand.web_certificate))
|
||||
objects.append(("view_certificatekeypair_certificate", brand.web_certificate))
|
||||
objects.append(("view_certificatekeypair_key", brand.web_certificate))
|
||||
objects.append(("authentik_crypto.view_certificatekeypair", brand.web_certificate))
|
||||
objects.append(
|
||||
("authentik_crypto.view_certificatekeypair_certificate", brand.web_certificate)
|
||||
)
|
||||
objects.append(
|
||||
("authentik_crypto.view_certificatekeypair_key", brand.web_certificate)
|
||||
)
|
||||
return objects
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# Generated by Django 5.2.10 on 2026-01-20 18:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_policies_event_matcher", "0024_alter_eventmatcherpolicy_action"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="eventmatcherpolicy",
|
||||
name="action",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("login", "Login"),
|
||||
("login_failed", "Login Failed"),
|
||||
("logout", "Logout"),
|
||||
("user_write", "User Write"),
|
||||
("suspicious_request", "Suspicious Request"),
|
||||
("password_set", "Password Set"),
|
||||
("secret_view", "Secret View"),
|
||||
("secret_rotate", "Secret Rotate"),
|
||||
("invitation_used", "Invite Used"),
|
||||
("authorize_application", "Authorize Application"),
|
||||
("source_linked", "Source Linked"),
|
||||
("impersonation_started", "Impersonation Started"),
|
||||
("impersonation_ended", "Impersonation Ended"),
|
||||
("flow_execution", "Flow Execution"),
|
||||
("policy_execution", "Policy Execution"),
|
||||
("policy_exception", "Policy Exception"),
|
||||
("property_mapping_exception", "Property Mapping Exception"),
|
||||
("system_task_execution", "System Task Execution"),
|
||||
("system_task_exception", "System Task Exception"),
|
||||
("system_exception", "System Exception"),
|
||||
("configuration_error", "Configuration Error"),
|
||||
("configuration_warning", "Configuration Warning"),
|
||||
("model_created", "Model Created"),
|
||||
("model_updated", "Model Updated"),
|
||||
("model_deleted", "Model Deleted"),
|
||||
("email_sent", "Email Sent"),
|
||||
("update_available", "Update Available"),
|
||||
("export_ready", "Export Ready"),
|
||||
("custom_", "Custom Prefix"),
|
||||
],
|
||||
default=None,
|
||||
help_text="Match created events with this action type. When left empty, all action types will be matched.",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -14,7 +14,7 @@ class TestBindingsAPI(APITestCase):
|
||||
super().setUp()
|
||||
self.pbm = PolicyBindingModel.objects.create()
|
||||
self.user = create_test_admin_user()
|
||||
self.group = self.user.ak_groups.first()
|
||||
self.group = self.user.groups.first()
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_valid_binding(self):
|
||||
|
||||
@@ -96,9 +96,11 @@ class LDAPProvider(OutpostModel, BackchannelProvider):
|
||||
def get_required_objects(self) -> Iterable[models.Model | str | tuple[str, models.Model]]:
|
||||
required = [self, "authentik_core.view_user", "authentik_core.view_group"]
|
||||
if self.certificate is not None:
|
||||
required.append(("view_certificatekeypair", self.certificate))
|
||||
required.append(("view_certificatekeypair_certificate", self.certificate))
|
||||
required.append(("view_certificatekeypair_key", self.certificate))
|
||||
required.append(("authentik_crypto.view_certificatekeypair", self.certificate))
|
||||
required.append(
|
||||
("authentik_crypto.view_certificatekeypair_certificate", self.certificate)
|
||||
)
|
||||
required.append(("authentik_crypto.view_certificatekeypair_key", self.certificate))
|
||||
return required
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -66,7 +66,7 @@ class TestUserinfo(OAuthTestCase):
|
||||
"given_name": self.user.name,
|
||||
"preferred_username": self.user.name,
|
||||
"nickname": self.user.name,
|
||||
"groups": [group.name for group in self.user.ak_groups.all()],
|
||||
"groups": [group.name for group in self.user.groups.all()],
|
||||
"sub": "bar",
|
||||
},
|
||||
)
|
||||
@@ -88,7 +88,7 @@ class TestUserinfo(OAuthTestCase):
|
||||
"given_name": self.user.name,
|
||||
"preferred_username": self.user.name,
|
||||
"nickname": self.user.name,
|
||||
"groups": [group.name for group in self.user.ak_groups.all()],
|
||||
"groups": [group.name for group in self.user.groups.all()],
|
||||
"sub": "bar",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -78,7 +78,7 @@ class GitHubUserTeamsView(View):
|
||||
user = token.user
|
||||
|
||||
orgs_response = []
|
||||
for org in user.ak_groups.all():
|
||||
for org in user.groups.all():
|
||||
_org = {
|
||||
"id": org.num_pk,
|
||||
"node_id": "",
|
||||
|
||||
@@ -182,9 +182,11 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
|
||||
def get_required_objects(self) -> Iterable[models.Model | str | tuple[str, models.Model]]:
|
||||
required = [self]
|
||||
if self.certificate is not None:
|
||||
required.append(("view_certificatekeypair", self.certificate))
|
||||
required.append(("view_certificatekeypair_certificate", self.certificate))
|
||||
required.append(("view_certificatekeypair_key", self.certificate))
|
||||
required.append(("authentik_crypto.view_certificatekeypair", self.certificate))
|
||||
required.append(
|
||||
("authentik_crypto.view_certificatekeypair_certificate", self.certificate)
|
||||
)
|
||||
required.append(("authentik_crypto.view_certificatekeypair_key", self.certificate))
|
||||
return required
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -67,9 +67,11 @@ class RadiusProvider(OutpostModel, Provider):
|
||||
def get_required_objects(self) -> Iterable[models.Model | str | tuple[str, models.Model]]:
|
||||
required = [self, "authentik_stages_mtls.pass_outpost_certificate"]
|
||||
if self.certificate is not None:
|
||||
required.append(("view_certificatekeypair", self.certificate))
|
||||
required.append(("view_certificatekeypair_certificate", self.certificate))
|
||||
required.append(("view_certificatekeypair_key", self.certificate))
|
||||
required.append(("authentik_crypto.view_certificatekeypair", self.certificate))
|
||||
required.append(
|
||||
("authentik_crypto.view_certificatekeypair_certificate", self.certificate)
|
||||
)
|
||||
required.append(("authentik_crypto.view_certificatekeypair_key", self.certificate))
|
||||
return required
|
||||
|
||||
def __str__(self):
|
||||
|
||||
@@ -19,7 +19,6 @@ from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.viewsets import ReadOnlyModelViewSet
|
||||
|
||||
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
|
||||
from authentik.core.models import User
|
||||
from authentik.lib.validators import RequiredTogetherValidator
|
||||
from authentik.policies.event_matcher.models import model_choices
|
||||
from authentik.rbac.models import Role
|
||||
@@ -72,7 +71,6 @@ class PermissionFilter(FilterSet):
|
||||
"""Filter permissions"""
|
||||
|
||||
role = ModelChoiceFilter(queryset=Role.objects.all(), method="filter_role")
|
||||
user = ModelChoiceFilter(queryset=User.objects.all())
|
||||
|
||||
def filter_role(self, queryset: QuerySet, name, value: Role) -> QuerySet:
|
||||
"""Filter permissions based on role"""
|
||||
@@ -85,7 +83,6 @@ class PermissionFilter(FilterSet):
|
||||
"content_type__model",
|
||||
"content_type__app_label",
|
||||
"role",
|
||||
"user",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ class RoleFilterSet(FilterSet):
|
||||
|
||||
inherited = BooleanFilter(
|
||||
method="filter_inherited",
|
||||
label="Include inherited roles (requires users or ak_groups filter)",
|
||||
label="Include inherited roles (requires users or groups filter)",
|
||||
)
|
||||
|
||||
users = extend_schema_field(OpenApiTypes.INT)(
|
||||
@@ -83,15 +83,15 @@ class RoleFilterSet(FilterSet):
|
||||
)
|
||||
)
|
||||
|
||||
ak_groups = extend_schema_field(OpenApiTypes.UUID)(
|
||||
groups = extend_schema_field(OpenApiTypes.UUID)(
|
||||
CharFilter(
|
||||
method="filter_ak_groups",
|
||||
method="filter_groups",
|
||||
label="Filter by group (use with inherited=true for all roles)",
|
||||
)
|
||||
)
|
||||
|
||||
def filter_inherited(self, queryset, name, value):
|
||||
"""This filter is handled by filter_users and filter_ak_groups"""
|
||||
"""This filter is handled by filter_users and filter_groups"""
|
||||
return queryset
|
||||
|
||||
def filter_users(self, queryset, name, value):
|
||||
@@ -105,7 +105,7 @@ class RoleFilterSet(FilterSet):
|
||||
return user.all_roles()
|
||||
return queryset.filter(users=user)
|
||||
|
||||
def filter_ak_groups(self, queryset, name, value):
|
||||
def filter_groups(self, queryset, name, value):
|
||||
"""Filter roles by group, optionally including inherited roles"""
|
||||
group = Group.objects.filter(pk=value).first()
|
||||
if not group:
|
||||
@@ -114,7 +114,7 @@ class RoleFilterSet(FilterSet):
|
||||
include_inherited = self.data.get("inherited", "").lower() == "true"
|
||||
if include_inherited:
|
||||
return group.all_roles()
|
||||
return queryset.filter(ak_groups=group)
|
||||
return queryset.filter(groups=group)
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 5.2.10 on 2026-01-19 21:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_rbac", "0009_remove_initialpermissions_mode"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="role",
|
||||
name="group",
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="role",
|
||||
name="name",
|
||||
field=models.TextField(unique=True),
|
||||
),
|
||||
]
|
||||
@@ -37,19 +37,7 @@ class Role(SerializerModel, ManagedModel):
|
||||
to it."""
|
||||
|
||||
uuid = models.UUIDField(default=uuid4, editable=False, unique=True, primary_key=True)
|
||||
# Due to the way django and django-guardian work, this is somewhat of a hack.
|
||||
# Django and django-guardian allow for setting permissions on users and groups, but they
|
||||
# only allow for a custom user object, not a custom group object, which is why
|
||||
# we have both authentik and django groups. With this model, we use the inbuilt group system
|
||||
# for RBAC. This means that every Role needs a single django group that its assigned to
|
||||
# which will hold all of the actual permissions
|
||||
# The main advantage of that is that all the permission checking just works out of the box,
|
||||
# as these permissions are checked by default by django and most other libraries that build
|
||||
# on top of django
|
||||
group = models.OneToOneField("auth.Group", on_delete=models.CASCADE, null=True)
|
||||
|
||||
# name field has the same constraints as the group model
|
||||
name = models.TextField(max_length=150, unique=True)
|
||||
name = models.TextField(unique=True)
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[BaseSerializer]:
|
||||
|
||||
@@ -32,7 +32,7 @@ class TestRoles(APITestCase):
|
||||
role = Role.objects.create(name=generate_id())
|
||||
role.assign_perms("authentik_core.view_application")
|
||||
group.roles.add(role)
|
||||
self.user.ak_groups.add(group)
|
||||
self.user.groups.add(group)
|
||||
self.assertTrue(self.user.has_perm("authentik_core.view_application"))
|
||||
|
||||
def test_remove_group_delete(self):
|
||||
@@ -91,7 +91,7 @@ class TestRoles(APITestCase):
|
||||
group.roles.add(role)
|
||||
group.users.add(self.user)
|
||||
self.assertTrue(self.user.has_perm("authentik_core.view_application"))
|
||||
self.user.ak_groups.remove(group)
|
||||
self.user.groups.remove(group)
|
||||
user = User.objects.get(username=self.user.username)
|
||||
self.assertFalse(user.has_perm("authentik_core.view_application"))
|
||||
|
||||
|
||||
@@ -148,6 +148,7 @@ TENANT_CREATION_FAKES_MIGRATIONS = True
|
||||
TENANT_BASE_SCHEMA = "template"
|
||||
PUBLIC_SCHEMA_NAME = CONFIG.get("postgresql.default_schema")
|
||||
|
||||
GUARDIAN_GROUP_MODEL = "authentik_core.Group"
|
||||
GUARDIAN_ROLE_MODEL = "authentik_rbac.Role"
|
||||
|
||||
SPECTACULAR_SETTINGS = {
|
||||
|
||||
@@ -113,10 +113,8 @@ class KerberosSync:
|
||||
groups.append(group)
|
||||
|
||||
with transaction.atomic():
|
||||
user.ak_groups.remove(
|
||||
*user.ak_groups.filter(groupsourceconnection__source=self._source)
|
||||
)
|
||||
user.ak_groups.add(*groups)
|
||||
user.groups.remove(*user.groups.filter(groupsourceconnection__source=self._source))
|
||||
user.groups.add(*groups)
|
||||
|
||||
except PropertyMappingExpressionException as exc:
|
||||
raise StopSync(exc, None, exc.mapping) from exc
|
||||
|
||||
@@ -59,10 +59,10 @@ class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
|
||||
self._task.info("Group syncing is disabled for this Source")
|
||||
return -1
|
||||
group_count = 0
|
||||
for group in page_data:
|
||||
if (attributes := self.get_attributes(group)) is None:
|
||||
for group_data in page_data:
|
||||
if (attributes := self.get_attributes(group_data)) is None:
|
||||
continue
|
||||
group_dn = flatten(flatten(group.get("entryDN", group.get("dn"))))
|
||||
group_dn = flatten(flatten(group_data.get("entryDN", group_data.get("dn"))))
|
||||
if not (uniq := self.get_identifier(attributes)):
|
||||
self._task.info(
|
||||
f"Uniqueness field not found/not set in attributes: '{group_dn}'",
|
||||
@@ -88,20 +88,20 @@ class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
|
||||
if "users" in defaults:
|
||||
del defaults["users"]
|
||||
parent = defaults.pop("parent", None)
|
||||
ak_group, created = Group.update_or_create_attributes(
|
||||
group, created = Group.update_or_create_attributes(
|
||||
{
|
||||
f"attributes__{LDAP_UNIQUENESS}": uniq,
|
||||
},
|
||||
defaults,
|
||||
)
|
||||
if parent:
|
||||
ak_group.parents.add(parent)
|
||||
group.parents.add(parent)
|
||||
self._logger.debug("Created group with attributes", **defaults)
|
||||
if not GroupLDAPSourceConnection.objects.filter(
|
||||
source=self._source, identifier=uniq
|
||||
):
|
||||
GroupLDAPSourceConnection.objects.create(
|
||||
source=self._source, group=ak_group, identifier=uniq
|
||||
source=self._source, group=group, identifier=uniq
|
||||
)
|
||||
except SkipObjectException:
|
||||
continue
|
||||
@@ -119,6 +119,6 @@ class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
|
||||
dn=group_dn,
|
||||
).save()
|
||||
else:
|
||||
self._logger.debug("Synced group", group=ak_group.name, created=created)
|
||||
self._logger.debug("Synced group", group=group.name, created=created)
|
||||
group_count += 1
|
||||
return group_count
|
||||
|
||||
@@ -50,9 +50,9 @@ class MembershipLDAPSynchronizer(BaseLDAPSynchronizer):
|
||||
self._task.info("Group syncing is disabled for this Source")
|
||||
return -1
|
||||
membership_count = 0
|
||||
for group in page_data:
|
||||
for group_data in page_data:
|
||||
if self._source.lookup_groups_from_user:
|
||||
group_dn = group.get("dn", {})
|
||||
group_dn = group_data.get("dn", {})
|
||||
escaped_dn = escape_filter_chars(group_dn)
|
||||
group_filter = f"({self._source.group_membership_field}={escaped_dn})"
|
||||
group_members = self._source.connection().extend.standard.paged_search(
|
||||
@@ -66,12 +66,12 @@ class MembershipLDAPSynchronizer(BaseLDAPSynchronizer):
|
||||
group_member_dn = group_member.get("dn", {})
|
||||
members.append(group_member_dn)
|
||||
else:
|
||||
if (attributes := self.get_attributes(group)) is None:
|
||||
if (attributes := self.get_attributes(group_data)) is None:
|
||||
continue
|
||||
members = attributes.get(self._source.group_membership_field, [])
|
||||
|
||||
ak_group = self.get_group(group)
|
||||
if not ak_group:
|
||||
group = self.get_group(group_data)
|
||||
if not group:
|
||||
continue
|
||||
|
||||
users = User.objects.filter(
|
||||
@@ -79,14 +79,14 @@ class MembershipLDAPSynchronizer(BaseLDAPSynchronizer):
|
||||
| Q(
|
||||
**{
|
||||
f"attributes__{self._source.user_membership_attribute}__isnull": True,
|
||||
"ak_groups__in": [ak_group],
|
||||
"groups__in": [group],
|
||||
}
|
||||
)
|
||||
).distinct()
|
||||
membership_count += 1
|
||||
membership_count += users.count()
|
||||
ak_group.users.set(users)
|
||||
ak_group.save()
|
||||
group.users.set(users)
|
||||
group.save()
|
||||
self._logger.debug("Successfully updated group membership")
|
||||
return membership_count
|
||||
|
||||
|
||||
@@ -186,9 +186,9 @@ class UserWriteStageView(StageView):
|
||||
with transaction.atomic():
|
||||
user.save()
|
||||
if self.executor.current_stage.create_users_group:
|
||||
user.ak_groups.add(self.executor.current_stage.create_users_group)
|
||||
user.groups.add(self.executor.current_stage.create_users_group)
|
||||
if PLAN_CONTEXT_GROUPS in self.executor.plan.context:
|
||||
user.ak_groups.add(*self.executor.plan.context[PLAN_CONTEXT_GROUPS])
|
||||
user.groups.add(*self.executor.plan.context[PLAN_CONTEXT_GROUPS])
|
||||
except (IntegrityError, ValueError, TypeError, InternalError) as exc:
|
||||
self.logger.warning("Failed to save user", exc=exc)
|
||||
return self.executor.stage_invalid(_("Failed to update user. Please try again later."))
|
||||
|
||||
@@ -69,7 +69,7 @@ class TestUserWriteStage(FlowTestCase):
|
||||
self.assertTrue(user_qs.exists())
|
||||
user = user_qs.first()
|
||||
self.assertTrue(user.check_password(password))
|
||||
self.assertEqual(list(user.ak_groups.order_by("name")), [self.other_group, self.group])
|
||||
self.assertEqual(list(user.groups.order_by("name")), [self.other_group, self.group])
|
||||
self.assertEqual(user.attributes, {USER_ATTRIBUTE_SOURCES: [self.source.name]})
|
||||
|
||||
self.assertTrue(
|
||||
|
||||
@@ -48,6 +48,28 @@ entries:
|
||||
policy: !KeyOf default-match-configuration-error
|
||||
target: !KeyOf default-notify-configuration-error
|
||||
|
||||
- model: authentik_policies_event_matcher.eventmatcherpolicy
|
||||
id: default-match-configuration-warning
|
||||
attrs:
|
||||
action: configuration_warning
|
||||
identifiers:
|
||||
name: default-match-configuration-warning
|
||||
- model: authentik_events.notificationrule
|
||||
id: default-notify-configuration-warning
|
||||
identifiers:
|
||||
name: default-notify-configuration-warning
|
||||
attrs:
|
||||
severity: warning
|
||||
group: !KeyOf group
|
||||
transports:
|
||||
- !KeyOf default-email-transport
|
||||
- !KeyOf default-local-transport
|
||||
- model: authentik_policies.policybinding
|
||||
identifiers:
|
||||
order: 0
|
||||
policy: !KeyOf default-match-configuration-warning
|
||||
target: !KeyOf default-notify-configuration-warning
|
||||
|
||||
- model: authentik_policies_event_matcher.eventmatcherpolicy
|
||||
id: default-match-update
|
||||
attrs:
|
||||
|
||||
@@ -5399,7 +5399,6 @@
|
||||
"authentik_core.add_user",
|
||||
"authentik_core.add_user_to_group",
|
||||
"authentik_core.add_usersourceconnection",
|
||||
"authentik_core.assign_user_permissions",
|
||||
"authentik_core.change_application",
|
||||
"authentik_core.change_applicationentitlement",
|
||||
"authentik_core.change_authenticatedsession",
|
||||
@@ -5433,7 +5432,6 @@
|
||||
"authentik_core.remove_user_from_group",
|
||||
"authentik_core.reset_user_password",
|
||||
"authentik_core.set_token_key",
|
||||
"authentik_core.unassign_user_permissions",
|
||||
"authentik_core.view_application",
|
||||
"authentik_core.view_applicationentitlement",
|
||||
"authentik_core.view_authenticatedsession",
|
||||
@@ -6084,13 +6082,11 @@
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"add_user",
|
||||
"assign_user_permissions",
|
||||
"change_user",
|
||||
"delete_user",
|
||||
"impersonate",
|
||||
"preview_user",
|
||||
"reset_user_password",
|
||||
"unassign_user_permissions",
|
||||
"view_user",
|
||||
"view_user_applications"
|
||||
]
|
||||
@@ -7527,6 +7523,7 @@
|
||||
"system_task_exception",
|
||||
"system_exception",
|
||||
"configuration_error",
|
||||
"configuration_warning",
|
||||
"model_created",
|
||||
"model_updated",
|
||||
"model_deleted",
|
||||
@@ -7644,6 +7641,7 @@
|
||||
"system_task_exception",
|
||||
"system_exception",
|
||||
"configuration_error",
|
||||
"configuration_warning",
|
||||
"model_created",
|
||||
"model_updated",
|
||||
"model_deleted",
|
||||
@@ -8390,6 +8388,7 @@
|
||||
"system_task_exception",
|
||||
"system_exception",
|
||||
"configuration_error",
|
||||
"configuration_warning",
|
||||
"model_created",
|
||||
"model_updated",
|
||||
"model_deleted",
|
||||
@@ -10737,7 +10736,6 @@
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"maxLength": 150,
|
||||
"minLength": 1,
|
||||
"title": "Name"
|
||||
},
|
||||
@@ -10768,7 +10766,6 @@
|
||||
"authentik_core.add_user",
|
||||
"authentik_core.add_user_to_group",
|
||||
"authentik_core.add_usersourceconnection",
|
||||
"authentik_core.assign_user_permissions",
|
||||
"authentik_core.change_application",
|
||||
"authentik_core.change_applicationentitlement",
|
||||
"authentik_core.change_authenticatedsession",
|
||||
@@ -10802,7 +10799,6 @@
|
||||
"authentik_core.remove_user_from_group",
|
||||
"authentik_core.reset_user_password",
|
||||
"authentik_core.set_token_key",
|
||||
"authentik_core.unassign_user_permissions",
|
||||
"authentik_core.view_application",
|
||||
"authentik_core.view_applicationentitlement",
|
||||
"authentik_core.view_authenticatedsession",
|
||||
|
||||
@@ -42,7 +42,7 @@ entries:
|
||||
"given_name": request.user.name,
|
||||
"preferred_username": request.user.username,
|
||||
"nickname": request.user.username,
|
||||
"groups": [group.name for group in request.user.ak_groups.all()],
|
||||
"groups": [group.name for group in request.user.groups.all()],
|
||||
}
|
||||
- identifiers:
|
||||
managed: goauthentik.io/providers/oauth2/scope-entitlements
|
||||
|
||||
@@ -51,7 +51,7 @@ entries:
|
||||
name: "authentik default SAML Mapping: Groups"
|
||||
saml_name: "http://schemas.xmlsoap.org/claims/Group"
|
||||
expression: |
|
||||
for group in request.user.ak_groups.all():
|
||||
for group in request.user.groups.all():
|
||||
yield group.name
|
||||
- identifiers:
|
||||
managed: goauthentik.io/providers/saml/ms-windowsaccountname
|
||||
|
||||
@@ -45,7 +45,7 @@ entries:
|
||||
"given_name": request.user.name,
|
||||
"preferred_username": request.user.username,
|
||||
"nickname": request.user.username,
|
||||
"groups": [group.name for group in request.user.ak_groups.all()],
|
||||
"groups": [group.name for group in request.user.groups.all()],
|
||||
"website" : "foo",
|
||||
"zoneinfo" : "foo",
|
||||
"birthdate" : "2000",
|
||||
|
||||
@@ -8,8 +8,8 @@ from django.http import HttpRequest
|
||||
|
||||
from guardian.conf import settings as guardian_settings
|
||||
from guardian.core import ObjectPermissionChecker
|
||||
from guardian.ctypes import get_content_type
|
||||
from guardian.exceptions import WrongAppError
|
||||
from guardian.utils import get_content_type
|
||||
|
||||
|
||||
def check_object_support(obj: Model) -> bool:
|
||||
|
||||
@@ -5,27 +5,13 @@ ANONYMOUS_USER_NAME = getattr(settings, "GUARDIAN_ANONYMOUS_USER_NAME", "Anonymo
|
||||
GET_INIT_ANONYMOUS_USER = getattr(
|
||||
settings, "GUARDIAN_GET_INIT_ANONYMOUS_USER", "guardian.management.get_init_anonymous_user"
|
||||
)
|
||||
GET_CONTENT_TYPE = getattr(
|
||||
settings, "GUARDIAN_GET_CONTENT_TYPE", "guardian.ctypes.get_default_content_type"
|
||||
)
|
||||
# Anonymous user cache TTL configuration
|
||||
# 0 = no cache (default), positive number = cache TTL in seconds, -1 = cache indefinitely
|
||||
ANONYMOUS_USER_CACHE_TTL = getattr(settings, "GUARDIAN_ANONYMOUS_USER_CACHE_TTL", 0)
|
||||
# Default to using guardian supplied generic object permission models
|
||||
USER_OBJ_PERMS_MODEL = getattr(
|
||||
settings, "GUARDIAN_USER_OBJ_PERMS_MODEL", "guardian.UserObjectPermission"
|
||||
)
|
||||
GROUP_OBJ_PERMS_MODEL = getattr(
|
||||
settings, "GUARDIAN_GROUP_OBJ_PERMS_MODEL", "guardian.GroupObjectPermission"
|
||||
)
|
||||
ROLE_OBJ_PERMS_MODEL = getattr(
|
||||
settings, "GUARDIAN_ROLE_OBJ_PERMS_MODEL", "guardian.RoleObjectPermission"
|
||||
)
|
||||
|
||||
# Since get_user_model() causes a circular import if called when app models are
|
||||
# being loaded, the user_model_label should be used when possible, with calls
|
||||
# to get_user_model deferred to execution time
|
||||
user_model_label = getattr(settings, "AUTH_USER_MODEL", "auth.User")
|
||||
group_model_label = getattr(settings, "GUARDIAN_GROUP_MODEL", None)
|
||||
role_model_label = getattr(settings, "GUARDIAN_ROLE_MODEL", None)
|
||||
if group_model_label is None:
|
||||
raise ImproperlyConfigured("ak-guardian requires settings.GUARDIAN_GROUP_MODEL")
|
||||
if role_model_label is None:
|
||||
raise ImproperlyConfigured("ak-guardian requires settings.GUARDIAN_ROLE_MODEL")
|
||||
|
||||
@@ -2,8 +2,7 @@ from django.contrib.auth.models import Permission
|
||||
from django.db.models import Model, Q
|
||||
from django.utils.encoding import force_str
|
||||
|
||||
from guardian.ctypes import get_content_type
|
||||
from guardian.utils import get_identity
|
||||
from guardian.utils import get_content_type, get_identity
|
||||
|
||||
|
||||
def remove_app_label(perm: str) -> str:
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
from typing import Any
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Model
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
from guardian.conf import settings as guardian_settings
|
||||
|
||||
|
||||
def get_content_type(obj: Model | type[Model]) -> Any:
|
||||
get_content_type_function = import_string(guardian_settings.GET_CONTENT_TYPE)
|
||||
return get_content_type_function(obj)
|
||||
|
||||
|
||||
def get_default_content_type(obj: Model | type[Model]) -> ContentType:
|
||||
"""Get content type for a given object using Django's content type framework.
|
||||
|
||||
Parameters:
|
||||
obj (Model | Type): Object for which content type is to be fetched.
|
||||
|
||||
Returns:
|
||||
Content type for the given object.
|
||||
|
||||
See Also:
|
||||
https://docs.djangoproject.com/en/5.1/ref/contrib/contenttypes/
|
||||
|
||||
"""
|
||||
return ContentType.objects.get_for_model(obj)
|
||||
@@ -16,12 +16,6 @@ class InvalidIdentity(GuardianError):
|
||||
pass
|
||||
|
||||
|
||||
class ObjectNotPersisted(GuardianError):
|
||||
"""Raised when the object has not been saved to the database."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class WrongAppError(GuardianError):
|
||||
"""Raised when the app name for a permission is incorrect."""
|
||||
|
||||
@@ -32,9 +26,3 @@ class MixedContentTypeError(GuardianError):
|
||||
"""Raised when content type for the provided permissions and/or class do not match."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class MultipleIdentityAndObjectError(GuardianError):
|
||||
"""Raised when an operation is attempted on both user/group and object."""
|
||||
|
||||
pass
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
from typing import Any
|
||||
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.db import models
|
||||
from django.db.models import Model, Q, QuerySet
|
||||
|
||||
from guardian.ctypes import get_content_type
|
||||
from guardian.exceptions import ObjectNotPersisted
|
||||
|
||||
|
||||
class BaseObjectPermissionManager(models.Manager):
|
||||
def assign_perm(self, perm: str, role: Any, obj: Model) -> Any:
|
||||
"""Assigns permission with given `perm` for an instance `obj` and `role`."""
|
||||
if getattr(obj, "pk", None) is None:
|
||||
raise ObjectNotPersisted(f"Object {obj} needs to be persisted first")
|
||||
ctype = get_content_type(obj)
|
||||
if not isinstance(perm, Permission):
|
||||
permission = Permission.objects.get(content_type=ctype, codename=perm)
|
||||
else:
|
||||
permission = perm
|
||||
|
||||
kwargs = {
|
||||
"permission": permission,
|
||||
"content_type": ctype,
|
||||
"object_pk": obj.pk,
|
||||
"role": role,
|
||||
}
|
||||
|
||||
obj_perm, _ = self.get_or_create(**kwargs)
|
||||
return obj_perm
|
||||
|
||||
def assign_perm_to_many(
|
||||
self, perm: str, roles: Any, obj: Model, ignore_conflicts: bool = False
|
||||
) -> Any:
|
||||
"""
|
||||
Bulk assigns given `perm` for the object `obj` to a set of roles.
|
||||
"""
|
||||
ctype = get_content_type(obj)
|
||||
if not isinstance(perm, Permission):
|
||||
permission = Permission.objects.get(content_type=ctype, codename=perm)
|
||||
else:
|
||||
permission = perm
|
||||
|
||||
kwargs = {
|
||||
"permission": permission,
|
||||
"content_type": ctype,
|
||||
"object_pk": obj.pk,
|
||||
}
|
||||
|
||||
to_add = []
|
||||
for role in roles:
|
||||
kwargs["role"] = role
|
||||
to_add.append(self.model(**kwargs))
|
||||
|
||||
return self.model.objects.bulk_create(to_add, ignore_conflicts=ignore_conflicts)
|
||||
|
||||
def remove_perm(self, perm: str, role: Any, obj: Model) -> tuple[int, dict]:
|
||||
"""
|
||||
Removes permission `perm` for an instance `obj` and given `role`.
|
||||
|
||||
Please note that we do NOT fetch object permission from database -
|
||||
we use `Queryset.delete` method for removing it.
|
||||
The main implication of this is that `post_delete` signals would NOT be fired.
|
||||
"""
|
||||
if getattr(obj, "pk", None) is None:
|
||||
raise ObjectNotPersisted(f"Object {obj} needs to be persisted first")
|
||||
|
||||
filters = Q(**{"role": role})
|
||||
|
||||
if isinstance(perm, Permission):
|
||||
filters &= Q(permission=perm)
|
||||
else:
|
||||
filters &= Q(permission__codename=perm, permission__content_type=get_content_type(obj))
|
||||
|
||||
filters &= Q(object_pk=obj.pk)
|
||||
return self.filter(filters).delete()
|
||||
|
||||
def bulk_remove_perm(self, perm: str, role: Any, queryset: QuerySet) -> tuple[int, dict]:
|
||||
"""
|
||||
Removes permission `perm` for a `queryset` and given `role`.
|
||||
|
||||
Please note that we do NOT fetch object permission from database -
|
||||
we use `Queryset.delete` method for removing it.
|
||||
The main implication of this is that `post_delete` signals would NOT be fired.
|
||||
"""
|
||||
filters = Q(**{"role": role})
|
||||
|
||||
if isinstance(perm, Permission):
|
||||
filters &= Q(permission=perm)
|
||||
else:
|
||||
ctype = get_content_type(queryset.model)
|
||||
filters &= Q(permission__codename=perm, permission__content_type=ctype)
|
||||
|
||||
filters &= Q(object_pk__in=[str(pk) for pk in queryset.values_list("pk", flat=True)])
|
||||
|
||||
return self.filter(filters).delete()
|
||||
|
||||
|
||||
class UserObjectPermissionManager(BaseObjectPermissionManager):
|
||||
pass
|
||||
|
||||
|
||||
class GroupObjectPermissionManager(BaseObjectPermissionManager):
|
||||
pass
|
||||
|
||||
|
||||
class RoleObjectPermissionManager(BaseObjectPermissionManager):
|
||||
pass
|
||||
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.2.10 on 2026-01-19 20:50
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("guardian", "0004_role_permissions"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name="GroupObjectPermission",
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name="UserObjectPermission",
|
||||
),
|
||||
]
|
||||
@@ -1,4 +1,4 @@
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
@@ -6,30 +6,25 @@ from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from guardian.conf import settings as guardian_settings
|
||||
from guardian.ctypes import get_content_type
|
||||
from guardian.managers import (
|
||||
GroupObjectPermissionManager,
|
||||
RoleObjectPermissionManager,
|
||||
UserObjectPermissionManager,
|
||||
)
|
||||
from guardian.utils import get_content_type
|
||||
|
||||
|
||||
class BaseObjectPermission(models.Model):
|
||||
class RoleObjectPermission(models.Model):
|
||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||
permission = models.ForeignKey(Permission, on_delete=models.CASCADE)
|
||||
role = models.ForeignKey(guardian_settings.role_model_label, on_delete=models.CASCADE)
|
||||
object_pk = models.CharField(_("object ID"), max_length=255)
|
||||
content_object = GenericForeignKey(fk_field="object_pk")
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
unique_together = ["role", "permission", "object_pk"]
|
||||
indexes = [
|
||||
models.Index(fields=["permission", "role", "content_type", "object_pk"]),
|
||||
models.Index(fields=["role", "content_type", "object_pk"]),
|
||||
]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "{} | {} | {}".format(
|
||||
str(self.content_object),
|
||||
str(
|
||||
getattr(self, "user", False)
|
||||
or str(getattr(self, "group", False))
|
||||
or str(getattr(self, "role", False))
|
||||
),
|
||||
str(self.permission.codename),
|
||||
)
|
||||
return f"{str(self.content_object)} | {str(self.role)} | {str(self.permission.codename)}"
|
||||
|
||||
def save(self, *args, **kwargs) -> None:
|
||||
content_type = get_content_type(self.content_object)
|
||||
@@ -41,47 +36,6 @@ class BaseObjectPermission(models.Model):
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class BaseGenericObjectPermission(models.Model):
|
||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||
object_pk = models.CharField(_("object ID"), max_length=255)
|
||||
content_object = GenericForeignKey(fk_field="object_pk")
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
indexes = [
|
||||
models.Index(fields=["content_type", "object_pk"]),
|
||||
]
|
||||
|
||||
|
||||
# The Role* classes follow the User* and Group* class structures for now.
|
||||
# TODO: restructure Role* classes.
|
||||
|
||||
|
||||
class RoleObjectPermissionBase(BaseObjectPermission):
|
||||
role = models.ForeignKey(guardian_settings.role_model_label, on_delete=models.CASCADE)
|
||||
|
||||
objects = RoleObjectPermissionManager()
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
unique_together = ["role", "permission", "content_object"]
|
||||
|
||||
|
||||
class RoleObjectPermissionAbstract(RoleObjectPermissionBase, BaseGenericObjectPermission):
|
||||
class Meta(RoleObjectPermissionBase.Meta, BaseGenericObjectPermission.Meta):
|
||||
abstract = True
|
||||
unique_together = ["role", "permission", "object_pk"]
|
||||
|
||||
|
||||
class RoleObjectPermission(RoleObjectPermissionAbstract):
|
||||
class Meta(RoleObjectPermissionAbstract.Meta):
|
||||
abstract = False
|
||||
indexes = [
|
||||
models.Index(fields=["permission", "role", "content_type", "object_pk"]),
|
||||
models.Index(fields=["role", "content_type", "object_pk"]),
|
||||
]
|
||||
|
||||
|
||||
class RoleModelPermission(models.Model):
|
||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||
permission = models.ForeignKey(Permission, on_delete=models.CASCADE)
|
||||
@@ -96,57 +50,3 @@ class RoleModelPermission(models.Model):
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"RoleModelPermission with role {self.role_id} and permission {self.permission_id}"
|
||||
|
||||
|
||||
# The following classes are deprecated and will be removed in a future release.
|
||||
# TODO: remove deprecated classes.
|
||||
|
||||
|
||||
class UserObjectPermissionBase(BaseObjectPermission):
|
||||
user = models.ForeignKey(guardian_settings.user_model_label, on_delete=models.CASCADE)
|
||||
|
||||
objects = UserObjectPermissionManager()
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
unique_together = ["user", "permission", "content_object"]
|
||||
|
||||
|
||||
class UserObjectPermissionAbstract(UserObjectPermissionBase, BaseGenericObjectPermission):
|
||||
class Meta(UserObjectPermissionBase.Meta, BaseGenericObjectPermission.Meta):
|
||||
abstract = True
|
||||
unique_together = ["user", "permission", "object_pk"]
|
||||
|
||||
|
||||
class UserObjectPermission(UserObjectPermissionAbstract):
|
||||
class Meta(UserObjectPermissionAbstract.Meta):
|
||||
abstract = False
|
||||
indexes = [
|
||||
models.Index(fields=["permission", "user", "content_type", "object_pk"]),
|
||||
models.Index(fields=["user", "content_type", "object_pk"]),
|
||||
]
|
||||
|
||||
|
||||
class GroupObjectPermissionBase(BaseObjectPermission):
|
||||
group = models.ForeignKey(Group, on_delete=models.CASCADE)
|
||||
|
||||
objects = GroupObjectPermissionManager()
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
unique_together = ["group", "permission", "content_object"]
|
||||
|
||||
|
||||
class GroupObjectPermissionAbstract(GroupObjectPermissionBase, BaseGenericObjectPermission):
|
||||
class Meta(GroupObjectPermissionBase.Meta, BaseGenericObjectPermission.Meta):
|
||||
abstract = True
|
||||
unique_together = ["group", "permission", "object_pk"]
|
||||
|
||||
|
||||
class GroupObjectPermission(GroupObjectPermissionAbstract):
|
||||
class Meta(GroupObjectPermissionAbstract.Meta):
|
||||
abstract = False
|
||||
indexes = [
|
||||
models.Index(fields=["permission", "group", "content_type", "object_pk"]),
|
||||
models.Index(fields=["group", "content_type", "object_pk"]),
|
||||
]
|
||||
|
||||
@@ -15,7 +15,6 @@ from django.db.models import (
|
||||
from django.db.models.expressions import RawSQL
|
||||
|
||||
from guardian.core import ObjectPermissionChecker
|
||||
from guardian.ctypes import get_content_type
|
||||
from guardian.exceptions import (
|
||||
GuardianError,
|
||||
InvalidIdentity,
|
||||
@@ -23,6 +22,7 @@ from guardian.exceptions import (
|
||||
)
|
||||
from guardian.utils import (
|
||||
get_anonymous_user,
|
||||
get_content_type,
|
||||
get_identity,
|
||||
get_role_model_perms_model,
|
||||
get_role_obj_perms_model,
|
||||
@@ -46,7 +46,7 @@ def assign_perm(
|
||||
perm: str | Permission,
|
||||
role: Any,
|
||||
obj: Model | None = None,
|
||||
) -> str | Permission | None:
|
||||
):
|
||||
"""Assigns permission to role and object pair.
|
||||
|
||||
Parameters:
|
||||
@@ -67,50 +67,37 @@ def assign_perm(
|
||||
if not role:
|
||||
return None
|
||||
|
||||
# If obj is None we try to operate on global permissions
|
||||
if obj is None:
|
||||
if not isinstance(perm, Permission):
|
||||
try:
|
||||
app_label, codename = perm.split(".", 1)
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
"For global permissions, first argument must be in format: "
|
||||
f"'app_label.codename' (is {perm})"
|
||||
) from None
|
||||
permission = Permission.objects.get(
|
||||
content_type__app_label=app_label, codename=codename
|
||||
)
|
||||
else:
|
||||
permission = perm
|
||||
if not isinstance(perm, Permission):
|
||||
try:
|
||||
app_label, codename = perm.split(".", 1)
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
"For global permissions, first argument must be in format: "
|
||||
f"'app_label.codename' (is {perm})"
|
||||
) from None
|
||||
permission = Permission.objects.get(content_type__app_label=app_label, codename=codename)
|
||||
else:
|
||||
permission = perm
|
||||
|
||||
kwargs = {
|
||||
"content_type": permission.content_type,
|
||||
"permission": permission,
|
||||
"role": role,
|
||||
}
|
||||
kwargs = {
|
||||
"content_type": permission.content_type,
|
||||
"permission": permission,
|
||||
"role": role,
|
||||
}
|
||||
if obj is None:
|
||||
model_perm, _ = get_role_model_perms_model().objects.get_or_create(**kwargs)
|
||||
return model_perm
|
||||
|
||||
if not isinstance(perm, Permission):
|
||||
if "." in perm:
|
||||
app_label, perm = perm.split(".", 1)
|
||||
|
||||
if isinstance(obj, QuerySet | list):
|
||||
raise RuntimeError("Currently not supported")
|
||||
|
||||
if isinstance(role, QuerySet | list):
|
||||
model = get_role_obj_perms_model(obj)
|
||||
return model.objects.assign_perm_to_many(perm, role, obj)
|
||||
|
||||
model = get_role_obj_perms_model(obj)
|
||||
return model.objects.assign_perm(perm, role, obj)
|
||||
else:
|
||||
kwargs["object_pk"] = obj.pk
|
||||
obj_perm, _ = get_role_obj_perms_model().objects.get_or_create(**kwargs)
|
||||
return obj_perm
|
||||
|
||||
|
||||
def remove_perm(
|
||||
perm: str | Permission,
|
||||
role: Any,
|
||||
obj: Model | QuerySet | None = None,
|
||||
) -> None:
|
||||
obj: Model | None = None,
|
||||
):
|
||||
"""Removes permission from role and object pair.
|
||||
|
||||
Parameters:
|
||||
@@ -128,40 +115,30 @@ def remove_perm(
|
||||
if not role:
|
||||
return None
|
||||
|
||||
if not isinstance(perm, Permission):
|
||||
try:
|
||||
app_label, codename = perm.split(".", 1)
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
"For global permissions, first argument must be in format: "
|
||||
f"'app_label.codename' (is {perm})"
|
||||
) from None
|
||||
permission = Permission.objects.get(content_type__app_label=app_label, codename=codename)
|
||||
else:
|
||||
permission = perm
|
||||
|
||||
kwargs = {
|
||||
"content_type": permission.content_type,
|
||||
"permission": permission,
|
||||
"role": role,
|
||||
}
|
||||
if obj is None:
|
||||
if not isinstance(perm, Permission):
|
||||
try:
|
||||
app_label, codename = perm.split(".", 1)
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
"For global permissions, first argument must be in format: "
|
||||
f"'app_label.codename' (is {perm})"
|
||||
) from None
|
||||
permission = Permission.objects.get(
|
||||
content_type__app_label=app_label, codename=codename
|
||||
)
|
||||
else:
|
||||
permission = perm
|
||||
|
||||
kwargs = {
|
||||
"content_type": permission.content_type,
|
||||
"permission": permission,
|
||||
"role": role,
|
||||
}
|
||||
|
||||
model_perm = get_role_model_perms_model().objects.filter(**kwargs).delete()
|
||||
return model_perm
|
||||
|
||||
if not isinstance(perm, Permission):
|
||||
if "." in perm:
|
||||
app_label, perm = perm.split(".", 1)
|
||||
perm = perm.split(".")[-1]
|
||||
|
||||
if isinstance(obj, QuerySet):
|
||||
raise RuntimeError("Currently not supported")
|
||||
|
||||
model = get_role_obj_perms_model(obj)
|
||||
return model.objects.remove_perm(perm, role, obj)
|
||||
else:
|
||||
kwargs["object_pk"] = obj.pk
|
||||
obj_perm = get_role_obj_perms_model().objects.filter(**kwargs).delete()
|
||||
return obj_perm
|
||||
|
||||
|
||||
def get_perms(identity: Any, obj: Model | None = None) -> set[str]:
|
||||
@@ -272,7 +249,7 @@ def get_objects_for_user( # noqa: PLR0912 PLR0915
|
||||
return queryset
|
||||
|
||||
# Now we should extract the list of pk values for which we would filter the queryset
|
||||
role_model = get_role_obj_perms_model(queryset.model)
|
||||
role_model = get_role_obj_perms_model()
|
||||
perms_queryset = (
|
||||
role_model.objects.filter(role__in=user.all_roles())
|
||||
.filter(permission__content_type=ctype)
|
||||
|
||||
@@ -11,11 +11,11 @@ import time
|
||||
from math import ceil
|
||||
from typing import Any
|
||||
|
||||
from django.apps import apps as django_apps
|
||||
from django.apps import apps
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db.models import Model, QuerySet
|
||||
|
||||
from guardian.conf import settings as guardian_settings
|
||||
@@ -24,6 +24,32 @@ from guardian.exceptions import InvalidIdentity
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_content_type(obj: Model | type[Model]) -> ContentType:
|
||||
return ContentType.objects.get_for_model(obj)
|
||||
|
||||
|
||||
def get_role_obj_perms_model() -> type[Model]:
|
||||
from guardian.models import RoleObjectPermission
|
||||
|
||||
return RoleObjectPermission
|
||||
|
||||
|
||||
def get_role_model_perms_model() -> type[Model]:
|
||||
from guardian.models import RoleModelPermission
|
||||
|
||||
return RoleModelPermission
|
||||
|
||||
|
||||
def get_group_model() -> type[Model]:
|
||||
app_name, model_name = guardian_settings.group_model_label.split(".", 1)
|
||||
return apps.get_model(app_name, model_name)
|
||||
|
||||
|
||||
def get_role_model() -> type[Model]:
|
||||
app_name, model_name = guardian_settings.role_model_label.split(".", 1)
|
||||
return apps.get_model(app_name, model_name)
|
||||
|
||||
|
||||
def _get_anonymous_user_cached() -> Any:
|
||||
"""Internal cached version of get_anonymous_user using Django's cache system."""
|
||||
cache_key = f"guardian:anonymous_user:{guardian_settings.ANONYMOUS_USER_NAME}"
|
||||
@@ -102,13 +128,14 @@ def get_identity(identity: Model) -> tuple[Any | None, Any | None, Any | None]:
|
||||
if isinstance(identity, AnonymousUser):
|
||||
identity = get_anonymous_user()
|
||||
|
||||
group_model = get_group_obj_perms_model().group.field.related_model
|
||||
role_model = get_role_obj_perms_model().role.field.related_model
|
||||
user_model = get_user_model()
|
||||
group_model = get_group_model()
|
||||
role_model = get_role_model()
|
||||
|
||||
# get identity from queryset model type
|
||||
if isinstance(identity, QuerySet):
|
||||
identity_model_type = identity.model
|
||||
if identity_model_type == get_user_model():
|
||||
if identity_model_type == user_model:
|
||||
return identity, None, None
|
||||
elif identity_model_type == group_model:
|
||||
return None, identity, None
|
||||
@@ -116,14 +143,14 @@ def get_identity(identity: Model) -> tuple[Any | None, Any | None, Any | None]:
|
||||
return None, None, identity
|
||||
|
||||
# get identity from the first element in the list
|
||||
if isinstance(identity, list) and isinstance(identity[0], get_user_model()):
|
||||
if isinstance(identity, list) and isinstance(identity[0], user_model):
|
||||
return identity, None, None
|
||||
if isinstance(identity, list) and isinstance(identity[0], group_model):
|
||||
return None, identity, None
|
||||
if isinstance(identity, list) and isinstance(identity[0], role_model):
|
||||
return None, None, identity
|
||||
|
||||
if isinstance(identity, get_user_model()):
|
||||
if isinstance(identity, user_model):
|
||||
return identity, None, None
|
||||
if isinstance(identity, group_model):
|
||||
return None, identity, None
|
||||
@@ -135,101 +162,6 @@ def get_identity(identity: Model) -> tuple[Any | None, Any | None, Any | None]:
|
||||
)
|
||||
|
||||
|
||||
def get_obj_perm_model_by_conf(setting_name: str) -> type[Model]:
|
||||
"""Return the model that matches the guardian settings.
|
||||
|
||||
Parameters:
|
||||
setting_name (str): The name of the setting to get the model from.
|
||||
|
||||
Returns:
|
||||
The model class that matches the guardian settings.
|
||||
|
||||
Raises:
|
||||
ImproperlyConfigured: If the setting value is not an installed model or
|
||||
does not follow the format 'app_label.model_name'.
|
||||
"""
|
||||
setting_value: str = getattr(guardian_settings, setting_name)
|
||||
try:
|
||||
return django_apps.get_model(setting_value, require_ready=False) # type: ignore
|
||||
except ValueError as e:
|
||||
raise ImproperlyConfigured(
|
||||
f"{setting_value} must be of the form 'app_label.model_name'"
|
||||
) from e
|
||||
except LookupError as e:
|
||||
raise ImproperlyConfigured(
|
||||
f"{setting_name} refers to model '{setting_value}' that has not been installed"
|
||||
) from e
|
||||
|
||||
|
||||
def get_obj_perms_model(
|
||||
obj: Model | None, base_cls: type[Model], generic_cls: type[Model]
|
||||
) -> type[Model]:
|
||||
"""Return the matching object permission model for the obj class.
|
||||
|
||||
Defaults to returning the generic object permission when no direct foreignkey is defined, or
|
||||
obj is None.
|
||||
"""
|
||||
# Default to the generic object permission model
|
||||
# when None obj is provided
|
||||
if obj is None:
|
||||
return generic_cls
|
||||
|
||||
if isinstance(obj, Model):
|
||||
obj = obj.__class__
|
||||
|
||||
return generic_cls
|
||||
|
||||
|
||||
def get_user_obj_perms_model(obj: Model | None = None) -> type[Model]:
|
||||
"""Returns model class that connects given `obj` and User class.
|
||||
|
||||
If obj is not specified, then the user generic object permission model
|
||||
that is returned is determined by the guardian settings for 'USER_OBJ_PERMS_MODEL'.
|
||||
"""
|
||||
from guardian.models import UserObjectPermissionBase
|
||||
|
||||
UserObjectPermission = get_obj_perm_model_by_conf("USER_OBJ_PERMS_MODEL")
|
||||
return get_obj_perms_model(obj, UserObjectPermissionBase, UserObjectPermission)
|
||||
|
||||
|
||||
def get_group_obj_perms_model(obj: Model | None = None) -> type[Model]:
|
||||
"""Returns model class that connects given `obj` and Group class.
|
||||
|
||||
If obj is not specified, then the group generic object permission model
|
||||
that is returned is determined by the guardian settings for 'GROUP_OBJ_PERMS_MODEL'.
|
||||
"""
|
||||
from guardian.models import GroupObjectPermissionBase
|
||||
|
||||
GroupObjectPermission = get_obj_perm_model_by_conf("GROUP_OBJ_PERMS_MODEL")
|
||||
return get_obj_perms_model(obj, GroupObjectPermissionBase, GroupObjectPermission)
|
||||
|
||||
|
||||
def get_role_obj_perms_model(obj: Model | None = None) -> type[Model]:
|
||||
"""Returns model class that connects given `obj` and Role class.
|
||||
|
||||
If obj is not specified, then the role generic object permission model
|
||||
that is returned is determined by the guardian settings for 'ROLE_OBJ_PERMS_MODEL'.
|
||||
"""
|
||||
from guardian.models import RoleObjectPermissionBase
|
||||
|
||||
RoleObjectPermission = get_obj_perm_model_by_conf("ROLE_OBJ_PERMS_MODEL")
|
||||
return get_obj_perms_model(obj, RoleObjectPermissionBase, RoleObjectPermission)
|
||||
|
||||
|
||||
def get_role_model_perms_model() -> type[Model]:
|
||||
"""Returns model class that connects the given Role class."""
|
||||
from guardian.models import RoleModelPermission
|
||||
|
||||
return RoleModelPermission
|
||||
|
||||
|
||||
def evict_obj_perms_cache(obj: Any) -> bool:
|
||||
if hasattr(obj, "_guardian_perms_cache"):
|
||||
delattr(obj, "_guardian_perms_cache")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def clean_orphan_obj_perms( # noqa: PLR0915
|
||||
batch_size: int | None = None,
|
||||
max_batches: int | None = None,
|
||||
|
||||
16
schema.yml
16
schema.yml
@@ -6908,6 +6908,7 @@ paths:
|
||||
enum:
|
||||
- authorize_application
|
||||
- configuration_error
|
||||
- configuration_warning
|
||||
- custom_
|
||||
- email_sent
|
||||
- export_ready
|
||||
@@ -7169,6 +7170,7 @@ paths:
|
||||
enum:
|
||||
- authorize_application
|
||||
- configuration_error
|
||||
- configuration_warning
|
||||
- custom_
|
||||
- email_sent
|
||||
- export_ready
|
||||
@@ -7296,6 +7298,7 @@ paths:
|
||||
enum:
|
||||
- authorize_application
|
||||
- configuration_error
|
||||
- configuration_warning
|
||||
- custom_
|
||||
- email_sent
|
||||
- export_ready
|
||||
@@ -10956,6 +10959,7 @@ paths:
|
||||
enum:
|
||||
- authorize_application
|
||||
- configuration_error
|
||||
- configuration_warning
|
||||
- custom_
|
||||
- email_sent
|
||||
- export_ready
|
||||
@@ -20449,10 +20453,6 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
- $ref: '#/components/parameters/QuerySearch'
|
||||
- in: query
|
||||
name: user
|
||||
schema:
|
||||
type: integer
|
||||
tags:
|
||||
- rbac
|
||||
security:
|
||||
@@ -20747,7 +20747,7 @@ paths:
|
||||
description: Role viewset
|
||||
parameters:
|
||||
- in: query
|
||||
name: ak_groups
|
||||
name: groups
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
@@ -20755,7 +20755,7 @@ paths:
|
||||
name: inherited
|
||||
schema:
|
||||
type: boolean
|
||||
description: Include inherited roles (requires users or ak_groups filter)
|
||||
description: Include inherited roles (requires users or groups filter)
|
||||
- in: query
|
||||
name: managed
|
||||
schema:
|
||||
@@ -38110,6 +38110,7 @@ components:
|
||||
- system_task_exception
|
||||
- system_exception
|
||||
- configuration_error
|
||||
- configuration_warning
|
||||
- model_created
|
||||
- model_updated
|
||||
- model_deleted
|
||||
@@ -49173,7 +49174,6 @@ components:
|
||||
name:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 150
|
||||
PatchedSAMLPropertyMappingRequest:
|
||||
type: object
|
||||
description: SAMLPropertyMapping Serializer
|
||||
@@ -52369,7 +52369,6 @@ components:
|
||||
title: Uuid
|
||||
name:
|
||||
type: string
|
||||
maxLength: 150
|
||||
required:
|
||||
- name
|
||||
- pk
|
||||
@@ -52458,7 +52457,6 @@ components:
|
||||
name:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 150
|
||||
required:
|
||||
- name
|
||||
SAMLBindingsEnum:
|
||||
|
||||
@@ -43,7 +43,9 @@ class TestProviderLDAP(SeleniumTestCase):
|
||||
authorization_flow=Flow.objects.get(slug="default-authentication-flow"),
|
||||
search_mode=APIAccessMode.CACHED,
|
||||
)
|
||||
self.user.assign_perms_to_managed_role("search_full_directory", ldap)
|
||||
self.user.assign_perms_to_managed_role(
|
||||
"authentik_providers_ldap.search_full_directory", ldap
|
||||
)
|
||||
# we need to create an application to actually access the ldap
|
||||
Application.objects.create(name=generate_id(), slug=generate_id(), provider=ldap)
|
||||
outpost: Outpost = Outpost.objects.create(
|
||||
@@ -300,7 +302,7 @@ class TestProviderLDAP(SeleniumTestCase):
|
||||
"gidNumber": 2000 + self.user.pk,
|
||||
"memberOf": [
|
||||
f"cn={group.name},ou=groups,dc=ldap,dc=goauthentik,dc=io"
|
||||
for group in self.user.ak_groups.all()
|
||||
for group in self.user.groups.all()
|
||||
],
|
||||
"homeDirectory": f"/home/{self.user.username}",
|
||||
"ak-active": True,
|
||||
@@ -381,7 +383,7 @@ class TestProviderLDAP(SeleniumTestCase):
|
||||
"gidNumber": 2000 + user.pk,
|
||||
"memberOf": [
|
||||
f"cn={group.name},ou=groups,dc=ldap,dc=goauthentik,dc=io"
|
||||
for group in user.ak_groups.all()
|
||||
for group in user.groups.all()
|
||||
],
|
||||
"homeDirectory": f"/home/{user.username}",
|
||||
"ak-active": True,
|
||||
|
||||
@@ -103,14 +103,14 @@ class TestSourceLDAPSamba(SeleniumTestCase):
|
||||
self.assertIsNotNone(User.objects.get(username="harry"))
|
||||
self.assertIsNotNone(Group.objects.get(name="dev"))
|
||||
self.assertEqual(
|
||||
list(User.objects.get(username="bob").ak_groups.all()), [Group.objects.get(name="dev")]
|
||||
list(User.objects.get(username="bob").groups.all()), [Group.objects.get(name="dev")]
|
||||
)
|
||||
self.assertEqual(list(User.objects.get(username="james").ak_groups.all()), [])
|
||||
self.assertEqual(list(User.objects.get(username="james").groups.all()), [])
|
||||
self.assertEqual(
|
||||
list(User.objects.get(username="john").ak_groups.all().order_by("name")),
|
||||
list(User.objects.get(username="john").groups.all().order_by("name")),
|
||||
[Group.objects.get(name="admins"), Group.objects.get(name="dev")],
|
||||
)
|
||||
self.assertEqual(list(User.objects.get(username="harry").ak_groups.all()), [])
|
||||
self.assertEqual(list(User.objects.get(username="harry").groups.all()), [])
|
||||
|
||||
@retry(exceptions=[LDAPSessionTerminatedByServerError])
|
||||
@apply_blueprint(
|
||||
|
||||
@@ -120,7 +120,7 @@ export class RelatedRoleList extends Table<Role> {
|
||||
if (this.targetGroup) {
|
||||
return this.#api.rbacRolesList({
|
||||
...config,
|
||||
akGroups: this.targetGroup.pk,
|
||||
groups: this.targetGroup.pk,
|
||||
inherited: this.showInherited,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,7 +12,11 @@ To try out the release candidate, replace your Docker image tag with the latest
|
||||
|
||||
## Highlights
|
||||
|
||||
<!-- ## Breaking changes -->
|
||||
## Breaking changes
|
||||
|
||||
### RBAC
|
||||
|
||||
`User.ak_groups` has been deprecated. Users' groups are now accessed through `User.groups`. Usage of `.ak_groups` will continue to function, but will create a configuration warning event. We recommend you check any custom code (e.g. expression policies, property mappings) that deals with group memberships to update them if necessary.
|
||||
|
||||
## New features and improvements
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ return {
|
||||
}
|
||||
```
|
||||
|
||||
You can see that the expression returns a Python dictionary. The dictionary keys must match [User properties](../../user/user_ref.mdx#object-properties) or [Group properties](../../groups/group_ref.md#object-properties). Note that for users, `ak_groups` and `group_attributes` cannot be set.
|
||||
You can see that the expression returns a Python dictionary. The dictionary keys must match [User properties](../../user/user_ref.mdx#object-properties) or [Group properties](../../groups/group_ref.md#object-properties). Note that for users, `groups` and `group_attributes` cannot be set.
|
||||
|
||||
See each source documentation for a reference of the available data. See the authentik [expressions documentation](./expressions.md) for available data and functions.
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ The User object has the following properties:
|
||||
- `path`: User's path, see [Path](#path)
|
||||
- `attributes`: Dynamic attributes, see [Attributes](#attributes)
|
||||
- `group_attributes()`: Merged attributes of all groups the user is a member of and the user's own attributes. Read-only.
|
||||
- `ak_groups`: This is a queryset of all the user's direct groups.
|
||||
- `groups`: This is a queryset of all the user's direct groups.
|
||||
- `all_groups()`: This is a queryset of all the user's direct and indirect groups.
|
||||
|
||||
:::info Clarifying direct vs indirect group membership
|
||||
@@ -33,7 +33,7 @@ These are examples of how User objects can be used within Policies and Property
|
||||
Use the following example to list all groups that a user object is a direct member of:
|
||||
|
||||
```python
|
||||
for group in user.ak_groups.all():
|
||||
for group in user.groups.all():
|
||||
yield group.name
|
||||
```
|
||||
|
||||
@@ -42,7 +42,7 @@ for group in user.ak_groups.all():
|
||||
Use the following example to list groups that a user object is a direct member of, filtered based on group name:
|
||||
|
||||
```python
|
||||
user.ak_groups.filter(name__startswith='test')
|
||||
user.groups.filter(name__startswith='test')
|
||||
```
|
||||
|
||||
### List a user's group memberships including parent groups
|
||||
|
||||
Reference in New Issue
Block a user