Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

providers/scim: modify filtergroup(s) behavior to allow (multi-)group filtering #13550

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions authentik/providers/scim/api/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class Meta:
"token",
"compatibility_mode",
"exclude_users_service_account",
"filter_group",
"filter_groups",
"dry_run",
]
extra_kwargs = {}
Expand All @@ -41,7 +41,7 @@ class SCIMProviderViewSet(OutgoingSyncProviderStatusMixin, UsedByMixin, ModelVie

queryset = SCIMProvider.objects.all()
serializer_class = SCIMProviderSerializer
filterset_fields = ["name", "exclude_users_service_account", "url", "filter_group"]
filterset_fields = ["name", "exclude_users_service_account", "url"]
search_fields = ["name", "url"]
ordering = ["name", "url"]
sync_single_task = scim_sync
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Generated by Django 5.0.13 on 2025-03-17 08:49

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("authentik_core", "0043_alter_group_options"),
("authentik_providers_scim", "0013_scimprovidergroup_attributes_and_more"),
]

operations = [
migrations.AddField(
model_name="scimprovider",
name="filter_groups",
field=models.ManyToManyField(
blank=True,
default=None,
help_text="Filter groups used to define sync-scope for users and groups.",
related_name="groups",
to="authentik_core.group",
),
),
migrations.AlterField(
model_name="scimprovider",
name="filter_group",
field=models.ForeignKey(
default=None,
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
related_name="group",
to="authentik_core.group",
),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 5.0.13 on 2025-03-17 08:50

from django.db import migrations


def make_many_groups(apps, schema_editor):
"""
Adds the Group object in SCIMProvider.filter_group to the
many-to-many relationship in SCIMProvider.filter_groups
"""
SCIMProvider = apps.get_model("authentik_providers_scim", "scimprovider")

for sCIMProvider in SCIMProvider.objects.all():
if not sCIMProvider.filter_group:
continue
sCIMProvider.filter_groups.add(sCIMProvider.filter_group)


class Migration(migrations.Migration):

dependencies = [
("authentik_providers_scim", "0014_scimprovider_filter_groups_and_more"),
]

operations = [migrations.RunPython(make_many_groups)]
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 5.0.13 on 2025-03-17 14:09

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("authentik_providers_scim", "0015_scimprovider_migrate_filter_groups"),
]

operations = [
migrations.RemoveField(
model_name="scimprovider",
name="filter_group",
),
migrations.AlterField(
model_name="scimprovider",
name="filter_groups",
field=models.ManyToManyField(
blank=True,
default=None,
help_text="Filter groups used to define sync-scope for users and groups.",
to="authentik_core.group",
),
),
]
19 changes: 13 additions & 6 deletions authentik/providers/scim/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,11 @@

exclude_users_service_account = models.BooleanField(default=False)

filter_group = models.ForeignKey(
"authentik_core.group", on_delete=models.SET_DEFAULT, default=None, null=True
filter_groups = models.ManyToManyField(
"authentik_core.group",
default=None,
blank=True,
help_text=_("Filter groups used to define sync-scope for users and groups."),
)

url = models.TextField(help_text=_("Base URL to SCIM requests, usually ends in /v2"))
Expand Down Expand Up @@ -121,12 +124,16 @@
base = base.exclude(type=UserTypes.SERVICE_ACCOUNT).exclude(
type=UserTypes.INTERNAL_SERVICE_ACCOUNT
)
if self.filter_group:
base = base.filter(ak_groups__in=[self.filter_group])
if self.filter_groups.exists():
base = base.filter(ak_groups__in=self.filter_groups.all())

Check warning on line 128 in authentik/providers/scim/models.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/models.py#L128

Added line #L128 was not covered by tests
return base.order_by("pk")
if type == Group:
# Get queryset of all groups with consistent ordering
return Group.objects.all().order_by("pk")
# Get a queryset of all roups with consistent ordering
# according to the provider's settings
base = Group.objects.all()
if self.filter_groups.exists():
base = base.filter(pk__in=self.filter_groups.all())

Check warning on line 135 in authentik/providers/scim/models.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/models.py#L135

Added line #L135 was not covered by tests
return base.order_by("pk")
raise ValueError(f"Invalid type {type}")

@property
Expand Down
226 changes: 226 additions & 0 deletions authentik/providers/scim/tests/test_filter_groups.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
"""SCIM Filter Groups tests"""

from django.test import TestCase
from requests_mock import Mocker

from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import Application, Group, User
from authentik.lib.generators import generate_id
from authentik.providers.scim.models import SCIMMapping, SCIMProvider
from authentik.providers.scim.tasks import scim_sync, sync_tasks


class SCIMFilterGroupsTests(TestCase):
"""SCIM Filter Groups tests"""

@apply_blueprint("system/providers-scim.yaml")
def setUp(self) -> None:
# Set up SCIM Provider
self.provider: SCIMProvider = SCIMProvider.objects.create(

Check warning on line 19 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L19

Added line #L19 was not covered by tests
name=generate_id(),
url="https://localhost",
token=generate_id(),
exclude_users_service_account=True,
)

self.app: Application = Application.objects.create(

Check warning on line 26 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L26

Added line #L26 was not covered by tests
name=generate_id(),
slug=generate_id(),
)
self.app.backchannel_providers.add(self.provider)
self.provider.property_mappings.add(

Check warning on line 31 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L30-L31

Added lines #L30 - L31 were not covered by tests
SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/user")
)
self.provider.property_mappings_group.add(

Check warning on line 34 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L34

Added line #L34 was not covered by tests
SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/group")
)

# Create test groups
self.filter_group1 = Group.objects.create(name="filter-group-1")
self.filter_group2 = Group.objects.create(name="filter-group-2")
self.unfiltered_group = Group.objects.create(name="unfiltered-group")

Check warning on line 41 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L39-L41

Added lines #L39 - L41 were not covered by tests

# Define group lists for testing
self.filter_groups = [self.filter_group1, self.filter_group2]
self.non_filter_groups = [self.unfiltered_group]
self.all_groups = self.filter_groups + self.non_filter_groups

Check warning on line 46 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L44-L46

Added lines #L44 - L46 were not covered by tests

# Create users in a loop and store in a dictionary
self.users = {}
for i in range(1, 5):
uid = generate_id()
self.users[i] = User.objects.create(

Check warning on line 52 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L49-L52

Added lines #L49 - L52 were not covered by tests
username=uid,
name=f"{uid} User",
email=f"{uid}@goauthentik.io",
)

# Define user groups
self.users_in_group1 = [self.users[1], self.users[3]]
self.users_in_group2 = [self.users[2], self.users[3]]
self.users_in_unfiltered_group = [self.users[4]]

Check warning on line 61 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L59-L61

Added lines #L59 - L61 were not covered by tests

# Calculate remaining groups
self.users_in_any_filter_group = list(set(self.users_in_group1 + self.users_in_group2))
self.all_users = list(self.users.values())
self.users_not_in_filter_groups = [

Check warning on line 66 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L64-L66

Added lines #L64 - L66 were not covered by tests
user for user in self.users.values() if user not in self.users_in_any_filter_group
]

# Assign users to groups
for user in self.users.values():
if user in self.users_in_group1:
user.ak_groups.add(self.filter_group1)
if user in self.users_in_group2:
user.ak_groups.add(self.filter_group2)
if user in self.users_in_unfiltered_group:
user.ak_groups.add(self.unfiltered_group)

Check warning on line 77 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L71-L77

Added lines #L71 - L77 were not covered by tests

def test_no_filter_groups(self):
"""Test with no filter groups set"""
# No filter groups set, should include all users and groups
users = self.provider.get_object_qs(User)
groups = self.provider.get_object_qs(Group)

Check warning on line 83 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L82-L83

Added lines #L82 - L83 were not covered by tests

self.assertEqual(users.count(), len(self.all_users))
self.assertGreaterEqual(groups.count(), len(self.all_groups))

Check warning on line 86 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L85-L86

Added lines #L85 - L86 were not covered by tests

# Verify all users are included
user_pks = list(users.values_list("pk", flat=True))
for user in self.all_users:
self.assertIn(user.pk, user_pks)

Check warning on line 91 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L89-L91

Added lines #L89 - L91 were not covered by tests

# Verify all test groups are included
group_pks = list(groups.values_list("pk", flat=True))
for group in self.all_groups:
self.assertIn(group.pk, group_pks)

Check warning on line 96 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L94-L96

Added lines #L94 - L96 were not covered by tests

def test_single_filter_group(self):
"""Test with one filter group set"""
# Set single filter group
self.provider.filter_groups.add(self.filter_group1)

Check warning on line 101 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L101

Added line #L101 was not covered by tests

users = self.provider.get_object_qs(User)
groups = self.provider.get_object_qs(Group)

Check warning on line 104 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L103-L104

Added lines #L103 - L104 were not covered by tests

# Verify test group is included
self.assertEqual(groups.count(), 1)

Check warning on line 107 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L107

Added line #L107 was not covered by tests

# Verify only users in filter_group1 are included
user_pks = set(users.values_list("pk", flat=True))
for user in self.all_users:
if user in self.users_in_group1:
self.assertIn(user.pk, user_pks)

Check warning on line 113 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L110-L113

Added lines #L110 - L113 were not covered by tests
else:
self.assertNotIn(user.pk, user_pks)

Check warning on line 115 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L115

Added line #L115 was not covered by tests

# Verify only filter_group1 is included
group_pks = list(groups.values_list("pk", flat=True))
for group in self.all_groups:
if group == self.filter_group1:
self.assertIn(group.pk, group_pks)

Check warning on line 121 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L118-L121

Added lines #L118 - L121 were not covered by tests
else:
self.assertNotIn(group.pk, group_pks)

Check warning on line 123 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L123

Added line #L123 was not covered by tests

def test_multiple_filter_groups(self):
"""Test with multiple filter groups set"""
# Set multiple filter groups
self.provider.filter_groups.add(*self.filter_groups)

Check warning on line 128 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L128

Added line #L128 was not covered by tests

users = self.provider.get_object_qs(User)
groups = self.provider.get_object_qs(Group)

Check warning on line 131 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L130-L131

Added lines #L130 - L131 were not covered by tests

# Only the two filter groups should be included
self.assertEqual(groups.count(), len(self.filter_groups))

Check warning on line 134 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L134

Added line #L134 was not covered by tests

# Verify users in either filter_group1 or filter_group2 are included
user_pks = set(users.values_list("pk", flat=True))
for user in self.all_users:
if user in self.users_in_any_filter_group:
self.assertIn(user.pk, user_pks)

Check warning on line 140 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L137-L140

Added lines #L137 - L140 were not covered by tests
else:
self.assertNotIn(user.pk, user_pks)

Check warning on line 142 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L142

Added line #L142 was not covered by tests

# Verify only the filter groups are included
group_pks = list(groups.values_list("pk", flat=True))
for group in self.all_groups:
if group in self.filter_groups:
self.assertIn(group.pk, group_pks)

Check warning on line 148 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L145-L148

Added lines #L145 - L148 were not covered by tests
else:
self.assertNotIn(group.pk, group_pks)

Check warning on line 150 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L150

Added line #L150 was not covered by tests

@Mocker()
def test_sync_with_filter_groups(self, mock: Mocker):
"""Test synchronization with filter groups set"""
# Add filter groups
self.provider.filter_groups.add(*self.filter_groups)

Check warning on line 156 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L156

Added line #L156 was not covered by tests

# Set up mock responses
mock.get(

Check warning on line 159 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L159

Added line #L159 was not covered by tests
"https://localhost/ServiceProviderConfig",
json={},
)

# Mock user creation
expected_users = self.users_in_any_filter_group
for user in expected_users:

Check warning on line 166 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L165-L166

Added lines #L165 - L166 were not covered by tests
# Mock POST response for creating the user
mock.post(

Check warning on line 168 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L168

Added line #L168 was not covered by tests
"https://localhost/Users",
json={"id": str(user.uid)},
)
# Mock PUT response for updating the user
mock.put(

Check warning on line 173 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L173

Added line #L173 was not covered by tests
f"https://localhost/Users/{user.uid}",
json={"id": str(user.uid)},
)
# Mock PATCH response for patching the user
mock.patch(

Check warning on line 178 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L178

Added line #L178 was not covered by tests
f"https://localhost/Users/{user.uid}",
json={"id": str(user.uid)},
)

# Mock group creation
expected_groups = self.filter_groups
for group in expected_groups:

Check warning on line 185 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L184-L185

Added lines #L184 - L185 were not covered by tests
# Mock POST response for creating the group
mock.post(

Check warning on line 187 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L187

Added line #L187 was not covered by tests
"https://localhost/Groups",
json={"id": str(group.pk)},
)
# Mock PUT response for updating the group
mock.put(

Check warning on line 192 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L192

Added line #L192 was not covered by tests
f"https://localhost/Groups/{group.pk}",
json={"id": str(group.pk)},
)
# Mock PATCH response for patching the group
mock.patch(

Check warning on line 197 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L197

Added line #L197 was not covered by tests
f"https://localhost/Groups/{group.pk}",
json={"id": str(group.pk)},
)

# Execute sync
sync_tasks.trigger_single_task(self.provider, scim_sync).get()

Check warning on line 203 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L203

Added line #L203 was not covered by tests

# Count POST calls to different endpoints
user_posts = 0
group_posts = 0
for req in mock.request_history:
if req.method == "POST":
if req.url == "https://localhost/Users":
user_posts += 1
elif req.url == "https://localhost/Groups":
group_posts += 1

Check warning on line 213 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L206-L213

Added lines #L206 - L213 were not covered by tests

# Verify number of synchronized users
self.assertEqual(

Check warning on line 216 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L216

Added line #L216 was not covered by tests
user_posts,
len(self.users_in_any_filter_group),
f"Expected {len(self.users_in_any_filter_group)} users to be synchronized",
)
# Verify number of synchronized groups
self.assertEqual(

Check warning on line 222 in authentik/providers/scim/tests/test_filter_groups.py

View check run for this annotation

Codecov / codecov/patch

authentik/providers/scim/tests/test_filter_groups.py#L222

Added line #L222 was not covered by tests
group_posts,
len(self.filter_groups),
f"Expected {len(self.users_in_any_filter_group)} groups to be synchronized",
)
13 changes: 9 additions & 4 deletions blueprints/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -6680,10 +6680,15 @@
"type": "boolean",
"title": "Exclude users service account"
},
"filter_group": {
"type": "string",
"format": "uuid",
"title": "Filter group"
"filter_groups": {
"type": "array",
"items": {
"type": "string",
"format": "uuid",
"description": "Filter groups used to define sync-scope for users and groups."
},
"title": "Filter groups",
"description": "Filter groups used to define sync-scope for users and groups."
},
"dry_run": {
"type": "boolean",
Expand Down
Loading
Loading