mirror of
https://github.com/goauthentik/authentik.git
synced 2026-06-17 19:09:11 +03:00
providers/scim: modify user- and group syncing behavior (#13947)
* providers/scim: modify user- and group syncing behavior rename filtergroup to groupfilters and allow multiple values only sync groups which are in the scimprovider's attribute \"group_filters\" only sync users which are entitled to view the scimprovider's application * Update authentik/providers/scim/api/providers.py Signed-off-by: Immanuel von Neumann <45020096+ImmanuelVonNeumann@users.noreply.github.com> * fix(authentik/scim): update schema.yml and test name * merge migrations Signed-off-by: Jens Langhammer <jens@goauthentik.io> * providers/scim: fix linting * format Signed-off-by: Jens Langhammer <jens@goauthentik.io> * filter eagerly Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Immanuel von Neumann <45020096+ImmanuelVonNeumann@users.noreply.github.com> Signed-off-by: Jens Langhammer <jens@goauthentik.io> Co-authored-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
committed by
GitHub
parent
fd209eeff9
commit
6ca26b501b
@@ -135,11 +135,11 @@ class GoogleWorkspaceProvider(OutgoingSyncProvider, BackchannelProvider):
|
||||
return GoogleWorkspaceGroupClient(self)
|
||||
raise ValueError(f"Invalid model {model}")
|
||||
|
||||
def get_object_qs(self, type: type[User | Group]) -> QuerySet[User | Group]:
|
||||
def get_object_qs(self, type: type[User | Group], **kwargs) -> QuerySet[User | Group]:
|
||||
if type == User:
|
||||
# Get queryset of all users with consistent ordering
|
||||
# according to the provider's settings
|
||||
base = User.objects.all().exclude_anonymous()
|
||||
base = User.objects.all().exclude_anonymous().filter(**kwargs)
|
||||
if self.exclude_users_service_account:
|
||||
base = base.exclude(type=UserTypes.SERVICE_ACCOUNT).exclude(
|
||||
type=UserTypes.INTERNAL_SERVICE_ACCOUNT
|
||||
@@ -149,7 +149,7 @@ class GoogleWorkspaceProvider(OutgoingSyncProvider, BackchannelProvider):
|
||||
return base.order_by("pk")
|
||||
if type == Group:
|
||||
# Get queryset of all groups with consistent ordering
|
||||
return Group.objects.all().order_by("pk")
|
||||
return Group.objects.all().filter(**kwargs).order_by("pk")
|
||||
raise ValueError(f"Invalid type {type}")
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -124,11 +124,11 @@ class MicrosoftEntraProvider(OutgoingSyncProvider, BackchannelProvider):
|
||||
return MicrosoftEntraGroupClient(self)
|
||||
raise ValueError(f"Invalid model {model}")
|
||||
|
||||
def get_object_qs(self, type: type[User | Group]) -> QuerySet[User | Group]:
|
||||
def get_object_qs(self, type: type[User | Group], **kwargs) -> QuerySet[User | Group]:
|
||||
if type == User:
|
||||
# Get queryset of all users with consistent ordering
|
||||
# according to the provider's settings
|
||||
base = User.objects.all().exclude_anonymous()
|
||||
base = User.objects.all().exclude_anonymous().filter(**kwargs)
|
||||
if self.exclude_users_service_account:
|
||||
base = base.exclude(type=UserTypes.SERVICE_ACCOUNT).exclude(
|
||||
type=UserTypes.INTERNAL_SERVICE_ACCOUNT
|
||||
@@ -138,7 +138,7 @@ class MicrosoftEntraProvider(OutgoingSyncProvider, BackchannelProvider):
|
||||
return base.order_by("pk")
|
||||
if type == Group:
|
||||
# Get queryset of all groups with consistent ordering
|
||||
return Group.objects.all().order_by("pk")
|
||||
return Group.objects.all().filter(**kwargs).order_by("pk")
|
||||
raise ValueError(f"Invalid type {type}")
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -53,7 +53,7 @@ class OutgoingSyncProvider(ScheduledModel, Model):
|
||||
) -> BaseOutgoingSyncClient[T, Any, Any, Self]:
|
||||
raise NotImplementedError
|
||||
|
||||
def get_object_qs[T: User | Group](self, type: type[T]) -> QuerySet[T]:
|
||||
def get_object_qs[T: User | Group](self, type: type[T], **kwargs) -> QuerySet[T]:
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -142,7 +142,7 @@ class SyncTasks:
|
||||
except TransientSyncException:
|
||||
return
|
||||
paginator = Paginator(
|
||||
provider.get_object_qs(_object_type).filter(**filter),
|
||||
provider.get_object_qs(_object_type, **filter),
|
||||
provider.sync_page_size,
|
||||
)
|
||||
if client.can_discover:
|
||||
@@ -227,13 +227,10 @@ class SyncTasks:
|
||||
return
|
||||
client = provider.client_for_model(instance.__class__)
|
||||
# Check if the object is allowed within the provider's restrictions
|
||||
queryset = provider.get_object_qs(instance.__class__)
|
||||
if not queryset:
|
||||
return
|
||||
|
||||
queryset = provider.get_object_qs(instance.__class__, pk=instance.pk)
|
||||
# The queryset we get from the provider must include the instance we've got given
|
||||
# otherwise ignore this provider
|
||||
if not queryset.filter(pk=instance.pk).exists():
|
||||
if not queryset or not queryset.exists():
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -351,10 +348,10 @@ class SyncTasks:
|
||||
return
|
||||
|
||||
# Check if the object is allowed within the provider's restrictions
|
||||
queryset: QuerySet = provider.get_object_qs(Group)
|
||||
queryset: QuerySet = provider.get_object_qs(Group, pk=group_pk)
|
||||
# The queryset we get from the provider must include the instance we've got given
|
||||
# otherwise ignore this provider
|
||||
if not queryset.filter(pk=group_pk).exists():
|
||||
if not queryset or not queryset.filter().exists():
|
||||
return
|
||||
|
||||
client = provider.client_for_model(Group)
|
||||
|
||||
@@ -38,9 +38,9 @@ class SCIMProviderSerializer(
|
||||
"compatibility_mode",
|
||||
"service_provider_config_cache_timeout",
|
||||
"exclude_users_service_account",
|
||||
"filter_group",
|
||||
"sync_page_size",
|
||||
"sync_page_timeout",
|
||||
"group_filters",
|
||||
"dry_run",
|
||||
]
|
||||
extra_kwargs = {}
|
||||
@@ -51,7 +51,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", "group_filters"]
|
||||
search_fields = ["name", "url"]
|
||||
ordering = ["name", "url"]
|
||||
sync_task = scim_sync
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# Generated by Django 5.0.13 on 2025-03-17 08:49
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
from django.apps.registry import Apps
|
||||
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
|
||||
def make_many_groups(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
"""
|
||||
Adds the Group object in SCIMProvider.filter_group to the
|
||||
many-to-many relationship in SCIMProvider.group_filters
|
||||
"""
|
||||
SCIMProvider = apps.get_model("authentik_providers_scim", "scimprovider")
|
||||
|
||||
for provider in SCIMProvider.objects.all():
|
||||
if not provider.filter_group:
|
||||
continue
|
||||
provider.group_filters.add(provider.filter_group)
|
||||
provider.dry_run = True
|
||||
provider.save(update_fields=["dry_run"])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0043_alter_group_options"),
|
||||
("authentik_providers_scim", "0018_scimprovider_service_provider_config_cache_timeout"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="scimprovider",
|
||||
name="group_filters",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
default=None,
|
||||
help_text="Group filters used to define sync-scope for 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",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(make_many_groups),
|
||||
migrations.RemoveField(
|
||||
model_name="scimprovider",
|
||||
name="filter_group",
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="scimprovider",
|
||||
name="group_filters",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
default=None,
|
||||
help_text="Group filters used to define sync-scope for groups.",
|
||||
to="authentik_core.group",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -17,6 +17,7 @@ from authentik.lib.models import InternallyManagedMixin, SerializerModel
|
||||
from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient
|
||||
from authentik.lib.sync.outgoing.models import OutgoingSyncProvider
|
||||
from authentik.lib.utils.time import timedelta_from_string, timedelta_string_validator
|
||||
from authentik.policies.engine import PolicyEngine
|
||||
from authentik.providers.scim.clients.auth import SCIMTokenAuth
|
||||
|
||||
LOGGER = get_logger()
|
||||
@@ -87,8 +88,11 @@ class SCIMProvider(OutgoingSyncProvider, BackchannelProvider):
|
||||
|
||||
exclude_users_service_account = models.BooleanField(default=False)
|
||||
|
||||
filter_group = models.ForeignKey(
|
||||
"authentik_core.group", on_delete=models.SET_DEFAULT, default=None, null=True
|
||||
group_filters = models.ManyToManyField(
|
||||
"authentik_core.group",
|
||||
default=None,
|
||||
blank=True,
|
||||
help_text=_("Group filters used to define sync-scope for groups."),
|
||||
)
|
||||
|
||||
url = models.TextField(help_text=_("Base URL to SCIM requests, usually ends in /v2"))
|
||||
@@ -176,21 +180,38 @@ class SCIMProvider(OutgoingSyncProvider, BackchannelProvider):
|
||||
cache.delete(cache_key)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def get_object_qs(self, type: type[User | Group]) -> QuerySet[User | Group]:
|
||||
def get_object_qs(self, type: type[User | Group], **kwargs) -> QuerySet[User | Group]:
|
||||
if type == User:
|
||||
# Get queryset of all users with consistent ordering
|
||||
# according to the provider's settings
|
||||
base = User.objects.all().exclude_anonymous()
|
||||
base = User.objects.all().exclude_anonymous().filter(**kwargs)
|
||||
if self.exclude_users_service_account:
|
||||
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])
|
||||
|
||||
# Filter users by their access to the backchannel application if an application is set
|
||||
# This handles both policy bindings and group_filters
|
||||
if self.backchannel_application:
|
||||
base = base.filter(
|
||||
pk__in=[
|
||||
user.pk
|
||||
for user in base
|
||||
if PolicyEngine(self.backchannel_application, user, None).build().passing
|
||||
]
|
||||
)
|
||||
return base.order_by("pk")
|
||||
|
||||
if type == Group:
|
||||
# Get queryset of all groups with consistent ordering
|
||||
return Group.objects.all().order_by("pk")
|
||||
# according to the provider's settings
|
||||
base = Group.objects.prefetch_related("scimprovidergroup_set").all().filter(**kwargs)
|
||||
|
||||
# Filter groups by group_filters if set
|
||||
if self.group_filters.exists():
|
||||
base = base.filter(pk__in=self.group_filters.values_list("pk", flat=True))
|
||||
|
||||
return base.order_by("pk")
|
||||
raise ValueError(f"Invalid type {type}")
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"""SCIM Application Policies tests"""
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.blueprints.tests import apply_blueprint
|
||||
from authentik.core.models import Application, Group, User
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.policies.models import PolicyBinding
|
||||
from authentik.providers.scim.models import SCIMMapping, SCIMProvider
|
||||
from authentik.tenants.models import Tenant
|
||||
|
||||
|
||||
class SCIMApplicationPoliciesTests(TestCase):
|
||||
"""SCIM Application Policies tests"""
|
||||
|
||||
@apply_blueprint("system/providers-scim.yaml")
|
||||
def setUp(self) -> None:
|
||||
# Delete all users and groups as to only have the test users and groups
|
||||
User.objects.all().exclude_anonymous().delete()
|
||||
Group.objects.all().delete()
|
||||
Tenant.objects.update(avatars="none")
|
||||
|
||||
self.provider: SCIMProvider = SCIMProvider.objects.create(
|
||||
name=generate_id(),
|
||||
url="https://localhost",
|
||||
token=generate_id(),
|
||||
exclude_users_service_account=True,
|
||||
)
|
||||
self.provider.property_mappings.add(
|
||||
SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/user")
|
||||
)
|
||||
self.provider.property_mappings_group.add(
|
||||
SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/group")
|
||||
)
|
||||
|
||||
self.app: Application = Application.objects.create(
|
||||
name=generate_id(),
|
||||
slug=generate_id(),
|
||||
)
|
||||
self.app.backchannel_providers.add(self.provider)
|
||||
|
||||
self.group1 = Group.objects.create(name="group-1")
|
||||
self.group2 = Group.objects.create(name="group-2")
|
||||
self.group3 = Group.objects.create(name="group-3")
|
||||
|
||||
self.users = {}
|
||||
for i in range(1, 5):
|
||||
uid = generate_id()
|
||||
self.users[i] = User.objects.create(
|
||||
username=uid,
|
||||
name=f"{uid} User",
|
||||
email=f"{uid}@goauthentik.io",
|
||||
)
|
||||
|
||||
self.users[1].ak_groups.add(self.group1)
|
||||
self.users[2].ak_groups.add(self.group2)
|
||||
self.users[4].ak_groups.add(self.group1)
|
||||
self.users[4].ak_groups.add(self.group2)
|
||||
|
||||
def test_no_group_policy(self):
|
||||
"""Test with no group policy set"""
|
||||
user_qs = self.provider.get_object_qs(User)
|
||||
|
||||
self.assertEqual(
|
||||
set([self.users[1].pk, self.users[2].pk, self.users[3].pk, self.users[4].pk]),
|
||||
set(user_qs.values_list("pk", flat=True)),
|
||||
)
|
||||
|
||||
def test_single_group_policy(self):
|
||||
"""Test with one group policy set"""
|
||||
PolicyBinding.objects.create(target=self.app, group=self.group1, order=0)
|
||||
|
||||
user_qs = self.provider.get_object_qs(User)
|
||||
|
||||
self.assertEqual(
|
||||
set([self.users[1].pk, self.users[4].pk]),
|
||||
set(user_qs.values_list("pk", flat=True)),
|
||||
)
|
||||
|
||||
def test_multiple_group_policies(self):
|
||||
"""Test with multiple group policies set"""
|
||||
PolicyBinding.objects.create(target=self.app, group=self.group1, order=0)
|
||||
PolicyBinding.objects.create(target=self.app, group=self.group2, order=0)
|
||||
|
||||
user_qs = self.provider.get_object_qs(User)
|
||||
|
||||
self.assertEqual(
|
||||
set([self.users[1].pk, self.users[2].pk, self.users[4].pk]),
|
||||
set(user_qs.values_list("pk", flat=True)),
|
||||
)
|
||||
@@ -0,0 +1,74 @@
|
||||
"""SCIM Group Filters tests"""
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
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
|
||||
|
||||
|
||||
class SCIMFilterGroupsTests(TestCase):
|
||||
"""SCIM Group Filters tests"""
|
||||
|
||||
@apply_blueprint("system/providers-scim.yaml")
|
||||
def setUp(self) -> None:
|
||||
# Delete all users and groups as to only have the test users and groups
|
||||
User.objects.all().exclude_anonymous().delete()
|
||||
Group.objects.all().delete()
|
||||
|
||||
self.provider: SCIMProvider = SCIMProvider.objects.create(
|
||||
name=generate_id(),
|
||||
url="https://localhost",
|
||||
token=generate_id(),
|
||||
exclude_users_service_account=True,
|
||||
)
|
||||
self.provider.property_mappings.add(
|
||||
SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/user")
|
||||
)
|
||||
self.provider.property_mappings_group.add(
|
||||
SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/group")
|
||||
)
|
||||
|
||||
self.app: Application = Application.objects.create(
|
||||
name=generate_id(),
|
||||
slug=generate_id(),
|
||||
)
|
||||
self.app.backchannel_providers.add(self.provider)
|
||||
|
||||
# Create test groups
|
||||
self.group1 = Group.objects.create(name="group-1")
|
||||
self.group2 = Group.objects.create(name="group-2")
|
||||
self.group3 = Group.objects.create(name="group-3")
|
||||
|
||||
def test_no_group_filters(self):
|
||||
"""Test with no group filters set"""
|
||||
group_qs = self.provider.get_object_qs(Group)
|
||||
|
||||
self.assertEqual(
|
||||
set([self.group1.pk, self.group2.pk, self.group3.pk]),
|
||||
set(group_qs.values_list("pk", flat=True)),
|
||||
)
|
||||
|
||||
def test_single_group_filter(self):
|
||||
"""Test with one group filter set"""
|
||||
self.provider.group_filters.add(self.group1)
|
||||
|
||||
group_qs = self.provider.get_object_qs(Group)
|
||||
|
||||
self.assertEqual(
|
||||
set([self.group1.pk]),
|
||||
set(group_qs.values_list("pk", flat=True)),
|
||||
)
|
||||
|
||||
def test_multiple_group_filters(self):
|
||||
"""Test with multiple group filters set"""
|
||||
self.provider.group_filters.add(self.group1)
|
||||
self.provider.group_filters.add(self.group2)
|
||||
|
||||
group_qs = self.provider.get_object_qs(Group)
|
||||
|
||||
self.assertEqual(
|
||||
set([self.group1.pk, self.group2.pk]),
|
||||
set(group_qs.values_list("pk", flat=True)),
|
||||
)
|
||||
+10
-5
@@ -10625,11 +10625,6 @@
|
||||
"type": "boolean",
|
||||
"title": "Exclude users service account"
|
||||
},
|
||||
"filter_group": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Filter group"
|
||||
},
|
||||
"sync_page_size": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
@@ -10643,6 +10638,16 @@
|
||||
"title": "Sync page timeout",
|
||||
"description": "Timeout for synchronization of a single page"
|
||||
},
|
||||
"group_filters": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Group filters used to define sync-scope for groups."
|
||||
},
|
||||
"title": "Group filters",
|
||||
"description": "Group filters used to define sync-scope for groups."
|
||||
},
|
||||
"dry_run": {
|
||||
"type": "boolean",
|
||||
"title": "Dry run",
|
||||
|
||||
+25
-15
@@ -18712,10 +18712,14 @@ paths:
|
||||
schema:
|
||||
type: boolean
|
||||
- in: query
|
||||
name: filter_group
|
||||
name: group_filters
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
explode: true
|
||||
style: form
|
||||
- $ref: '#/components/parameters/QueryName'
|
||||
- $ref: '#/components/parameters/QueryPaginationOrdering'
|
||||
- $ref: '#/components/parameters/QueryPaginationPage'
|
||||
@@ -49533,10 +49537,6 @@ components:
|
||||
to disable.
|
||||
exclude_users_service_account:
|
||||
type: boolean
|
||||
filter_group:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
sync_page_size:
|
||||
type: integer
|
||||
maximum: 2147483647
|
||||
@@ -49546,6 +49546,12 @@ components:
|
||||
type: string
|
||||
minLength: 1
|
||||
description: Timeout for synchronization of a single page
|
||||
group_filters:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Group filters used to define sync-scope for groups.
|
||||
dry_run:
|
||||
type: boolean
|
||||
description: When enabled, provider will not modify or create objects in
|
||||
@@ -53439,10 +53445,6 @@ components:
|
||||
to disable.
|
||||
exclude_users_service_account:
|
||||
type: boolean
|
||||
filter_group:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
sync_page_size:
|
||||
type: integer
|
||||
maximum: 2147483647
|
||||
@@ -53451,6 +53453,12 @@ components:
|
||||
sync_page_timeout:
|
||||
type: string
|
||||
description: Timeout for synchronization of a single page
|
||||
group_filters:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Group filters used to define sync-scope for groups.
|
||||
dry_run:
|
||||
type: boolean
|
||||
description: When enabled, provider will not modify or create objects in
|
||||
@@ -53561,10 +53569,6 @@ components:
|
||||
to disable.
|
||||
exclude_users_service_account:
|
||||
type: boolean
|
||||
filter_group:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
sync_page_size:
|
||||
type: integer
|
||||
maximum: 2147483647
|
||||
@@ -53574,6 +53578,12 @@ components:
|
||||
type: string
|
||||
minLength: 1
|
||||
description: Timeout for synchronization of a single page
|
||||
group_filters:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Group filters used to define sync-scope for groups.
|
||||
dry_run:
|
||||
type: boolean
|
||||
description: When enabled, provider will not modify or create objects in
|
||||
|
||||
@@ -12,15 +12,17 @@ import "#components/ak-number-input";
|
||||
import "#elements/utils/TimeDeltaHelp";
|
||||
import "#components/ak-text-input";
|
||||
|
||||
import { propertyMappingsProvider, propertyMappingsSelector } from "./SCIMProviderFormHelpers.js";
|
||||
import {
|
||||
groupsProvider,
|
||||
groupsSelector,
|
||||
propertyMappingsProvider,
|
||||
propertyMappingsSelector,
|
||||
} from "./SCIMProviderFormHelpers.js";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
import {
|
||||
CompatibilityModeEnum,
|
||||
CoreApi,
|
||||
CoreGroupsListRequest,
|
||||
Group,
|
||||
OAuthSource,
|
||||
SCIMAuthenticationModeEnum,
|
||||
SCIMProvider,
|
||||
@@ -235,33 +237,15 @@ export function renderForm({ provider = {}, errors = {}, update }: SCIMProviderF
|
||||
>
|
||||
</ak-switch-input>
|
||||
|
||||
<ak-form-element-horizontal label=${msg("Group")} name="filterGroup">
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Group[]> => {
|
||||
const args: CoreGroupsListRequest = {
|
||||
ordering: "name",
|
||||
includeUsers: false,
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const groups = await new CoreApi(DEFAULT_CONFIG).coreGroupsList(args);
|
||||
return groups.results;
|
||||
}}
|
||||
.renderElement=${(group: Group): string => {
|
||||
return group.name;
|
||||
}}
|
||||
.value=${(group: Group | undefined): string | undefined => {
|
||||
return group ? group.pk : undefined;
|
||||
}}
|
||||
.selected=${(group: Group): boolean => {
|
||||
return group.pk === provider.filterGroup;
|
||||
}}
|
||||
blankable
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-form-element-horizontal label=${msg("Group Filter")} name="groupFilters">
|
||||
<ak-dual-select-dynamic-selected
|
||||
.provider=${groupsProvider}
|
||||
.selector=${groupsSelector(provider?.groupFilters, null)}
|
||||
available-label=${msg("Available Groups")}
|
||||
selected-label=${msg("Selected Groups")}
|
||||
></ak-dual-select-dynamic-selected>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Only sync users within the selected group.")}
|
||||
${msg("Groups to be synced. If empty, all groups will be synced.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
|
||||
@@ -2,9 +2,10 @@ import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
import { DualSelectPair } from "#elements/ak-dual-select/types";
|
||||
|
||||
import { PropertymappingsApi, SCIMMapping } from "@goauthentik/api";
|
||||
import { CoreApi, Group, PropertymappingsApi, SCIMMapping } from "@goauthentik/api";
|
||||
|
||||
const mappingToSelect = (m: SCIMMapping) => [m.pk, m.name, m.name, m];
|
||||
const groupToSelect = (g: Group) => [g.pk, g.name, g.name, g];
|
||||
|
||||
export async function propertyMappingsProvider(page = 1, search = "") {
|
||||
const propertyMappings = await new PropertymappingsApi(
|
||||
@@ -47,3 +48,45 @@ export function propertyMappingsSelector(
|
||||
.map(mappingToSelect);
|
||||
};
|
||||
}
|
||||
|
||||
export async function groupsProvider(page = 1, search = "") {
|
||||
const groups = await new CoreApi(DEFAULT_CONFIG).coreGroupsList({
|
||||
ordering: "name",
|
||||
includeUsers: false,
|
||||
pageSize: 20,
|
||||
search: search.trim(),
|
||||
page,
|
||||
});
|
||||
return {
|
||||
pagination: groups.pagination,
|
||||
options: groups.results.map(groupToSelect),
|
||||
};
|
||||
}
|
||||
|
||||
export function groupsSelector(
|
||||
instanceGroups: string[] | undefined,
|
||||
defaultSelected: string | null = null,
|
||||
) {
|
||||
// If we have no instance groups (new provider), return empty selection
|
||||
// if (!instanceGroups || instanceGroups.length === 0) {
|
||||
if (!instanceGroups) {
|
||||
return async (groups: DualSelectPair<Group>[]) =>
|
||||
groups.filter(
|
||||
([_0, _1, _2, group]: DualSelectPair<Group>) => group?.name === defaultSelected,
|
||||
);
|
||||
}
|
||||
|
||||
// For existing providers, load the selected groups
|
||||
return async () => {
|
||||
const groups = await Promise.allSettled(
|
||||
instanceGroups.map((groupId) =>
|
||||
new CoreApi(DEFAULT_CONFIG).coreGroupsRetrieve({ groupUuid: groupId }),
|
||||
),
|
||||
);
|
||||
|
||||
return groups
|
||||
.filter((s) => s.status === "fulfilled")
|
||||
.map((s) => (s as PromiseFulfilledResult<Group>).value)
|
||||
.map(groupToSelect);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -64,7 +64,23 @@ All selected mappings are applied in the order of their name, and are deeply mer
|
||||
|
||||
### Compatibility modes
|
||||
|
||||
Some applications require specific adjustments to work correctly with SCIM. authentik provides compatibility modes that modify SCIM behavior for vendor-specific implementations.
|
||||
By default, service accounts are excluded from being synchronized. This can be configured in the SCIM provider.
|
||||
|
||||
#### User Filtering
|
||||
|
||||
Users can be filtered using application policies.
|
||||
|
||||
Only users who can view the scim provider's application are synced by the scim provider.
|
||||
|
||||
#### Group Filters
|
||||
|
||||
Group Filters allow you to define the group syncing scope of a SCIM provider.
|
||||
|
||||
In its default configuration, with no group filters selected, the SCIM provider will sync all groups.
|
||||
|
||||
If group filters are selected, only selected groups will be synced.
|
||||
|
||||
Currently, changes to filter groups do _not_ remove previously synchronized groups and members.
|
||||
|
||||
Available compatibility modes:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user