Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2021-11-15 11:49:08 +01:00 committed by Jens Langhammer
parent 51d3511f8b
commit 2cab4b7cda
No known key found for this signature in database
23 changed files with 2750 additions and 2 deletions

View File

@ -17,7 +17,7 @@
"kubernetes", "kubernetes",
"sso", "sso",
"slo", "slo",
"scim", "scim"
], ],
"python.linting.pylintEnabled": true, "python.linting.pylintEnabled": true,
"todo-tree.tree.showCountsInTree": true, "todo-tree.tree.showCountsInTree": true,

View File

@ -13,6 +13,8 @@ from rest_framework.settings import api_settings
from authentik.api.pagination import PAGINATION_COMPONENT_NAME, PAGINATION_SCHEMA from authentik.api.pagination import PAGINATION_COMPONENT_NAME, PAGINATION_SCHEMA
from authentik.api.apps import AuthentikAPIConfig
def build_standard_type(obj, **kwargs): def build_standard_type(obj, **kwargs):
"""Build a basic type with optional add owns.""" """Build a basic type with optional add owns."""
@ -100,3 +102,12 @@ def postprocess_schema_responses(result, generator: SchemaGenerator, **kwargs):
comp = result["components"]["schemas"][component] comp = result["components"]["schemas"][component]
comp["additionalProperties"] = {} comp["additionalProperties"] = {}
return result return result
def preprocess_schema_exclude_non_api(endpoints, **kwargs):
"""Filter out all API Views which are not mounted under /api"""
return [
(path, path_regex, method, callback)
for path, path_regex, method, callback in endpoints
if path.startswith("/" + AuthentikAPIConfig.mountpoint)
]

View File

@ -82,8 +82,9 @@ INSTALLED_APPS = [
"authentik.sources.oauth", "authentik.sources.oauth",
"authentik.sources.plex", "authentik.sources.plex",
"authentik.sources.saml", "authentik.sources.saml",
"authentik.stages.authenticator", "authentik.sources.scim",
"authentik.stages.authenticator_duo", "authentik.stages.authenticator_duo",
"authentik.stages.authenticator",
"authentik.stages.authenticator_sms", "authentik.stages.authenticator_sms",
"authentik.stages.authenticator_static", "authentik.stages.authenticator_static",
"authentik.stages.authenticator_totp", "authentik.stages.authenticator_totp",
@ -146,6 +147,9 @@ SPECTACULAR_SETTINGS = {
"UserTypeEnum": "authentik.core.models.UserTypes", "UserTypeEnum": "authentik.core.models.UserTypes",
}, },
"ENUM_ADD_EXPLICIT_BLANK_NULL_CHOICE": False, "ENUM_ADD_EXPLICIT_BLANK_NULL_CHOICE": False,
"PREPROCESSING_HOOKS": [
"authentik.api.schema.preprocess_schema_exclude_non_api",
],
"POSTPROCESSING_HOOKS": [ "POSTPROCESSING_HOOKS": [
"authentik.api.schema.postprocess_schema_responses", "authentik.api.schema.postprocess_schema_responses",
"drf_spectacular.hooks.postprocess_schema_enums", "drf_spectacular.hooks.postprocess_schema_enums",

View File

View File

@ -0,0 +1,26 @@
"""SCIMSource API Views"""
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.sources.scim.models import SCIMSource
class SCIMSourceSerializer(SourceSerializer):
"""SCIMSource Serializer"""
class Meta:
model = SCIMSource
fields = SourceSerializer.Meta.fields + ["token"]
class SCIMSourceViewSet(UsedByMixin, ModelViewSet):
"""SCIMSource Viewset"""
queryset = SCIMSource.objects.all()
serializer_class = SCIMSourceSerializer
lookup_field = "slug"
filterset_fields = "__all__"
search_fields = ["name", "slug"]
ordering = ["name"]

View File

@ -0,0 +1,12 @@
"""Authentik SCIM app config"""
from django.apps import AppConfig
class AuthentikSourceSCIMConfig(AppConfig):
"""authentik SCIM Source app config"""
name = "authentik.sources.scim"
label = "authentik_sources_scim"
verbose_name = "authentik Sources.SCIM"
mountpoint = "source/scim/"

View File

@ -0,0 +1,42 @@
# Generated by Django 3.2.9 on 2021-11-15 12:51
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("authentik_core", "0018_auto_20210330_1345_squashed_0028_alter_token_intent"),
]
operations = [
migrations.CreateModel(
name="SCIMSource",
fields=[
(
"source_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_core.source",
),
),
(
"token",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="authentik_core.token"
),
),
],
options={
"abstract": False,
},
bases=("authentik_core.source",),
),
]

