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:
Marc 'risson' Schmitt
2024-07-29 22:32:51 +02:00
committed by GitHub
parent 1b285f85c0
commit 3b1c42776b
16 changed files with 864 additions and 71 deletions

View File

@ -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:

View 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"]

View File

@ -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",

View File

@ -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",),
),
]

View File

@ -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"""

View File

@ -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",
)

View File

@ -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),

View File

@ -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"""

View File

@ -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", []):

View File

@ -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,

View File

@ -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": {

View File

@ -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

View File

@ -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";

View File

@ -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;
}
}

View File

@ -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";

View File

@ -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">