sources/scim: add property mappings (#10650)
* sources/scim: add property mappings Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix filterset Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix doc link Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * lint Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> --------- Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
This commit is contained in:

committed by
GitHub

parent
1b285f85c0
commit
3b1c42776b
@ -269,8 +269,6 @@ class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
|
||||
ak_groups = models.ManyToManyField("Group", related_name="users")
|
||||
password_change_date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
attributes = models.JSONField(default=dict, blank=True)
|
||||
|
||||
objects = UserManager()
|
||||
|
||||
class Meta:
|
||||
|
32
authentik/sources/scim/api/property_mappings.py
Normal file
32
authentik/sources/scim/api/property_mappings.py
Normal file
@ -0,0 +1,32 @@
|
||||
"""SCIM source property mappings API"""
|
||||
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.property_mappings import PropertyMappingFilterSet, PropertyMappingSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.sources.scim.models import SCIMSourcePropertyMapping
|
||||
|
||||
|
||||
class SCIMSourcePropertyMappingSerializer(PropertyMappingSerializer):
|
||||
"""SCIMSourcePropertyMapping Serializer"""
|
||||
|
||||
class Meta:
|
||||
model = SCIMSourcePropertyMapping
|
||||
fields = PropertyMappingSerializer.Meta.fields
|
||||
|
||||
|
||||
class SCIMSourcePropertyMappingFilter(PropertyMappingFilterSet):
|
||||
"""Filter for SCIMSourcePropertyMapping"""
|
||||
|
||||
class Meta(PropertyMappingFilterSet.Meta):
|
||||
model = SCIMSourcePropertyMapping
|
||||
|
||||
|
||||
class SCIMSourcePropertyMappingViewSet(UsedByMixin, ModelViewSet):
|
||||
"""SCIMSourcePropertyMapping Viewset"""
|
||||
|
||||
queryset = SCIMSourcePropertyMapping.objects.all()
|
||||
serializer_class = SCIMSourcePropertyMappingSerializer
|
||||
filterset_class = SCIMSourcePropertyMappingFilter
|
||||
search_fields = ["name"]
|
||||
ordering = ["name"]
|
@ -34,11 +34,12 @@ class SCIMSourceSerializer(SourceSerializer):
|
||||
"name",
|
||||
"slug",
|
||||
"enabled",
|
||||
"user_property_mappings",
|
||||
"group_property_mappings",
|
||||
"component",
|
||||
"verbose_name",
|
||||
"verbose_name_plural",
|
||||
"meta_model_name",
|
||||
"user_matching_mode",
|
||||
"managed",
|
||||
"user_path_template",
|
||||
"root_url",
|
||||
|
@ -0,0 +1,36 @@
|
||||
# Generated by Django 5.0.7 on 2024-07-26 13:19
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0037_remove_source_property_mappings"),
|
||||
("authentik_sources_scim", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="SCIMSourcePropertyMapping",
|
||||
fields=[
|
||||
(
|
||||
"propertymapping_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="authentik_core.propertymapping",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "SCIM Source Property Mapping",
|
||||
"verbose_name_plural": "SCIM Source Property Mappings",
|
||||
},
|
||||
bases=("authentik_core.propertymapping",),
|
||||
),
|
||||
]
|
@ -1,13 +1,14 @@
|
||||
"""SCIM Source"""
|
||||
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from django.db import models
|
||||
from django.templatetags.static import static
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.serializers import BaseSerializer
|
||||
from rest_framework.serializers import BaseSerializer, Serializer
|
||||
|
||||
from authentik.core.models import Group, Source, Token, User
|
||||
from authentik.core.models import Group, PropertyMapping, Source, Token, User
|
||||
from authentik.lib.models import SerializerModel
|
||||
|
||||
|
||||
@ -38,6 +39,41 @@ class SCIMSource(Source):
|
||||
|
||||
return SCIMSourceSerializer
|
||||
|
||||
@property
|
||||
def property_mapping_type(self) -> type[PropertyMapping]:
|
||||
return SCIMSourcePropertyMapping
|
||||
|
||||
def get_base_user_properties(self, data: dict[str, Any]) -> dict[str, Any | dict[str, Any]]:
|
||||
properties = {}
|
||||
|
||||
def get_email(data: list[dict]) -> str:
|
||||
"""Wrapper to get primary email or first email"""
|
||||
for email in data:
|
||||
if email.get("primary", False):
|
||||
return email.get("value")
|
||||
if len(data) < 1:
|
||||
return ""
|
||||
return data[0].get("value")
|
||||
|
||||
if "userName" in data:
|
||||
properties["username"] = data.get("userName")
|
||||
if "name" in data:
|
||||
properties["name"] = data.get("name", {}).get("formatted", data.get("displayName"))
|
||||
if "emails" in data:
|
||||
properties["email"] = get_email(data.get("emails"))
|
||||
if "active" in data:
|
||||
properties["is_active"] = data.get("active")
|
||||
|
||||
return properties
|
||||
|
||||
def get_base_group_properties(self, data: dict[str, Any]) -> dict[str, Any | dict[str, Any]]:
|
||||
properties = {}
|
||||
|
||||
if "displayName" in data:
|
||||
properties["name"] = data.get("displayName")
|
||||
|
||||
return properties
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"SCIM Source {self.name}"
|
||||
|
||||
@ -47,6 +83,26 @@ class SCIMSource(Source):
|
||||
verbose_name_plural = _("SCIM Sources")
|
||||
|
||||
|
||||
class SCIMSourcePropertyMapping(PropertyMapping):
|
||||
"""Map SCIM properties to User or Group object attributes"""
|
||||
|
||||
@property
|
||||
def component(self) -> str:
|
||||
return "ak-property-mapping-scim-source-form"
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
from authentik.sources.scim.api.property_mappings import (
|
||||
SCIMSourcePropertyMappingSerializer,
|
||||
)
|
||||
|
||||
return SCIMSourcePropertyMappingSerializer
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("SCIM Source Property Mapping")
|
||||
verbose_name_plural = _("SCIM Source Property Mappings")
|
||||
|
||||
|
||||
class SCIMSourceUser(SerializerModel):
|
||||
"""Mapping of a user and source to a SCIM user ID"""
|
||||
|
||||
|
@ -10,7 +10,7 @@ from authentik.core.tests.utils import create_test_user
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.providers.scim.clients.schema import User as SCIMUserSchema
|
||||
from authentik.sources.scim.models import SCIMSource, SCIMSourceUser
|
||||
from authentik.sources.scim.models import SCIMSource, SCIMSourcePropertyMapping, SCIMSourceUser
|
||||
from authentik.sources.scim.views.v2.base import SCIM_CONTENT_TYPE
|
||||
|
||||
|
||||
@ -87,3 +87,44 @@ class TestSCIMUsers(APITestCase):
|
||||
action=EventAction.MODEL_CREATED, user__username=self.source.token.user.username
|
||||
).exists()
|
||||
)
|
||||
|
||||
def test_user_property_mappings(self):
|
||||
"""Test user property_mappings"""
|
||||
self.source.user_property_mappings.set(
|
||||
[
|
||||
SCIMSourcePropertyMapping.objects.create(
|
||||
name=generate_id(),
|
||||
expression='return {"attributes": {"phone": data.get("phoneNumber")}}',
|
||||
)
|
||||
]
|
||||
)
|
||||
user = create_test_user()
|
||||
ext_id = generate_id()
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
"authentik_sources_scim:v2-users",
|
||||
kwargs={
|
||||
"source_slug": self.source.slug,
|
||||
},
|
||||
),
|
||||
data=dumps(
|
||||
{
|
||||
"userName": generate_id(),
|
||||
"externalId": ext_id,
|
||||
"emails": [
|
||||
{
|
||||
"primary": True,
|
||||
"value": user.email,
|
||||
}
|
||||
],
|
||||
"phoneNumber": "0123456789",
|
||||
}
|
||||
),
|
||||
content_type=SCIM_CONTENT_TYPE,
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(
|
||||
SCIMSourceUser.objects.get(source=self.source, id=ext_id).user.attributes["phone"],
|
||||
"0123456789",
|
||||
)
|
||||
|
@ -3,6 +3,7 @@
|
||||
from django.urls import path
|
||||
|
||||
from authentik.sources.scim.api.groups import SCIMSourceGroupViewSet
|
||||
from authentik.sources.scim.api.property_mappings import SCIMSourcePropertyMappingViewSet
|
||||
from authentik.sources.scim.api.sources import SCIMSourceViewSet
|
||||
from authentik.sources.scim.api.users import SCIMSourceUserViewSet
|
||||
from authentik.sources.scim.views.v2 import (
|
||||
@ -68,6 +69,7 @@ urlpatterns = [
|
||||
]
|
||||
|
||||
api_urlpatterns = [
|
||||
("propertymappings/source/scim", SCIMSourcePropertyMappingViewSet),
|
||||
("sources/scim", SCIMSourceViewSet),
|
||||
("sources/scim_users", SCIMSourceUserViewSet),
|
||||
("sources/scim_groups", SCIMSourceGroupViewSet),
|
||||
|
@ -5,7 +5,7 @@ from urllib.parse import urlparse
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.paginator import Page, Paginator
|
||||
from django.db.models import Model, Q, QuerySet
|
||||
from django.db.models import Q, QuerySet
|
||||
from django.http import HttpRequest
|
||||
from django.urls import resolve
|
||||
from rest_framework.parsers import JSONParser
|
||||
@ -19,6 +19,8 @@ from structlog import BoundLogger
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import Group, User
|
||||
from authentik.core.sources.mapper import SourceMapper
|
||||
from authentik.lib.sync.mapper import PropertyMappingManager
|
||||
from authentik.sources.scim.models import SCIMSource
|
||||
from authentik.sources.scim.views.v2.auth import SCIMTokenAuth
|
||||
|
||||
@ -47,11 +49,9 @@ class SCIMView(APIView):
|
||||
parser_classes = [SCIMParser]
|
||||
renderer_classes = [SCIMRenderer]
|
||||
|
||||
model: type[Model]
|
||||
|
||||
def setup(self, request: HttpRequest, *args: Any, **kwargs: Any) -> None:
|
||||
self.logger = get_logger().bind()
|
||||
return super().setup(request, *args, **kwargs)
|
||||
super().setup(request, *args, **kwargs)
|
||||
|
||||
def get_authenticators(self):
|
||||
return [SCIMTokenAuth(self)]
|
||||
@ -113,6 +113,31 @@ class SCIMView(APIView):
|
||||
return page
|
||||
|
||||
|
||||
class SCIMObjectView(SCIMView):
|
||||
"""Base SCIM View for object management"""
|
||||
|
||||
mapper: SourceMapper
|
||||
manager: PropertyMappingManager
|
||||
|
||||
model: type[User | Group]
|
||||
|
||||
def initial(self, request: Request, *args, **kwargs) -> None:
|
||||
super().initial(request, *args, **kwargs)
|
||||
# This needs to happen after authentication has happened, because we don't have
|
||||
# a source attribute before
|
||||
self.mapper = SourceMapper(self.source)
|
||||
self.manager = self.mapper.get_manager(self.model, ["data"])
|
||||
|
||||
def build_object_properties(self, data: dict[str, Any]) -> dict[str, Any | dict[str, Any]]:
|
||||
return self.mapper.build_object_properties(
|
||||
object_type=self.model,
|
||||
manager=self.manager,
|
||||
user=None,
|
||||
request=self.request,
|
||||
data=data,
|
||||
)
|
||||
|
||||
|
||||
class SCIMRootView(SCIMView):
|
||||
"""Root SCIM View"""
|
||||
|
||||
|
@ -16,10 +16,10 @@ from authentik.core.models import Group, User
|
||||
from authentik.providers.scim.clients.schema import SCIM_USER_SCHEMA
|
||||
from authentik.providers.scim.clients.schema import Group as SCIMGroupModel
|
||||
from authentik.sources.scim.models import SCIMSourceGroup
|
||||
from authentik.sources.scim.views.v2.base import SCIMView
|
||||
from authentik.sources.scim.views.v2.base import SCIMObjectView
|
||||
|
||||
|
||||
class GroupsView(SCIMView):
|
||||
class GroupsView(SCIMObjectView):
|
||||
"""SCIM Group view"""
|
||||
|
||||
model = Group
|
||||
@ -77,14 +77,17 @@ class GroupsView(SCIMView):
|
||||
@atomic
|
||||
def update_group(self, connection: SCIMSourceGroup | None, data: QueryDict):
|
||||
"""Partial update a group"""
|
||||
group = connection.group if connection else Group()
|
||||
if _group := Group.objects.filter(name=data.get("displayName")).first():
|
||||
group = _group
|
||||
if "displayName" in data:
|
||||
group.name = data.get("displayName")
|
||||
if group.name == "":
|
||||
properties = self.build_object_properties(data)
|
||||
|
||||
if not properties.get("name"):
|
||||
raise ValidationError("Invalid group")
|
||||
group.save()
|
||||
|
||||
group = connection.group if connection else Group()
|
||||
if _group := Group.objects.filter(name=properties.get("name")).first():
|
||||
group = _group
|
||||
|
||||
group.update_attributes(properties)
|
||||
|
||||
if "members" in data:
|
||||
query = Q()
|
||||
for _member in data.get("members", []):
|
||||
|
@ -14,23 +14,14 @@ from authentik.core.models import User
|
||||
from authentik.providers.scim.clients.schema import SCIM_USER_SCHEMA
|
||||
from authentik.providers.scim.clients.schema import User as SCIMUserModel
|
||||
from authentik.sources.scim.models import SCIMSourceUser
|
||||
from authentik.sources.scim.views.v2.base import SCIMView
|
||||
from authentik.sources.scim.views.v2.base import SCIMObjectView
|
||||
|
||||
|
||||
class UsersView(SCIMView):
|
||||
class UsersView(SCIMObjectView):
|
||||
"""SCIM User view"""
|
||||
|
||||
model = User
|
||||
|
||||
def get_email(self, data: list[dict]) -> str:
|
||||
"""Wrapper to get primary email or first email"""
|
||||
for email in data:
|
||||
if email.get("primary", False):
|
||||
return email.get("value")
|
||||
if len(data) < 1:
|
||||
return ""
|
||||
return data[0].get("value")
|
||||
|
||||
def user_to_scim(self, scim_user: SCIMSourceUser) -> dict:
|
||||
"""Convert User to SCIM data"""
|
||||
payload = SCIMUserModel(
|
||||
@ -97,21 +88,16 @@ class UsersView(SCIMView):
|
||||
@atomic
|
||||
def update_user(self, connection: SCIMSourceUser | None, data: QueryDict):
|
||||
"""Partial update a user"""
|
||||
user = connection.user if connection else User()
|
||||
if _user := User.objects.filter(username=data.get("userName")).first():
|
||||
user = _user
|
||||
user.path = self.source.get_user_path()
|
||||
if "userName" in data:
|
||||
user.username = data.get("userName")
|
||||
if "name" in data:
|
||||
user.name = data.get("name", {}).get("formatted", data.get("displayName"))
|
||||
if "emails" in data:
|
||||
user.email = self.get_email(data.get("emails"))
|
||||
if "active" in data:
|
||||
user.is_active = data.get("active")
|
||||
if user.username == "":
|
||||
properties = self.build_object_properties(data)
|
||||
|
||||
if not properties.get("username"):
|
||||
raise ValidationError("Invalid user")
|
||||
user.save()
|
||||
|
||||
user = connection.user if connection else User()
|
||||
if _user := User.objects.filter(username=properties.get("username")).first():
|
||||
user = _user
|
||||
user.update_attributes(properties)
|
||||
|
||||
if not connection:
|
||||
connection, _ = SCIMSourceUser.objects.get_or_create(
|
||||
source=self.source,
|
||||
|
@ -1299,6 +1299,43 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"model",
|
||||
"identifiers"
|
||||
],
|
||||
"properties": {
|
||||
"model": {
|
||||
"const": "authentik_sources_scim.scimsourcepropertymapping"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"absent",
|
||||
"present",
|
||||
"created",
|
||||
"must_created"
|
||||
],
|
||||
"default": "present"
|
||||
},
|
||||
"conditions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"attrs": {
|
||||
"$ref": "#/$defs/model_authentik_sources_scim.scimsourcepropertymapping"
|
||||
},
|
||||
"identifiers": {
|
||||
"$ref": "#/$defs/model_authentik_sources_scim.scimsourcepropertymapping"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
@ -3572,6 +3609,7 @@
|
||||
"authentik_sources_saml.samlsource",
|
||||
"authentik_sources_saml.usersamlsourceconnection",
|
||||
"authentik_sources_scim.scimsource",
|
||||
"authentik_sources_scim.scimsourcepropertymapping",
|
||||
"authentik_stages_authenticator_duo.authenticatorduostage",
|
||||
"authentik_stages_authenticator_duo.duodevice",
|
||||
"authentik_stages_authenticator_sms.authenticatorsmsstage",
|
||||
@ -5247,17 +5285,21 @@
|
||||
"type": "boolean",
|
||||
"title": "Enabled"
|
||||
},
|
||||
"user_matching_mode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"identifier",
|
||||
"email_link",
|
||||
"email_deny",
|
||||
"username_link",
|
||||
"username_deny"
|
||||
],
|
||||
"title": "User matching mode",
|
||||
"description": "How the source determines if an existing user should be authenticated or a new user enrolled."
|
||||
"user_property_mappings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"title": "User property mappings"
|
||||
},
|
||||
"group_property_mappings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"title": "Group property mappings"
|
||||
},
|
||||
"user_path_template": {
|
||||
"type": "string",
|
||||
@ -5272,6 +5314,31 @@
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"model_authentik_sources_scim.scimsourcepropertymapping": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"managed": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"minLength": 1,
|
||||
"title": "Managed by authentik",
|
||||
"description": "Objects that are managed by authentik. These objects are created and updated automatically. This flag only indicates that an object can be overwritten by migrations. You can still modify the objects via the API, but expect changes to be overwritten in a later update."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Name"
|
||||
},
|
||||
"expression": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Expression"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"model_authentik_stages_authenticator_duo.authenticatorduostage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
432
schema.yml
432
schema.yml
@ -15948,6 +15948,292 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/propertymappings/source/scim/:
|
||||
get:
|
||||
operationId: propertymappings_source_scim_list
|
||||
description: SCIMSourcePropertyMapping Viewset
|
||||
parameters:
|
||||
- in: query
|
||||
name: expression
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: managed
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
explode: true
|
||||
style: form
|
||||
- in: query
|
||||
name: 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: pm_uuid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- name: search
|
||||
required: false
|
||||
in: query
|
||||
description: A search term.
|
||||
schema:
|
||||
type: string
|
||||
tags:
|
||||
- propertymappings
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PaginatedSCIMSourcePropertyMappingList'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
post:
|
||||
operationId: propertymappings_source_scim_create
|
||||
description: SCIMSourcePropertyMapping Viewset
|
||||
tags:
|
||||
- propertymappings
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SCIMSourcePropertyMappingRequest'
|
||||
required: true
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'201':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SCIMSourcePropertyMapping'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/propertymappings/source/scim/{pm_uuid}/:
|
||||
get:
|
||||
operationId: propertymappings_source_scim_retrieve
|
||||
description: SCIMSourcePropertyMapping Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: pm_uuid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this SCIM Source Property Mapping.
|
||||
required: true
|
||||
tags:
|
||||
- propertymappings
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SCIMSourcePropertyMapping'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
put:
|
||||
operationId: propertymappings_source_scim_update
|
||||
description: SCIMSourcePropertyMapping Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: pm_uuid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this SCIM Source Property Mapping.
|
||||
required: true
|
||||
tags:
|
||||
- propertymappings
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SCIMSourcePropertyMappingRequest'
|
||||
required: true
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SCIMSourcePropertyMapping'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
patch:
|
||||
operationId: propertymappings_source_scim_partial_update
|
||||
description: SCIMSourcePropertyMapping Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: pm_uuid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this SCIM Source Property Mapping.
|
||||
required: true
|
||||
tags:
|
||||
- propertymappings
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PatchedSCIMSourcePropertyMappingRequest'
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SCIMSourcePropertyMapping'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
delete:
|
||||
operationId: propertymappings_source_scim_destroy
|
||||
description: SCIMSourcePropertyMapping Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: pm_uuid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this SCIM Source Property Mapping.
|
||||
required: true
|
||||
tags:
|
||||
- propertymappings
|
||||
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: ''
|
||||
/propertymappings/source/scim/{pm_uuid}/used_by/:
|
||||
get:
|
||||
operationId: propertymappings_source_scim_used_by_list
|
||||
description: Get a list of all objects that use this object
|
||||
parameters:
|
||||
- in: path
|
||||
name: pm_uuid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this SCIM Source Property Mapping.
|
||||
required: true
|
||||
tags:
|
||||
- propertymappings
|
||||
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/all/:
|
||||
get:
|
||||
operationId: providers_all_list
|
||||
@ -21023,6 +21309,7 @@ paths:
|
||||
- authentik_sources_saml.samlsource
|
||||
- authentik_sources_saml.usersamlsourceconnection
|
||||
- authentik_sources_scim.scimsource
|
||||
- authentik_sources_scim.scimsourcepropertymapping
|
||||
- authentik_stages_authenticator_duo.authenticatorduostage
|
||||
- authentik_stages_authenticator_duo.duodevice
|
||||
- authentik_stages_authenticator_sms.authenticatorsmsstage
|
||||
@ -21243,6 +21530,7 @@ paths:
|
||||
- authentik_sources_saml.samlsource
|
||||
- authentik_sources_saml.usersamlsourceconnection
|
||||
- authentik_sources_scim.scimsource
|
||||
- authentik_sources_scim.scimsourcepropertymapping
|
||||
- authentik_stages_authenticator_duo.authenticatorduostage
|
||||
- authentik_stages_authenticator_duo.duodevice
|
||||
- authentik_stages_authenticator_sms.authenticatorsmsstage
|
||||
@ -38829,6 +39117,7 @@ components:
|
||||
- authentik_sources_saml.samlsource
|
||||
- authentik_sources_saml.usersamlsourceconnection
|
||||
- authentik_sources_scim.scimsource
|
||||
- authentik_sources_scim.scimsourcepropertymapping
|
||||
- authentik_stages_authenticator_duo.authenticatorduostage
|
||||
- authentik_stages_authenticator_duo.duodevice
|
||||
- authentik_stages_authenticator_sms.authenticatorsmsstage
|
||||
@ -40874,6 +41163,18 @@ components:
|
||||
required:
|
||||
- pagination
|
||||
- results
|
||||
PaginatedSCIMSourcePropertyMappingList:
|
||||
type: object
|
||||
properties:
|
||||
pagination:
|
||||
$ref: '#/components/schemas/Pagination'
|
||||
results:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/SCIMSourcePropertyMapping'
|
||||
required:
|
||||
- pagination
|
||||
- results
|
||||
PaginatedSCIMSourceUserList:
|
||||
type: object
|
||||
properties:
|
||||
@ -43781,6 +44082,25 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
attributes: {}
|
||||
PatchedSCIMSourcePropertyMappingRequest:
|
||||
type: object
|
||||
description: SCIMSourcePropertyMapping Serializer
|
||||
properties:
|
||||
managed:
|
||||
type: string
|
||||
nullable: true
|
||||
minLength: 1
|
||||
title: Managed by authentik
|
||||
description: Objects that are managed by authentik. These objects are created
|
||||
and updated automatically. This flag only indicates that an object can
|
||||
be overwritten by migrations. You can still modify the objects via the
|
||||
API, but expect changes to be overwritten in a later update.
|
||||
name:
|
||||
type: string
|
||||
minLength: 1
|
||||
expression:
|
||||
type: string
|
||||
minLength: 1
|
||||
PatchedSCIMSourceRequest:
|
||||
type: object
|
||||
description: SCIMSource Serializer
|
||||
@ -43797,11 +44117,16 @@ components:
|
||||
pattern: ^[-a-zA-Z0-9_]+$
|
||||
enabled:
|
||||
type: boolean
|
||||
user_matching_mode:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/UserMatchingModeEnum'
|
||||
description: How the source determines if an existing user should be authenticated
|
||||
or a new user enrolled.
|
||||
user_property_mappings:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
group_property_mappings:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
user_path_template:
|
||||
type: string
|
||||
minLength: 1
|
||||
@ -46886,6 +47211,16 @@ components:
|
||||
pattern: ^[-a-zA-Z0-9_]+$
|
||||
enabled:
|
||||
type: boolean
|
||||
user_property_mappings:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
group_property_mappings:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
component:
|
||||
type: string
|
||||
description: Get object component so that we know how to edit the object
|
||||
@ -46902,11 +47237,6 @@ components:
|
||||
type: string
|
||||
description: Return internal model name
|
||||
readOnly: true
|
||||
user_matching_mode:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/UserMatchingModeEnum'
|
||||
description: How the source determines if an existing user should be authenticated
|
||||
or a new user enrolled.
|
||||
managed:
|
||||
type: string
|
||||
nullable: true
|
||||
@ -46977,6 +47307,73 @@ components:
|
||||
- group
|
||||
- id
|
||||
- source
|
||||
SCIMSourcePropertyMapping:
|
||||
type: object
|
||||
description: SCIMSourcePropertyMapping Serializer
|
||||
properties:
|
||||
pk:
|
||||
type: string
|
||||
format: uuid
|
||||
readOnly: true
|
||||
title: Pm uuid
|
||||
managed:
|
||||
type: string
|
||||
nullable: true
|
||||
title: Managed by authentik
|
||||
description: Objects that are managed by authentik. These objects are created
|
||||
and updated automatically. This flag only indicates that an object can
|
||||
be overwritten by migrations. You can still modify the objects via the
|
||||
API, but expect changes to be overwritten in a later update.
|
||||
name:
|
||||
type: string
|
||||
expression:
|
||||
type: string
|
||||
component:
|
||||
type: string
|
||||
description: Get object's component so that we know how to edit the object
|
||||
readOnly: true
|
||||
verbose_name:
|
||||
type: string
|
||||
description: Return object's verbose_name
|
||||
readOnly: true
|
||||
verbose_name_plural:
|
||||
type: string
|
||||
description: Return object's plural verbose_name
|
||||
readOnly: true
|
||||
meta_model_name:
|
||||
type: string
|
||||
description: Return internal model name
|
||||
readOnly: true
|
||||
required:
|
||||
- component
|
||||
- expression
|
||||
- meta_model_name
|
||||
- name
|
||||
- pk
|
||||
- verbose_name
|
||||
- verbose_name_plural
|
||||
SCIMSourcePropertyMappingRequest:
|
||||
type: object
|
||||
description: SCIMSourcePropertyMapping Serializer
|
||||
properties:
|
||||
managed:
|
||||
type: string
|
||||
nullable: true
|
||||
minLength: 1
|
||||
title: Managed by authentik
|
||||
description: Objects that are managed by authentik. These objects are created
|
||||
and updated automatically. This flag only indicates that an object can
|
||||
be overwritten by migrations. You can still modify the objects via the
|
||||
API, but expect changes to be overwritten in a later update.
|
||||
name:
|
||||
type: string
|
||||
minLength: 1
|
||||
expression:
|
||||
type: string
|
||||
minLength: 1
|
||||
required:
|
||||
- expression
|
||||
- name
|
||||
SCIMSourceRequest:
|
||||
type: object
|
||||
description: SCIMSource Serializer
|
||||
@ -46993,11 +47390,16 @@ components:
|
||||
pattern: ^[-a-zA-Z0-9_]+$
|
||||
enabled:
|
||||
type: boolean
|
||||
user_matching_mode:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/UserMatchingModeEnum'
|
||||
description: How the source determines if an existing user should be authenticated
|
||||
or a new user enrolled.
|
||||
user_property_mappings:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
group_property_mappings:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
user_path_template:
|
||||
type: string
|
||||
minLength: 1
|
||||
|
@ -6,6 +6,7 @@ import "@goauthentik/admin/property-mappings/PropertyMappingRACForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingRadiusForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingSAMLForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingSCIMForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingSCIMSourceForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingScopeForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingTestForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingWizard";
|
||||
|
@ -0,0 +1,75 @@
|
||||
import { BasePropertyMappingForm } from "@goauthentik/admin/property-mappings/BasePropertyMappingForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { docLink } from "@goauthentik/common/global";
|
||||
import "@goauthentik/elements/CodeMirror";
|
||||
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import { PropertymappingsApi, SCIMSourcePropertyMapping } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-property-mapping-scim-source-form")
|
||||
export class PropertyMappingSCIMSourceForm extends BasePropertyMappingForm<SCIMSourcePropertyMapping> {
|
||||
loadInstance(pk: string): Promise<SCIMSourcePropertyMapping> {
|
||||
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsSourceScimRetrieve({
|
||||
pmUuid: pk,
|
||||
});
|
||||
}
|
||||
|
||||
async send(data: SCIMSourcePropertyMapping): Promise<SCIMSourcePropertyMapping> {
|
||||
if (this.instance) {
|
||||
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsSourceScimUpdate({
|
||||
pmUuid: this.instance.pk,
|
||||
sCIMSourcePropertyMappingRequest: data,
|
||||
});
|
||||
} else {
|
||||
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsSourceScimCreate({
|
||||
sCIMSourcePropertyMappingRequest: data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html` <ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.name)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Expression")}
|
||||
?required=${true}
|
||||
name="expression"
|
||||
>
|
||||
<ak-codemirror
|
||||
mode=${CodeMirrorMode.Python}
|
||||
value="${ifDefined(this.instance?.expression)}"
|
||||
>
|
||||
</ak-codemirror>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Expression using Python.")}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="${docLink(
|
||||
"/docs/sources/property-mappings/expression?utm_source=authentik",
|
||||
)}"
|
||||
>
|
||||
${msg("See documentation for a list of all variables.")}
|
||||
</a>
|
||||
</p>
|
||||
</ak-form-element-horizontal>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-property-mapping-scim-source-form": PropertyMappingSCIMSourceForm;
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ import "@goauthentik/admin/property-mappings/PropertyMappingLDAPSourceForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingNotification";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingRACForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingSAMLForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingSCIMSourceForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingScopeForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingTestForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
|
@ -2,6 +2,8 @@ import { placeholderHelperText } from "@goauthentik/admin/helperText";
|
||||
import { BaseSourceForm } from "@goauthentik/admin/sources/BaseSourceForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
|
||||
import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
|
||||
@ -10,7 +12,35 @@ import { TemplateResult, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import { SCIMSource, SCIMSourceRequest, SourcesApi } from "@goauthentik/api";
|
||||
import {
|
||||
PropertymappingsApi,
|
||||
SCIMSource,
|
||||
SCIMSourcePropertyMapping,
|
||||
SCIMSourceRequest,
|
||||
SourcesApi,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
async function propertyMappingsProvider(page = 1, search = "") {
|
||||
const propertyMappings = await new PropertymappingsApi(
|
||||
DEFAULT_CONFIG,
|
||||
).propertymappingsSourceScimList({
|
||||
ordering: "managed",
|
||||
pageSize: 20,
|
||||
search: search.trim(),
|
||||
page,
|
||||
});
|
||||
return {
|
||||
pagination: propertyMappings.pagination,
|
||||
options: propertyMappings.results.map((m) => [m.pk, m.name, m.name, m]),
|
||||
};
|
||||
}
|
||||
|
||||
function makePropertyMappingsSelector(instanceMappings?: string[]) {
|
||||
const localMappings = instanceMappings ? new Set(instanceMappings) : undefined;
|
||||
return localMappings
|
||||
? ([pk, _]: DualSelectPair) => localMappings.has(pk)
|
||||
: ([_0, _1, _2, _]: DualSelectPair<SCIMSourcePropertyMapping>) => false;
|
||||
}
|
||||
|
||||
@customElement("ak-source-scim-form")
|
||||
export class SCIMSourceForm extends BaseSourceForm<SCIMSource> {
|
||||
@ -65,6 +95,43 @@ export class SCIMSourceForm extends BaseSourceForm<SCIMSource> {
|
||||
<label class="pf-c-check__label"> ${msg("Enabled")} </label>
|
||||
</div>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-group ?expanded=${true}>
|
||||
<span slot="header"> ${msg("SCIM Attribute mapping")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("User Property Mappings")}
|
||||
name="userPropertyMappings"
|
||||
>
|
||||
<ak-dual-select-dynamic-selected
|
||||
.provider=${propertyMappingsProvider}
|
||||
.selector=${makePropertyMappingsSelector(
|
||||
this.instance?.userPropertyMappings,
|
||||
)}
|
||||
available-label="${msg("Available User Property Mappings")}"
|
||||
selected-label="${msg("Selected User Property Mappings")}"
|
||||
></ak-dual-select-dynamic-selected>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Property mappings for user creation.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Group Property Mappings")}
|
||||
name="groupPropertyMappings"
|
||||
>
|
||||
<ak-dual-select-dynamic-selected
|
||||
.provider=${propertyMappingsProvider}
|
||||
.selector=${makePropertyMappingsSelector(
|
||||
this.instance?.groupPropertyMappings,
|
||||
)}
|
||||
available-label="${msg("Available Group Property Mappings")}"
|
||||
selected-label="${msg("Selected Group Property Mappings")}"
|
||||
></ak-dual-select-dynamic-selected>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Property mappings for group creation.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
|
Reference in New Issue
Block a user