View File

@ -0,0 +1,26 @@
"""SCIM Source"""
from django.db import models
from rest_framework.serializers import BaseSerializer
from authentik.core.models import Source, Token
USER_ATTRIBUTE_SCIM_ID = "goauthentik.io/sources/scim/id"
USER_ATTRIBUTE_SCIM_ADDRESS = "goauthentik.io/sources/scim/address"
USER_ATTRIBUTE_SCIM_ENTERPRISE = "goauthentik.io/sources/scim/enterprise"
class SCIMSource(Source):
"""SCIM Source"""
token = models.ForeignKey(Token, on_delete=models.CASCADE)
@property
def component(self) -> str:
"""Return component used to edit this object"""
return "ak-source-scim-form"
@property
def serializer(self) -> BaseSerializer:
from authentik.sources.scim.api import SCIMSourceSerializer
return SCIMSourceSerializer

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,58 @@
"""SCIM URLs"""
from django.urls import path
from authentik.sources.scim.views.v2 import (
groups,
resource_types,
schemas,
service_provider_config,
users,
)
urlpatterns = [
path(
"<slug:source_slug>/v2/Users",
users.UsersView.as_view(),
name="v2-users",
),
path(
"<slug:source_slug>/v2/Users/<str:user_id>",
users.UsersView.as_view(),
name="v2-users",
),
path(
"<slug:source_slug>/v2/Groups",
groups.GroupsView.as_view(),
name="v2-groups",
),
path(
"<slug:source_slug>/v2/Groups/<str:group_id>",
groups.GroupsView.as_view(),
name="v2-groups",
),
path(
"<slug:source_slug>/v2/Schemas",
schemas.SchemaView.as_view(),
name="v2-schema",
),
path(
"<slug:source_slug>/v2/Schemas/<str:schema_uri>",
schemas.SchemaView.as_view(),
name="v2-schema",
),
path(
"<slug:source_slug>/v2/ServiceProviderConfig",
service_provider_config.ServiceProviderConfigView.as_view(),
name="v2-service-provider-config",
),
path(
"<slug:source_slug>/v2/ResourceTypes",
resource_types.ResourceTypesView.as_view(),
name="v2-resource-types",
),
path(
"<slug:source_slug>/v2/ResourceTypes/<str:resource_type>",
resource_types.ResourceTypesView.as_view(),
name="v2-resource-types",
),
]

View File

View File

@ -0,0 +1,41 @@
"""SCIM Token auth"""
from base64 import b64decode
from typing import Any, Optional, Union
from rest_framework.authentication import BaseAuthentication, get_authorization_header
from rest_framework.request import Request
from authentik.core.models import Token, TokenIntents, User
class SCIMTokenAuth(BaseAuthentication):
"""SCIM Token auth"""
def legacy(self, key: str, source_slug: str) -> Optional[Token]:
"""Legacy HTTP-Basic auth for testing"""
_username, _, password = b64decode(key.encode()).decode().partition(":")
token = self.check_token(password, source_slug)
if token:
return (token.user, token)
return None
def check_token(self, key: str, source_slug: str) -> Optional[Token]:
"""Check that a token exists, is not expired, and is assigned to the correct source"""
token = Token.filter_not_expired(key=key, intent=TokenIntents.INTENT_API).first()
if not token:
return None
if not token.scimsource_set.exists():
return None
if token.scimsource_set.first().slug != source_slug:
return None
return token
def authenticate(self, request: Request) -> Union[tuple[User, Any], None]:
kwargs = request._request.resolver_match.kwargs
source_slug = kwargs.get("source_slug", None)
auth = get_authorization_header(request).decode()
auth_type, _, key = auth.partition(" ")
if auth_type != "Bearer":
return self.legacy(key, source_slug)
token = self.check_token(key, source_slug)
return (token.user, token)

