providers/sync: improve v3 (#9966)

* make external id field externally visible

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* catch up scim provider

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add missing views to scim provider

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* make neither user nor group required for mapping testing

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* improve SkipObject handling

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* allow deletion of connection objects

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* make entra logs less noisy

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* make event_matcher less noisy

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L
2024-06-06 12:47:48 +09:00
committed by GitHub
parent 0c652a210d
commit 88e9c9b669
28 changed files with 963 additions and 51 deletions

View File

@ -58,7 +58,7 @@ from authentik.outposts.models import OutpostServiceConnection
from authentik.policies.models import Policy, PolicyBindingModel
from authentik.policies.reputation.models import Reputation
from authentik.providers.oauth2.models import AccessToken, AuthorizationCode, RefreshToken
from authentik.providers.scim.models import SCIMGroup, SCIMUser
from authentik.providers.scim.models import SCIMProviderGroup, SCIMProviderUser
from authentik.sources.scim.models import SCIMSourceGroup, SCIMSourceUser
from authentik.stages.authenticator_webauthn.models import WebAuthnDeviceType
from authentik.tenants.models import Tenant
@ -97,8 +97,8 @@ def excluded_models() -> list[type[Model]]:
# FIXME: these shouldn't need to be explicitly listed, but rather based off of a mixin
FlowToken,
LicenseUsage,
SCIMGroup,
SCIMUser,
SCIMProviderGroup,
SCIMProviderUser,
Tenant,
SystemTask,
ConnectionToken,

View File

@ -80,8 +80,10 @@ class PropertyMappingViewSet(
class PropertyMappingTestSerializer(PolicyTestSerializer):
"""Test property mapping execution for a user/group with context"""
user = PrimaryKeyRelatedField(queryset=User.objects.all(), required=False)
group = PrimaryKeyRelatedField(queryset=Group.objects.all(), required=False)
user = PrimaryKeyRelatedField(queryset=User.objects.all(), required=False, allow_null=True)
group = PrimaryKeyRelatedField(
queryset=Group.objects.all(), required=False, allow_null=True
)
queryset = PropertyMapping.objects.select_subclasses()
serializer_class = PropertyMappingSerializer

View File

@ -19,6 +19,7 @@ class GoogleWorkspaceProviderGroupSerializer(ModelSerializer):
model = GoogleWorkspaceProviderGroup
fields = [
"id",
"google_id",
"group",
"group_obj",
"provider",

View File

@ -19,6 +19,7 @@ class GoogleWorkspaceProviderUserSerializer(ModelSerializer):
model = GoogleWorkspaceProviderUser
fields = [
"id",
"google_id",
"user",
"user_obj",
"provider",

View File

@ -19,6 +19,7 @@ class MicrosoftEntraProviderGroupSerializer(ModelSerializer):
model = MicrosoftEntraProviderGroup
fields = [
"id",
"microsoft_id",
"group",
"group_obj",
"provider",

View File

@ -19,6 +19,7 @@ class MicrosoftEntraProviderUserSerializer(ModelSerializer):
model = MicrosoftEntraProviderUser
fields = [
"id",
"microsoft_id",
"user",
"user_obj",
"provider",

View File

@ -102,6 +102,8 @@ def get_logger_config():
"gunicorn": "INFO",
"requests_mock": "WARNING",
"hpack": "WARNING",
"httpx": "WARNING",
"azure": "WARNING",
}
for handler_name, level in handler_level_map.items():
base_config["loggers"][handler_name] = {

View File

@ -57,6 +57,8 @@ class PropertyMappingManager:
mapping.set_context(user, request, **kwargs)
try:
value = mapping.evaluate(mapping.model.expression)
except PropertyMappingExpressionException as exc:
raise exc from exc
except Exception as exc:
raise PropertyMappingExpressionException(exc, mapping.model) from exc
if value is None:

View File

@ -91,10 +91,9 @@ class BaseOutgoingSyncClient[
}
eval_kwargs.setdefault("user", None)
for value in self.mapper.iter_eval(**eval_kwargs):
try:
always_merger.merge(raw_final_object, value)
except SkipObjectException as exc:
raise exc from exc
always_merger.merge(raw_final_object, value)
except SkipObjectException as exc:
raise exc from exc
except PropertyMappingExpressionException as exc:
# Value error can be raised when assigning invalid data to an attribute
Event.new(
@ -104,7 +103,7 @@ class BaseOutgoingSyncClient[
).save()
raise StopSync(exc, obj, exc.mapping) from exc
if not raw_final_object:
raise StopSync(ValueError("No user mappings configured"), obj)
raise StopSync(ValueError("No mappings configured"), obj)
for key, value in defaults.items():
raw_final_object.setdefault(key, value)
return raw_final_object

View File

@ -125,6 +125,7 @@ class SyncTasks:
try:
client.write(obj)
except SkipObjectException:
self.logger.debug("skipping object due to SkipObject", obj=obj)
continue
except BadRequestSyncException as exc:
self.logger.warning("failed to sync object", exc=exc, obj=obj)

View File

@ -102,7 +102,7 @@ class EventMatcherPolicy(Policy):
result = checker(request, event)
if result is None:
continue
LOGGER.info(
LOGGER.debug(
"Event matcher check result",
checker=checker.__name__,
result=result,

View File

@ -0,0 +1,43 @@
"""SCIMProviderGroup API Views"""
from rest_framework import mixins
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserGroupSerializer
from authentik.providers.scim.models import SCIMProviderGroup
class SCIMProviderGroupSerializer(ModelSerializer):
"""SCIMProviderGroup Serializer"""
group_obj = UserGroupSerializer(source="group", read_only=True)
class Meta:
model = SCIMProviderGroup
fields = [
"id",
"scim_id",
"group",
"group_obj",
"provider",
]
class SCIMProviderGroupViewSet(
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):
"""SCIMProviderGroup Viewset"""
queryset = SCIMProviderGroup.objects.all().select_related("group")
serializer_class = SCIMProviderGroupSerializer
filterset_fields = ["provider__id", "group__name", "group__group_uuid"]
search_fields = ["provider__name", "group__name"]
ordering = ["group__name"]

View File

@ -0,0 +1,43 @@
"""SCIMProviderUser API Views"""
from rest_framework import mixins
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.groups import GroupMemberSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.providers.scim.models import SCIMProviderUser
class SCIMProviderUserSerializer(ModelSerializer):
"""SCIMProviderUser Serializer"""
user_obj = GroupMemberSerializer(source="user", read_only=True)
class Meta:
model = SCIMProviderUser
fields = [
"id",
"scim_id",
"user",
"user_obj",
"provider",
]
class SCIMProviderUserViewSet(
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):
"""SCIMProviderUser Viewset"""
queryset = SCIMProviderUser.objects.all().select_related("user")
serializer_class = SCIMProviderUserSerializer
filterset_fields = ["provider__id", "user__username", "user__id"]
search_fields = ["provider__name", "user__username"]
ordering = ["user__username"]

View File

@ -19,13 +19,18 @@ from authentik.providers.scim.clients.exceptions import (
)
from authentik.providers.scim.clients.schema import SCIM_GROUP_SCHEMA, PatchRequest
from authentik.providers.scim.clients.schema import Group as SCIMGroupSchema
from authentik.providers.scim.models import SCIMGroup, SCIMMapping, SCIMProvider, SCIMUser
from authentik.providers.scim.models import (
SCIMMapping,
SCIMProvider,
SCIMProviderGroup,
SCIMProviderUser,
)
class SCIMGroupClient(SCIMClient[Group, SCIMGroup, SCIMGroupSchema]):
class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]):
"""SCIM client for groups"""
connection_type = SCIMGroup
connection_type = SCIMProviderGroup
connection_type_query = "group"
mapper: PropertyMappingManager
@ -37,7 +42,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroup, SCIMGroupSchema]):
["group", "provider", "connection"],
)
def to_schema(self, obj: Group, connection: SCIMGroup) -> SCIMGroupSchema:
def to_schema(self, obj: Group, connection: SCIMProviderGroup) -> SCIMGroupSchema:
"""Convert authentik user into SCIM"""
raw_scim_group = super().to_schema(
obj,
@ -52,7 +57,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroup, SCIMGroupSchema]):
scim_group.externalId = str(obj.pk)
users = list(obj.users.order_by("id").values_list("id", flat=True))
connections = SCIMUser.objects.filter(provider=self.provider, user__pk__in=users)
connections = SCIMProviderUser.objects.filter(provider=self.provider, user__pk__in=users)
members = []
for user in connections:
members.append(
@ -66,7 +71,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroup, SCIMGroupSchema]):
def delete(self, obj: Group):
"""Delete group"""
scim_group = SCIMGroup.objects.filter(provider=self.provider, group=obj).first()
scim_group = SCIMProviderGroup.objects.filter(provider=self.provider, group=obj).first()
if not scim_group:
self.logger.debug("Group does not exist in SCIM, skipping")
return None
@ -88,9 +93,11 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroup, SCIMGroupSchema]):
scim_id = response.get("id")
if not scim_id or scim_id == "":
raise StopSync("SCIM Response with missing or invalid `id`")
return SCIMGroup.objects.create(provider=self.provider, group=group, scim_id=scim_id)
return SCIMProviderGroup.objects.create(
provider=self.provider, group=group, scim_id=scim_id
)
def update(self, group: Group, connection: SCIMGroup):
def update(self, group: Group, connection: SCIMProviderGroup):
"""Update existing group"""
scim_group = self.to_schema(group, connection)
scim_group.id = connection.scim_id
@ -158,16 +165,16 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroup, SCIMGroupSchema]):
"""Add users in users_set to group"""
if len(users_set) < 1:
return
scim_group = SCIMGroup.objects.filter(provider=self.provider, group=group).first()
scim_group = SCIMProviderGroup.objects.filter(provider=self.provider, group=group).first()
if not scim_group:
self.logger.warning(
"could not sync group membership, group does not exist", group=group
)
return
user_ids = list(
SCIMUser.objects.filter(user__pk__in=users_set, provider=self.provider).values_list(
"scim_id", flat=True
)
SCIMProviderUser.objects.filter(
user__pk__in=users_set, provider=self.provider
).values_list("scim_id", flat=True)
)
if len(user_ids) < 1:
return
@ -184,16 +191,16 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroup, SCIMGroupSchema]):
"""Remove users in users_set from group"""
if len(users_set) < 1:
return
scim_group = SCIMGroup.objects.filter(provider=self.provider, group=group).first()
scim_group = SCIMProviderGroup.objects.filter(provider=self.provider, group=group).first()
if not scim_group:
self.logger.warning(
"could not sync group membership, group does not exist", group=group
)
return
user_ids = list(
SCIMUser.objects.filter(user__pk__in=users_set, provider=self.provider).values_list(
"scim_id", flat=True
)
SCIMProviderUser.objects.filter(
user__pk__in=users_set, provider=self.provider
).values_list("scim_id", flat=True)
)
if len(user_ids) < 1:
return