View File

@ -0,0 +1,30 @@
"""SCIM Utils"""
from rest_framework.parsers import JSONParser
from rest_framework.permissions import IsAuthenticated
from rest_framework.renderers import JSONRenderer
from rest_framework.views import APIView
from authentik.sources.scim.views.v2.auth import SCIMTokenAuth
SCIM_CONTENT_TYPE = "application/scim+json"
class SCIMParser(JSONParser):
"""SCIM clients use a custom content type"""
media_type = SCIM_CONTENT_TYPE
class SCIMRenderer(JSONRenderer):
"""SCIM clients also expect a custom content type"""
media_type = SCIM_CONTENT_TYPE
class SCIMView(APIView):
"""Base class for SCIM Views"""
authentication_classes = [SCIMTokenAuth]
permission_classes = [IsAuthenticated]
parser_classes = [SCIMParser]
renderer_classes = [SCIMRenderer]

View File

@ -0,0 +1,103 @@
"""SCIM Group Views"""
from typing import Optional
from django.core.paginator import Paginator
from django.http import Http404, QueryDict
from django.urls import reverse
from rest_framework.request import Request
from rest_framework.response import Response
from structlog.stdlib import get_logger
from authentik.core.models import Group
from authentik.sources.scim.views.v2.base import SCIM_CONTENT_TYPE, SCIMView
LOGGER = get_logger()
class GroupsView(SCIMView):
"""SCIM Group View"""
def group_to_scim(self, group: Group) -> dict:
"""Convert group to SCIM"""
return {
"id": str(group.pk),
"meta": {
"resourceType": "Group",
"location": self.request.build_absolute_uri(
reverse(
"authentik_sources_scim:v2-groups",
kwargs={
"source_slug": self.kwargs["source_slug"],
"group_id": str(group.pk),
},
)
),
},
"displayName": group.name,
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
}
def get(self, request: Request, group_id: Optional[str] = None, **kwargs) -> Response:
"""List Group handler"""
if group_id:
group = Group.objects.filter(pk=group_id).first()
if not group:
raise Http404
return Response(self.group_to_scim(group))
groups = Group.objects.all().order_by("pk")
per_page = 50
paginator = Paginator(groups, per_page=per_page)
start_index = int(request.query_params.get("startIndex", 1))
page = paginator.page(int(max(start_index / per_page, 1)))
return Response(
{
"totalResults": paginator.count,
"itemsPerPage": per_page,
"startIndex": page.start_index(),
"schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
"Resources": [self.group_to_scim(group) for group in page],
}
)
def update_group(self, group: Group, data: QueryDict) -> Group:
"""Partial update a group"""
if "displayName" in data:
group.name = data.get("displayName")
return group
def post(self, request: Request, **kwargs) -> Response:
"""Create group handler"""
group = Group.objects.filter(name=request.data.get("displayName")).first()
if group:
LOGGER.debug("Found existing group")
return Response(status=409)
group = self.update_group(Group(), request.data)
group.save()
return Response(self.group_to_scim(group), status=201)
def patch(self, request: Request, group_id: str, **kwargs) -> Response:
"""Update group handler"""
return self.put(request, group_id, **kwargs)
def put(self, request: Request, group_id: str, **kwargs) -> Response:
"""Update group handler"""
group: Optional[Group] = Group.objects.filter(pk=group_id).first()
if not group:
raise Http404
self.update_group(group, request.data)
group.save()
return Response(self.group_to_scim(group), status=200)
def delete(self, request: Request, group_id: str, **kwargs) -> Response:
"""Delete group handler"""
group: Optional[Group] = Group.objects.filter(pk=group_id).first()
if not group:
raise Http404
group.delete()
return Response(
{},
status=204,
headers={
"Content-Type": SCIM_CONTENT_TYPE,
},
)

View File

@ -0,0 +1,153 @@
"""SCIM Meta views"""
from typing import Optional
from django.http import Http404
from django.urls import reverse
from rest_framework.request import Request
from rest_framework.response import Response
from authentik.sources.scim.views.v2.base import SCIMView
class ResourceTypesView(SCIMView):
"""https://ldapwiki.com/wiki/SCIM%20ResourceTypes%20endpoint"""
def get_resource_types(self):
"""List all resource types"""
return [
{
"id": "ServiceProviderConfig",
"name": "ServiceProviderConfig",
"description": "the service providers configuration",
"endpoint": "/ServiceProviderConfig",
"schema": "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig",
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:ResourceType",
],
"meta": {
"resourceType": "ResourceType",
"location": self.request.build_absolute_uri(
reverse(
"authentik_sources_scim:v2-resource-types",
kwargs={
"source_slug": self.kwargs["source_slug"],
"resource_type": "ServiceProviderConfig",
},
)
),
},
},
{
"id": "ResourceType",
"name": "ResourceType",
"description": "ResourceType",
"endpoint": "/ResourceTypes",
"schema": "urn:ietf:params:scim:schemas:core:2.0:ResourceType",
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:ResourceType",
],
"meta": {
"resourceType": "ResourceType",
"location": self.request.build_absolute_uri(
reverse(
"authentik_sources_scim:v2-resource-types",
kwargs={
"source_slug": self.kwargs["source_slug"],
"resource_type": "ResourceType",
},
)
),
},
},
{
"id": "Schema",
"name": "Schema",
"description": "Schema endpoint description",
"endpoint": "/Schemas",
"schema": "urn:ietf:params:scim:schemas:core:2.0:Schema",
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:ResourceType",
],
"meta": {
"resourceType": "ResourceType",
"location": self.request.build_absolute_uri(
reverse(
"authentik_sources_scim:v2-resource-types",
kwargs={
"source_slug": self.kwargs["source_slug"],
"resource_type": "Schema",
},
)
),
},
},
{
"id": "User",
"name": "User",
"endpoint": "/Users",
"description": "https://tools.ietf.org/html/rfc7643#section-8.7.1",
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:ResourceType"],
"schema": "urn:ietf:params:scim:schemas:core:2.0:User",
"schemaExtensions": [
{
"schema": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User",
"required": True,
}
],
"meta": {
"location": self.request.build_absolute_uri(
reverse(
"authentik_sources_scim:v2-resource-types",
kwargs={
"source_slug": self.kwargs["source_slug"],
"resource_type": "User",
},
)
),
"resourceType": "ResourceType",
},
},
{
"id": "Group",
"name": "Group",
"description": "Group",
"endpoint": "/Groups",
"schema": "urn:ietf:params:scim:schemas:core:2.0:Group",
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:ResourceType",
],
"meta": {
"resourceType": "ResourceType",
"location": self.request.build_absolute_uri(
reverse(
"authentik_sources_scim:v2-resource-types",
kwargs={
"source_slug": self.kwargs["source_slug"],
"resource_type": "Group",
},
)
),
},
},
]
# pylint: disable=unused-argument
def get(
self, request: Request, source_slug: str, resource_type: Optional[str] = None
) -> Response:
"""Get resource types as SCIM response"""
resource_types = self.get_resource_types()
if resource_type:
resource = [x for x in resource_types if x.get("id") == resource_type]
if resource:
return Response(resource[0])
raise Http404
return Response(
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
"totalResults": len(resource_types),
"itemsPerPage": len(resource_types),
"startIndex": 1,
"Resources": resource_types,
}
)

View File