View File

@ -9,13 +9,13 @@ from authentik.policies.utils import delete_none_values
from authentik.providers.scim.clients.base import SCIMClient
from authentik.providers.scim.clients.schema import SCIM_USER_SCHEMA
from authentik.providers.scim.clients.schema import User as SCIMUserSchema
from authentik.providers.scim.models import SCIMMapping, SCIMProvider, SCIMUser
from authentik.providers.scim.models import SCIMMapping, SCIMProvider, SCIMProviderUser
class SCIMUserClient(SCIMClient[User, SCIMUser, SCIMUserSchema]):
class SCIMUserClient(SCIMClient[User, SCIMProviderUser, SCIMUserSchema]):
"""SCIM client for users"""
connection_type = SCIMUser
connection_type = SCIMProviderUser
connection_type_query = "user"
mapper: PropertyMappingManager
@ -27,7 +27,7 @@ class SCIMUserClient(SCIMClient[User, SCIMUser, SCIMUserSchema]):
["provider", "connection"],
)
def to_schema(self, obj: User, connection: SCIMUser) -> SCIMUserSchema:
def to_schema(self, obj: User, connection: SCIMProviderUser) -> SCIMUserSchema:
"""Convert authentik user into SCIM"""
raw_scim_user = super().to_schema(
obj,
@ -44,7 +44,7 @@ class SCIMUserClient(SCIMClient[User, SCIMUser, SCIMUserSchema]):
def delete(self, obj: User):
"""Delete user"""
scim_user = SCIMUser.objects.filter(provider=self.provider, user=obj).first()
scim_user = SCIMProviderUser.objects.filter(provider=self.provider, user=obj).first()
if not scim_user:
self.logger.debug("User does not exist in SCIM, skipping")
return None
@ -66,9 +66,9 @@ class SCIMUserClient(SCIMClient[User, SCIMUser, SCIMUserSchema]):
scim_id = response.get("id")
if not scim_id or scim_id == "":
raise StopSync("SCIM Response with missing or invalid `id`")
return SCIMUser.objects.create(provider=self.provider, user=user, scim_id=scim_id)
return SCIMProviderUser.objects.create(provider=self.provider, user=user, scim_id=scim_id)
def update(self, user: User, connection: SCIMUser):
def update(self, user: User, connection: SCIMProviderUser):
"""Update existing user"""
scim_user = self.to_schema(user, connection)
scim_user.id = connection.scim_id

View File

@ -0,0 +1,24 @@
# Generated by Django 5.0.6 on 2024-06-04 07:45
from django.conf import settings
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0035_alter_group_options_and_more"),
("authentik_providers_scim", "0007_scimgroup_scim_id_scimuser_scim_id_and_more"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RenameModel(
old_name="SCIMGroup",
new_name="SCIMProviderGroup",
),
migrations.RenameModel(
old_name="SCIMUser",
new_name="SCIMProviderUser",
),
]

View File

@ -10,6 +10,7 @@ from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import Serializer
from authentik.core.models import BackchannelProvider, Group, PropertyMapping, User, UserTypes
from authentik.lib.models import SerializerModel
from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient
from authentik.lib.sync.outgoing.models import OutgoingSyncProvider
@ -106,7 +107,7 @@ class SCIMMapping(PropertyMapping):
verbose_name_plural = _("SCIM Mappings")
class SCIMUser(models.Model):
class SCIMProviderUser(SerializerModel):
"""Mapping of a user and provider to a SCIM user ID"""
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
@ -114,14 +115,20 @@ class SCIMUser(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
provider = models.ForeignKey(SCIMProvider, on_delete=models.CASCADE)
@property
def serializer(self) -> type[Serializer]:
from authentik.providers.scim.api.users import SCIMProviderUserSerializer
return SCIMProviderUserSerializer
class Meta:
unique_together = (("scim_id", "user", "provider"),)
def __str__(self) -> str:
return f"SCIM User {self.user_id} to {self.provider_id}"
return f"SCIM Provider User {self.user_id} to {self.provider_id}"
class SCIMGroup(models.Model):
class SCIMProviderGroup(SerializerModel):
"""Mapping of a group and provider to a SCIM user ID"""
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
@ -129,8 +136,14 @@ class SCIMGroup(models.Model):
group = models.ForeignKey(Group, on_delete=models.CASCADE)
provider = models.ForeignKey(SCIMProvider, on_delete=models.CASCADE)
@property
def serializer(self) -> type[Serializer]:
from authentik.providers.scim.api.groups import SCIMProviderGroupSerializer
return SCIMProviderGroupSerializer
class Meta:
unique_together = (("scim_id", "group", "provider"),)
def __str__(self) -> str:
return f"SCIM Group {self.group_id} to {self.provider_id}"
return f"SCIM Provider Group {self.group_id} to {self.provider_id}"

View File

@ -1,9 +1,17 @@
"""API URLs"""
from authentik.providers.scim.api.groups import (
SCIMProviderGroupViewSet,
)
from authentik.providers.scim.api.property_mappings import SCIMMappingViewSet
from authentik.providers.scim.api.providers import SCIMProviderViewSet
from authentik.providers.scim.api.users import (
SCIMProviderUserViewSet,
)
api_urlpatterns = [
("providers/scim", SCIMProviderViewSet),
("providers/scim_users", SCIMProviderUserViewSet),
("providers/scim_groups", SCIMProviderGroupViewSet),
("propertymappings/scim", SCIMMappingViewSet),
]

View File

@ -161,19 +161,18 @@ class BaseLDAPSynchronizer:
dn=object_dn,
source=self._source,
):
try:
if isinstance(value, (bytes)):
self._logger.warning("property mapping returned bytes", mapping=mapping)
continue
object_field = mapping.object_field
if object_field.startswith("attributes."):
# Because returning a list might desired, we can't
# rely on flatten here. Instead, just save the result as-is
set_path_in_dict(properties, object_field, value)
else:
properties[object_field] = flatten(value)
except SkipObjectException as exc:
raise exc from exc
if isinstance(value, (bytes)):
self._logger.warning("property mapping returned bytes", mapping=mapping)
continue
object_field = mapping.object_field
if object_field.startswith("attributes."):
# Because returning a list might desired, we can't
# rely on flatten here. Instead, just save the result as-is
set_path_in_dict(properties, object_field, value)
else:
properties[object_field] = flatten(value)
except SkipObjectException as exc:
raise exc from exc
except PropertyMappingExpressionException as exc:
# Value error can be raised when assigning invalid data to an attribute
Event.new(

View File

@ -19726,6 +19726,403 @@ paths:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/providers/scim_groups/:
get:
operationId: providers_scim_groups_list
description: SCIMProviderGroup Viewset
parameters:
- in: query
name: group__group_uuid
schema:
type: string
format: uuid
- in: query
name: group__name
schema:
type: string
- name: ordering
required: false
in: query
description: Which field to use when ordering the results.
schema:
type: string
- name: page
required: false
in: query
description: A page number within the paginated result set.
schema:
type: integer
- name: page_size
required: false
in: query
description: Number of results to return per page.
schema:
type: integer
- in: query
name: provider__id
schema:
type: integer
- name: search
required: false
in: query
description: A search term.
schema:
type: string
tags:
- providers
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/PaginatedSCIMProviderGroupList'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
post:
operationId: providers_scim_groups_create
description: SCIMProviderGroup Viewset
tags:
- providers
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/SCIMProviderGroupRequest'
required: true
security:
- authentik: []
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/SCIMProviderGroup'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/providers/scim_groups/{id}/:
get:
operationId: providers_scim_groups_retrieve
description: SCIMProviderGroup Viewset
parameters:
- in: path
name: id
schema:
type: string
format: uuid
description: A UUID string identifying this scim provider group.
required: true
tags:
- providers
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/SCIMProviderGroup'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
delete:
operationId: providers_scim_groups_destroy
description: SCIMProviderGroup Viewset
parameters:
- in: path
name: id
schema:
type: string
format: uuid
description: A UUID string identifying this scim provider group.
required: true
tags:
- providers
security:
- authentik: []
responses:
'204':
description: No response body
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/providers/scim_groups/{id}/used_by/:
get:
operationId: providers_scim_groups_used_by_list
description: Get a list of all objects that use this object
parameters:
- in: path
name: id
schema:
type: string
format: uuid
description: A UUID string identifying this scim provider group.
required: true
tags:
- providers
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/UsedBy'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/providers/scim_users/:
get:
operationId: providers_scim_users_list
description: SCIMProviderUser Viewset
parameters:
- name: ordering
required: false
in: query
description: Which field to use when ordering the results.
schema:
type: string
- name: page
required: false
in: query
description: A page number within the paginated result set.
schema:
type: integer
- name: page_size
required: false
in: query
description: Number of results to return per page.
schema:
type: integer
- in: query
name: provider__id
schema:
type: integer
- name: search
required: false
in: query
description: A search term.
schema:
type: string
- in: query
name: user__id
schema:
type: integer
- in: query
name: user__username
schema:
type: string
tags:
- providers
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/PaginatedSCIMProviderUserList'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
post:
operationId: providers_scim_users_create
description: SCIMProviderUser Viewset
tags:
- providers
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/SCIMProviderUserRequest'
required: true
security:
- authentik: []
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/SCIMProviderUser'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/providers/scim_users/{id}/:
get:
operationId: providers_scim_users_retrieve
description: SCIMProviderUser Viewset
parameters:
- in: path
name: id
schema:
type: string
format: uuid
description: A UUID string identifying this scim provider user.
required: true
tags:
- providers
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/SCIMProviderUser'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
delete:
operationId: providers_scim_users_destroy
description: SCIMProviderUser Viewset
parameters:
- in: path
name: id
schema:
type: string
format: uuid
description: A UUID string identifying this scim provider user.
required: true
tags:
- providers
security:
- authentik: []
responses:
'204':
description: No response body
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/providers/scim_users/{id}/used_by/:
get:
operationId: providers_scim_users_used_by_list
description: Get a list of all objects that use this object
parameters:
- in: path
name: id
schema:
type: string
format: uuid
description: A UUID string identifying this scim provider user.
required: true
tags:
- providers
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/UsedBy'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/rac/connection_tokens/:
get:
operationId: rac_connection_tokens_list
@ -36335,6 +36732,8 @@ components:
type: string
format: uuid
readOnly: true
google_id:
type: string
group:
type: string
format: uuid
@ -36348,6 +36747,7 @@ components:
readOnly: true
required:
- attributes
- google_id
- group
- group_obj
- id
@ -36356,12 +36756,16 @@ components:
type: object
description: GoogleWorkspaceProviderGroup Serializer
properties:
google_id:
type: string
minLength: 1
group:
type: string
format: uuid
provider:
type: integer
required:
- google_id
- group
- provider
GoogleWorkspaceProviderMapping:
@ -36484,6 +36888,8 @@ components:
type: string
format: uuid
readOnly: true
google_id:
type: string
user:
type: integer
user_obj:
@ -36496,6 +36902,7 @@ components:
readOnly: true
required:
- attributes
- google_id
- id
- provider
- user
@ -36504,11 +36911,15 @@ components:
type: object
description: GoogleWorkspaceProviderUser Serializer
properties:
google_id:
type: string
minLength: 1
user:
type: integer
provider:
type: integer
required:
- google_id
- provider
- user
Group:
@ -38003,6 +38414,8 @@ components:
type: string
format: uuid
readOnly: true
microsoft_id:
type: string
group:
type: string
format: uuid
@ -38019,11 +38432,15 @@ components:
- group
- group_obj
- id
- microsoft_id
- provider
MicrosoftEntraProviderGroupRequest:
type: object
description: MicrosoftEntraProviderGroup Serializer
properties:
microsoft_id:
type: string
minLength: 1
group:
type: string
format: uuid
@ -38031,6 +38448,7 @@ components:
type: integer
required:
- group
- microsoft_id
- provider
MicrosoftEntraProviderMapping:
type: object
@ -38149,6 +38567,8 @@ components:
type: string
format: uuid
readOnly: true
microsoft_id:
type: string
user:
type: integer
user_obj:
@ -38162,6 +38582,7 @@ components:
required:
- attributes
- id
- microsoft_id
- provider
- user
- user_obj
@ -38169,11 +38590,15 @@ components:
type: object
description: MicrosoftEntraProviderUser Serializer
properties:
microsoft_id:
type: string
minLength: 1
user:
type: integer
provider:
type: integer
required:
- microsoft_id
- provider
- user
ModelEnum:
@ -40166,6 +40591,18 @@ components:
required:
- pagination
- results
PaginatedSCIMProviderGroupList:
type: object
properties:
pagination:
$ref: '#/components/schemas/Pagination'
results:
type: array
items:
$ref: '#/components/schemas/SCIMProviderGroup'
required:
- pagination
- results
PaginatedSCIMProviderList:
type: object
properties:
@ -40178,6 +40615,18 @@ components:
required:
- pagination
- results
PaginatedSCIMProviderUserList:
type: object
properties:
pagination:
$ref: '#/components/schemas/Pagination'
results:
type: array
items:
$ref: '#/components/schemas/SCIMProviderUser'
required:
- pagination
- results
PaginatedSCIMSourceGroupList:
type: object
properties:
@ -44147,12 +44596,14 @@ components:
properties:
user:
type: integer
nullable: true
context:
type: object
additionalProperties: {}
group:
type: string
format: uuid
nullable: true
PropertyMappingTestResult:
type: object
description: Result of a Property-mapping test
@ -45906,6 +46357,47 @@ components:
- url
- verbose_name
- verbose_name_plural
SCIMProviderGroup:
type: object
description: SCIMProviderGroup Serializer
properties:
id:
type: string
format: uuid
readOnly: true
scim_id:
type: string
group:
type: string
format: uuid
group_obj:
allOf:
- $ref: '#/components/schemas/UserGroup'
readOnly: true
provider:
type: integer
required:
- group
- group_obj
- id
- provider
- scim_id
SCIMProviderGroupRequest:
type: object
description: SCIMProviderGroup Serializer
properties:
scim_id:
type: string
minLength: 1
group:
type: string
format: uuid
provider:
type: integer
required:
- group
- provider
- scim_id
SCIMProviderRequest:
type: object
description: SCIMProvider Serializer
@ -45942,6 +46434,45 @@ components:
- name
- token
- url
SCIMProviderUser:
type: object
description: SCIMProviderUser Serializer
properties:
id:
type: string
format: uuid
readOnly: true
scim_id:
type: string
user:
type: integer
user_obj:
allOf:
- $ref: '#/components/schemas/GroupMember'
readOnly: true
provider:
type: integer
required:
- id
- provider
- scim_id
- user
- user_obj
SCIMProviderUserRequest:
type: object
description: SCIMProviderUser Serializer
properties:
scim_id:
type: string
minLength: 1
user:
type: integer
provider:
type: integer
required:
- provider
- scim_id
- user
SCIMSource:
type: object
description: SCIMSource Serializer

View File

@ -126,6 +126,7 @@ export class PolicyTestForm extends Form<PropertyMappingTestRequest> {
renderForm(): TemplateResult {
return html`<ak-form-element-horizontal label=${msg("User")} name="user">
<ak-search-select
blankable
.fetchObjects=${async (query?: string): Promise<User[]> => {
const args: CoreUsersListRequest = {
ordering: "username",
@ -153,6 +154,7 @@ export class PolicyTestForm extends Form<PropertyMappingTestRequest> {
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Group")} name="group">
<ak-search-select
blankable
.fetchObjects=${async (query?: string): Promise<Group[]> => {
const args: CoreGroupsListRequest = {
ordering: "name",

View File

@ -1,5 +1,6 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { uiConfig } from "@goauthentik/common/ui/config";
import "@goauthentik/elements/forms/DeleteBulkForm";
import { PaginatedResponse, Table, TableColumn } from "@goauthentik/elements/table/Table";
import { msg } from "@lit/localize";
@ -19,6 +20,26 @@ export class GoogleWorkspaceProviderGroupList extends Table<GoogleWorkspaceProvi
return true;
}
checkbox = true;
clearOnRefresh = true;
renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
objectLabel=${msg("Google Workspace Group(s)")}
.objects=${this.selectedElements}
.delete=${(item: GoogleWorkspaceProviderGroup) => {
return new ProvidersApi(DEFAULT_CONFIG).providersGoogleWorkspaceGroupsDestroy({
id: item.id,
});
}}
>
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
${msg("Delete")}
</button>
</ak-forms-delete-bulk>`;
}
async apiEndpoint(page: number): Promise<PaginatedResponse<GoogleWorkspaceProviderGroup>> {
return new ProvidersApi(DEFAULT_CONFIG).providersGoogleWorkspaceGroupsList({
page: page,

View File

@ -1,5 +1,6 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { uiConfig } from "@goauthentik/common/ui/config";
import "@goauthentik/elements/forms/DeleteBulkForm";
import { PaginatedResponse, Table, TableColumn } from "@goauthentik/elements/table/Table";
import { msg } from "@lit/localize";
@ -19,6 +20,26 @@ export class GoogleWorkspaceProviderUserList extends Table<GoogleWorkspaceProvid
expandable = true;
checkbox = true;
clearOnRefresh = true;
renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
objectLabel=${msg("Google Workspace User(s)")}
.objects=${this.selectedElements}
.delete=${(item: GoogleWorkspaceProviderUser) => {
return new ProvidersApi(DEFAULT_CONFIG).providersGoogleWorkspaceUsersDestroy({
id: item.id,
});
}}
>
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
${msg("Delete")}
</button>
</ak-forms-delete-bulk>`;
}
async apiEndpoint(page: number): Promise<PaginatedResponse<GoogleWorkspaceProviderUser>> {
return new ProvidersApi(DEFAULT_CONFIG).providersGoogleWorkspaceUsersList({
page: page,

View File

@ -1,5 +1,6 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { uiConfig } from "@goauthentik/common/ui/config";
import "@goauthentik/elements/forms/DeleteBulkForm";
import { PaginatedResponse, Table, TableColumn } from "@goauthentik/elements/table/Table";
import { msg } from "@lit/localize";
@ -19,6 +20,23 @@ export class MicrosoftEntraProviderGroupList extends Table<MicrosoftEntraProvide
return true;
}
renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
objectLabel=${msg("Microsoft Entra Group(s)")}
.objects=${this.selectedElements}
.delete=${(item: MicrosoftEntraProviderGroup) => {
return new ProvidersApi(DEFAULT_CONFIG).providersMicrosoftEntraGroupsDestroy({
id: item.id,
});
}}
>
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
${msg("Delete")}
</button>
</ak-forms-delete-bulk>`;
}
async apiEndpoint(page: number): Promise<PaginatedResponse<MicrosoftEntraProviderGroup>> {
return new ProvidersApi(DEFAULT_CONFIG).providersMicrosoftEntraGroupsList({
page: page,

View File

@ -1,5 +1,6 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { uiConfig } from "@goauthentik/common/ui/config";
import "@goauthentik/elements/forms/DeleteBulkForm";
import { PaginatedResponse, Table, TableColumn } from "@goauthentik/elements/table/Table";
import { msg } from "@lit/localize";
@ -19,6 +20,26 @@ export class MicrosoftEntraProviderUserList extends Table<MicrosoftEntraProvider
return true;
}
checkbox = true;
clearOnRefresh = true;
renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
objectLabel=${msg("Microsoft Entra User(s)")}
.objects=${this.selectedElements}
.delete=${(item: MicrosoftEntraProviderUser) => {
return new ProvidersApi(DEFAULT_CONFIG).providersMicrosoftEntraUsersDestroy({
id: item.id,
});
}}
>
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
${msg("Delete")}
</button>
</ak-forms-delete-bulk>`;
}
async apiEndpoint(page: number): Promise<PaginatedResponse<MicrosoftEntraProviderUser>> {
return new ProvidersApi(DEFAULT_CONFIG).providersMicrosoftEntraUsersList({
page: page,

View File

@ -0,0 +1,63 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { uiConfig } from "@goauthentik/common/ui/config";
import "@goauthentik/elements/forms/DeleteBulkForm";
import { PaginatedResponse, Table, TableColumn } from "@goauthentik/elements/table/Table";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ProvidersApi, SCIMProviderGroup } from "@goauthentik/api";
@customElement("ak-provider-scim-groups-list")
export class SCIMProviderGroupList extends Table<SCIMProviderGroup> {
@property({ type: Number })
providerId?: number;
searchEnabled(): boolean {
return true;
}
checkbox = true;
clearOnRefresh = true;
renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
objectLabel=${msg("SCIM Group(s)")}
.objects=${this.selectedElements}
.delete=${(item: SCIMProviderGroup) => {
return new ProvidersApi(DEFAULT_CONFIG).providersScimGroupsDestroy({
id: item.id,
});
}}
>
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
${msg("Delete")}
</button>
</ak-forms-delete-bulk>`;
}
async apiEndpoint(page: number): Promise<PaginatedResponse<SCIMProviderGroup>> {
return new ProvidersApi(DEFAULT_CONFIG).providersScimGroupsList({
page: page,
pageSize: (await uiConfig()).pagination.perPage,
ordering: this.order,
search: this.search || "",
providerId: this.providerId,
});
}
columns(): TableColumn[] {
return [new TableColumn(msg("Name")), new TableColumn(msg("ID"))];
}
row(item: SCIMProviderGroup): TemplateResult[] {
return [
html`<a href="#/identity/groups/${item.groupObj.pk}">
<div>${item.groupObj.name}</div>
</a>`,
html`${item.id}`,
];
}
}

View File

@ -0,0 +1,64 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { uiConfig } from "@goauthentik/common/ui/config";
import "@goauthentik/elements/forms/DeleteBulkForm";
import { PaginatedResponse, Table, TableColumn } from "@goauthentik/elements/table/Table";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ProvidersApi, SCIMProviderUser } from "@goauthentik/api";
@customElement("ak-provider-google-workspace-users-list")
export class SCIMProviderUserList extends Table<SCIMProviderUser> {
@property({ type: Number })
providerId?: number;
searchEnabled(): boolean {
return true;
}
checkbox = true;
clearOnRefresh = true;
renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
objectLabel=${msg("SCIM User(s)")}
.objects=${this.selectedElements}
.delete=${(item: SCIMProviderUser) => {
return new ProvidersApi(DEFAULT_CONFIG).providersScimUsersDestroy({
id: item.id,
});
}}
>
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
${msg("Delete")}
</button>
</ak-forms-delete-bulk>`;
}
async apiEndpoint(page: number): Promise<PaginatedResponse<SCIMProviderUser>> {
return new ProvidersApi(DEFAULT_CONFIG).providersScimUsersList({
page: page,
pageSize: (await uiConfig()).pagination.perPage,
ordering: this.order,
search: this.search || "",
providerId: this.providerId,
});
}
columns(): TableColumn[] {
return [new TableColumn(msg("Username")), new TableColumn(msg("ID"))];
}
row(item: SCIMProviderUser): TemplateResult[] {
return [
html`<a href="#/identity/users/${item.userObj.pk}">
<div>${item.userObj.username}</div>
<small>${item.userObj.name}</small>
</a>`,
html`${item.id}`,
];
}
}

View File

@ -1,4 +1,6 @@
import "@goauthentik/admin/providers/scim/SCIMProviderForm";
import "@goauthentik/admin/providers/scim/SCIMProviderGroupList";
import "@goauthentik/admin/providers/scim/SCIMProviderUserList";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants";
import "@goauthentik/components/events/ObjectChangelog";
@ -102,6 +104,28 @@ export class SCIMProviderViewPage extends AKElement {
</div>
</div>
</section>
<section
slot="page-users"
data-tab-title="${msg("Provisioned Users")}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<div class="pf-l-grid pf-m-gutter">
<ak-provider-scim-users-list
providerId=${this.provider.pk}
></ak-provider-scim-users-list>
</div>
</section>
<section
slot="page-groups"
data-tab-title="${msg("Provisioned Groups")}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<div class="pf-l-grid pf-m-gutter">
<ak-provider-scim-groups-list
providerId=${this.provider.pk}
></ak-provider-scim-groups-list>
</div>
</section>
<ak-rbac-object-permission-page
slot="page-permissions"
data-tab-title="${msg("Permissions")}"