@ -0,0 +1,52 @@
"""Schema Views"""
from json import loads
from typing import Optional
from django.http import Http404
from django.urls import reverse
from rest_framework.request import Request
from rest_framework.response import Response
from authentik.sources.scim.views.v2.base import SCIMView
with open("authentik/sources/scim/schemas/schema.json", "r", encoding="utf-8") as SCHEMA_FILE:
_raw_schemas = loads(SCHEMA_FILE.read())
class SchemaView(SCIMView):
"""https://ldapwiki.com/wiki/SCIM%20Schemas%20Attribute"""
def get_schemas(self):
"""List of all schemas"""
schemas = []
for raw_schema in _raw_schemas:
raw_schema["meta"]["location"] = self.request.build_absolute_uri(
reverse(
"authentik_sources_scim:v2-schema",
kwargs={
"source_slug": self.kwargs["source_slug"],
"schema_uri": raw_schema["id"],
},
)
)
schemas.append(raw_schema)
return schemas
# pylint: disable=unused-argument
def get(self, request: Request, source_slug: str, schema_uri: Optional[str] = None) -> Response:
"""Get schemas as SCIM response"""
schemas = self.get_schemas()
if schema_uri:
schema = [x for x in schemas if x.get("id") == schema_uri]
if schema:
return Response(schema[0])
raise Http404
return Response(
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
"totalResults": len(schemas),
"itemsPerPage": len(schemas),
"startIndex": 1,
"Resources": schemas,
}
)

View File

@ -0,0 +1,35 @@
"""SCIM Meta views"""
from rest_framework.request import Request
from rest_framework.response import Response
from authentik.sources.scim.views.v2.base import SCIMView
class ServiceProviderConfigView(SCIMView):
"""ServiceProviderConfig, https://ldapwiki.com/wiki/SCIM%20ServiceProviderConfig%20endpoint"""
# pylint: disable=unused-argument
def get(self, request: Request, source_slug: str) -> Response:
"""Get ServiceProviderConfig"""
return Response(
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
"authenticationSchemes": [
{
"type": "oauthbearertoken",
"name": "OAuth Bearer Token",
"description": (
"Authentication scheme using the OAuth Bearer Token Standard"
),
"specUri": "https://www.rfc-editor.org/info/rfc6750",
"primary": True,
},
],
"patch": {"supported": True},
"bulk": {"supported": False, "maxOperations": 0, "maxPayloadSize": 0},
"filter": {"supported": False, "maxResults": 200},
"changePassword": {"supported": True},
"sort": {"supported": False},
"etag": {"supported": False},
}
)

View File

@ -0,0 +1,151 @@
"""SCIM User Views"""
from typing import Optional
from django.core.paginator import Paginator
from django.http import Http404, QueryDict
from django.urls import reverse
from guardian.shortcuts import get_anonymous_user
from rest_framework.request import Request
from rest_framework.response import Response
from structlog.stdlib import get_logger
from authentik.core.models import User
from authentik.sources.scim.models import (
USER_ATTRIBUTE_SCIM_ADDRESS,
USER_ATTRIBUTE_SCIM_ENTERPRISE,
USER_ATTRIBUTE_SCIM_ID,
)
from authentik.sources.scim.views.v2.base import SCIM_CONTENT_TYPE, SCIMView
LOGGER = get_logger()
class UsersView(SCIMView):
"""SCIM User view"""
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")
return data[0].get("value")
def user_to_scim(self, user: User) -> dict:
"""Convert User to SCIM data"""
payload = {
"id": str(user.pk),
"meta": {
"resourceType": "User",
"created": user.date_joined,
# TODO: use events to find last edit?
"lastModified": user.date_joined,
"location": self.request.build_absolute_uri(
reverse(
"authentik_sources_scim:v2-users",
kwargs={
"source_slug": self.kwargs["source_slug"],
"user_id": str(user.pk),
},
)
),
},
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": user.attributes.get(
USER_ATTRIBUTE_SCIM_ENTERPRISE, {}
),
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User",
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User",
],
"userName": user.username,
"name": {},
"displayName": user.name,
"active": user.is_active,
"emails": [{"value": user.email, "type": "work", "primary": True}],
}
if USER_ATTRIBUTE_SCIM_ID in user.attributes:
payload["externalId"] = user.attributes[USER_ATTRIBUTE_SCIM_ID]
return payload
def get(self, request: Request, user_id: Optional[str] = None, **kwargs) -> Response:
"""List User handler"""
if user_id:
user = User.objects.filter(pk=user_id).first()
if not user:
raise Http404
return Response(self.user_to_scim(user))
users = User.objects.all().exclude(pk=get_anonymous_user().pk).order_by("pk")
per_page = 50
paginator = Paginator(users, per_page=per_page)
start_index = int(request.query_params.get("startIndex", 1))
page = paginator.page(int(max(start_index / per_page, 1)))
return Response(
{
"totalResults": paginator.count,
"itemsPerPage": per_page,
"startIndex": page.start_index(),
"schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
"Resources": [self.user_to_scim(user) for user in page],
}
)
def update_user(self, user: User, data: QueryDict) -> User:
"""Partial update a user"""
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 "externalId" in data:
user.attributes[USER_ATTRIBUTE_SCIM_ID] = data.get("externalId")
if "addresses" in data:
user.attributes[USER_ATTRIBUTE_SCIM_ADDRESS] = data.get("addresses")
if "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User" in data:
user.attributes[USER_ATTRIBUTE_SCIM_ENTERPRISE] = data.get(
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
)
return user
def post(self, request: Request, **kwargs) -> Response:
"""Create user handler"""
user = User.objects.filter(
**{
f"attributes__{USER_ATTRIBUTE_SCIM_ID}": request.data.get("externalId"),
"username": request.data.get("userName"),
}
).first()
if user:
LOGGER.debug("Found existing user")
return Response(status=409)
user = self.update_user(User(), request.data)
user.save()
return Response(self.user_to_scim(user), status=201)
def patch(self, request: Request, user_id: str, **kwargs) -> Response:
"""Update user handler"""
return self.put(request, user_id, **kwargs)
def put(self, request: Request, user_id: str, **kwargs) -> Response:
"""Update user handler"""
user: Optional[User] = User.objects.filter(pk=user_id).first()
if not user:
raise Http404
self.update_user(user, request.data)
user.save()
return Response(self.user_to_scim(user), status=200)
def delete(self, request: Request, user_id: str, **kwargs) -> Response:
"""Delete user handler"""
user: Optional[User] = User.objects.filter(pk=user_id).first()
if not user:
raise Http404
user.delete()
return Response(
{},
status=204,
headers={
"Content-Type": SCIM_CONTENT_TYPE,
},
)

View File

@ -51,6 +51,10 @@ export class SourceViewPage extends AKElement {
return html`<ak-source-plex-view return html`<ak-source-plex-view
sourceSlug=${this.source.slug} sourceSlug=${this.source.slug}
></ak-source-plex-view>`; ></ak-source-plex-view>`;
case "ak-source-scim-form":
return html`<ak-source-scim-view
sourceSlug=${this.source.slug}
></ak-source-scim-view>`;
default: default:
return html`<p>Invalid source type ${this.source.component}</p>`; return html`<p>Invalid source type ${this.source.component}</p>`;
} }

View File

@ -0,0 +1,79 @@
import { t } from "@lingui/macro";
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 { DEFAULT_CONFIG } from "../../../api/Config";
import "../../../elements/CodeMirror";
import "../../../elements/forms/FormGroup";
import "../../../elements/forms/HorizontalFormElement";
import { ModelForm } from "../../../elements/forms/ModelForm";
import { first } from "../../../utils";
@customElement("ak-source-scim-form")
export class SCIMSourceForm extends ModelForm<SCIMSource, string> {
loadInstance(pk: string): Promise<SCIMSource> {
return new SourcesApi(DEFAULT_CONFIG)
.sourcesScimRetrieve({
slug: pk,
})
.then((source) => {
return source;
});
}
getSuccessMessage(): string {
if (this.instance) {
return t`Successfully updated source.`;
} else {
return t`Successfully created source.`;
}
}
send = (data: SCIMSource): Promise<SCIMSource> => {
if (this.instance?.slug) {
return new SourcesApi(DEFAULT_CONFIG).sourcesScimPartialUpdate({
slug: this.instance.slug,
patchedSCIMSourceRequest: data,
});
} else {
return new SourcesApi(DEFAULT_CONFIG).sourcesScimCreate({
sCIMSourceRequest: data as unknown as SCIMSourceRequest,
});
}
};
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal label=${t`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=${t`Slug`} ?required=${true} name="slug">
<input
type="text"
value="${ifDefined(this.instance?.slug)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="enabled">
<div class="pf-c-check">
<input
type="checkbox"
class="pf-c-check__input"
?checked=${first(this.instance?.enabled, true)}
/>
<label class="pf-c-check__label"> ${t`Enabled`} </label>
</div>
</ak-form-element-horizontal>
</form>`;
}
}

View File

@ -0,0 +1,125 @@
import { t } from "@lingui/macro";
import { CSSResult, LitElement, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import AKGlobal from "../../../authentik.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { SCIMSource, SourcesApi } from "@goauthentik/api";
import { DEFAULT_CONFIG } from "../../../api/Config";
import { EVENT_REFRESH } from "../../../constants";
import "../../../elements/CodeMirror";
import "../../../elements/Tabs";
import "../../../elements/buttons/SpinnerButton";
import "../../../elements/events/ObjectChangelog";
import "../../../elements/forms/ModalForm";
import "../../policies/BoundPoliciesList";
import "./SCIMSourceForm";
@customElement("ak-source-scim-view")
export class SCIMSourceViewPage extends LitElement {
@property({ type: String })
set sourceSlug(value: string) {
new SourcesApi(DEFAULT_CONFIG)
.sourcesScimRetrieve({
slug: value,
})
.then((source) => {
this.source = source;
});
}
@property({ attribute: false })
source?: SCIMSource;
static get styles(): CSSResult[] {
return [PFBase, PFPage, PFButton, PFGrid, PFContent, PFCard, PFDescriptionList, AKGlobal];
}
constructor() {
super();
this.addEventListener(EVENT_REFRESH, () => {
if (!this.source?.pk) return;
this.sourceSlug = this.source?.slug;
});
}
render(): TemplateResult {
if (!this.source) {
return html``;
}
return html`<ak-tabs>
<section
slot="page-overview"
data-tab-title="${t`Overview`}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<div class="pf-l-grid pf-m-gutter">
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
<div class="pf-c-card__body">
<dl class="pf-c-description-list pf-m-2-col-on-lg">
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${t`Name`}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${this.source.name}
</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${t`Callback URL`}</span
>
</dt>
<dd class="pf-c-description-list__description">
<code class="pf-c-description-list__text"></code>
</dd>
</div>
</dl>
</div>
<div class="pf-c-card__footer">
<ak-forms-modal>
<span slot="submit"> ${t`Update`} </span>
<span slot="header"> ${t`Update SCIM Source`} </span>
<ak-source-scim-form slot="form" .instancePk=${this.source.slug}>
</ak-source-scim-form>
<button slot="trigger" class="pf-c-button pf-m-primary">
${t`Edit`}
</button>
</ak-forms-modal>
</div>
</div>
</div>
</section>
<section
slot="page-changelog"
data-tab-title="${t`Changelog`}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<div class="pf-l-grid pf-m-gutter">
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
<div class="pf-c-card__body">
<ak-object-changelog
targetModelPk=${this.source.pk || ""}
targetModelApp="authentik_sources_scim"
targetModelName="scimsource"
>
</ak-object-changelog>
</div>
</div>
</div>
</section>
</ak-tabs>`;
}
}