Merge branch 'main' into dev
* main: (34 commits) web: bump API Client version (#9299) core: fix api schema for users and groups (#9298) providers/oauth2: fix refresh_token grant returning incorrect id_token (#9275) web: bump @sentry/browser from 7.110.0 to 7.110.1 in /web in the sentry group (#9278) core, web: update translations (#9277) web: bump the rollup group in /web with 3 updates (#9280) web: bump lit from 3.1.2 to 3.1.3 in /web (#9282) web: bump @lit/context from 1.1.0 to 1.1.1 in /web (#9281) website: bump @types/react from 18.2.78 to 18.2.79 in /website (#9286) core: bump goauthentik.io/api/v3 from 3.2024022.10 to 3.2024022.11 (#9285) core: bump sqlparse from 0.4.4 to 0.5.0 (#9276) lifecycle: gunicorn: fix app preload (#9274) events: add indexes (#9272) web/flows: fix passwordless hidden without input (#9273) root: fix geoipupdate arguments (#9271) website/docs: cleanup more (#9249) web: bump API Client version (#9270) sources: add SCIM source (#3051) core: delegated group member management (#9254) web: bump API Client version (#9269) ...
This commit is contained in:
2
.github/workflows/ci-main.yml
vendored
2
.github/workflows/ci-main.yml
vendored
@ -160,6 +160,8 @@ jobs:
|
||||
glob: tests/e2e/test_provider_ldap* tests/e2e/test_source_ldap*
|
||||
- name: radius
|
||||
glob: tests/e2e/test_provider_radius*
|
||||
- name: scim
|
||||
glob: tests/e2e/test_source_scim*
|
||||
- name: flows
|
||||
glob: tests/e2e/test_flows*
|
||||
steps:
|
||||
|
@ -3,6 +3,12 @@ on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '30 1 1,15 * *'
|
||||
|
||||
env:
|
||||
POSTGRES_DB: authentik
|
||||
POSTGRES_USER: authentik
|
||||
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -70,10 +70,10 @@ RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
||||
GOARM="${TARGETVARIANT#v}" go build -o /go/authentik ./cmd/server
|
||||
|
||||
# Stage 4: MaxMind GeoIP
|
||||
FROM --platform=${BUILDPLATFORM} ghcr.io/maxmind/geoipupdate:v7.0 as geoip
|
||||
FROM --platform=${BUILDPLATFORM} ghcr.io/maxmind/geoipupdate:v7.0.1 as geoip
|
||||
|
||||
ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City GeoLite2-ASN"
|
||||
ENV GEOIPUPDATE_VERBOSE="true"
|
||||
ENV GEOIPUPDATE_VERBOSE="1"
|
||||
ENV GEOIPUPDATE_ACCOUNT_ID_FILE="/run/secrets/GEOIPUPDATE_ACCOUNT_ID"
|
||||
ENV GEOIPUPDATE_LICENSE_KEY_FILE="/run/secrets/GEOIPUPDATE_LICENSE_KEY"
|
||||
|
||||
|
@ -12,6 +12,7 @@ from drf_spectacular.settings import spectacular_settings
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from rest_framework.settings import api_settings
|
||||
|
||||
from authentik.api.apps import AuthentikAPIConfig
|
||||
from authentik.api.pagination import PAGINATION_COMPONENT_NAME, PAGINATION_SCHEMA
|
||||
|
||||
|
||||
@ -101,3 +102,12 @@ def postprocess_schema_responses(result, generator: SchemaGenerator, **kwargs):
|
||||
comp = result["components"]["schemas"][component]
|
||||
comp["additionalProperties"] = {}
|
||||
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)
|
||||
]
|
||||
|
@ -51,6 +51,7 @@ from authentik.policies.models import Policy, PolicyBindingModel
|
||||
from authentik.policies.reputation.models import Reputation
|
||||
from authentik.providers.oauth2.models import AccessToken, AuthorizationCode, RefreshToken
|
||||
from authentik.providers.scim.models import SCIMGroup, SCIMUser
|
||||
from authentik.sources.scim.models import SCIMSourceGroup, SCIMSourceUser
|
||||
from authentik.stages.authenticator_webauthn.models import WebAuthnDeviceType
|
||||
from authentik.tenants.models import Tenant
|
||||
|
||||
@ -97,6 +98,8 @@ def excluded_models() -> list[type[Model]]:
|
||||
RefreshToken,
|
||||
Reputation,
|
||||
WebAuthnDeviceType,
|
||||
SCIMSourceUser,
|
||||
SCIMSourceGroup,
|
||||
)
|
||||
|
||||
|
||||
|
@ -5,10 +5,15 @@ from json import loads
|
||||
from django.http import Http404
|
||||
from django_filters.filters import CharFilter, ModelMultipleChoiceFilter
|
||||
from django_filters.filterset import FilterSet
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||
from drf_spectacular.utils import (
|
||||
OpenApiParameter,
|
||||
OpenApiResponse,
|
||||
extend_schema,
|
||||
extend_schema_field,
|
||||
)
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import CharField, IntegerField
|
||||
from rest_framework.fields import CharField, IntegerField, SerializerMethodField
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ListSerializer, ModelSerializer, ValidationError
|
||||
@ -45,9 +50,7 @@ class GroupSerializer(ModelSerializer):
|
||||
"""Group Serializer"""
|
||||
|
||||
attributes = JSONDictField(required=False)
|
||||
users_obj = ListSerializer(
|
||||
child=GroupMemberSerializer(), read_only=True, source="users", required=False
|
||||
)
|
||||
users_obj = SerializerMethodField(allow_null=True)
|
||||
roles_obj = ListSerializer(
|
||||
child=RoleSerializer(),
|
||||
read_only=True,
|
||||
@ -58,6 +61,19 @@ class GroupSerializer(ModelSerializer):
|
||||
|
||||
num_pk = IntegerField(read_only=True)
|
||||
|
||||
@property
|
||||
def _should_include_users(self) -> bool:
|
||||
request: Request = self.context.get("request", None)
|
||||
if not request:
|
||||
return True
|
||||
return str(request.query_params.get("include_users", "true")).lower() == "true"
|
||||
|
||||
@extend_schema_field(GroupMemberSerializer(many=True))
|
||||
def get_users_obj(self, instance: Group) -> list[GroupMemberSerializer] | None:
|
||||
if not self._should_include_users:
|
||||
return None
|
||||
return GroupMemberSerializer(instance.users, many=True).data
|
||||
|
||||
def validate_parent(self, parent: Group | None):
|
||||
"""Validate group parent (if set), ensuring the parent isn't itself"""
|
||||
if not self.instance or not parent:
|
||||
@ -130,22 +146,29 @@ class GroupFilter(FilterSet):
|
||||
fields = ["name", "is_superuser", "members_by_pk", "attributes", "members_by_username"]
|
||||
|
||||
|
||||
class UserAccountSerializer(PassiveSerializer):
|
||||
"""Account adding/removing operations"""
|
||||
|
||||
pk = IntegerField(required=True)
|
||||
|
||||
|
||||
class GroupViewSet(UsedByMixin, ModelViewSet):
|
||||
"""Group Viewset"""
|
||||
|
||||
class UserAccountSerializer(PassiveSerializer):
|
||||
"""Account adding/removing operations"""
|
||||
|
||||
pk = IntegerField(required=True)
|
||||
|
||||
queryset = Group.objects.all().select_related("parent").prefetch_related("users")
|
||||
serializer_class = GroupSerializer
|
||||
search_fields = ["name", "is_superuser"]
|
||||
filterset_class = GroupFilter
|
||||
ordering = ["name"]
|
||||
|
||||
@permission_required(None, ["authentik_core.add_user"])
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter("include_users", bool, default=True),
|
||||
]
|
||||
)
|
||||
def list(self, request, *args, **kwargs):
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
@permission_required("authentik_core.add_user_to_group")
|
||||
@extend_schema(
|
||||
request=UserAccountSerializer,
|
||||
responses={
|
||||
@ -153,7 +176,13 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
|
||||
404: OpenApiResponse(description="User not found"),
|
||||
},
|
||||
)
|
||||
@action(detail=True, methods=["POST"], pagination_class=None, filter_backends=[])
|
||||
@action(
|
||||
detail=True,
|
||||
methods=["POST"],
|
||||
pagination_class=None,
|
||||
filter_backends=[],
|
||||
permission_classes=[],
|
||||
)
|
||||
def add_user(self, request: Request, pk: str) -> Response:
|
||||
"""Add user to group"""
|
||||
group: Group = self.get_object()
|
||||
@ -169,7 +198,7 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
|
||||
group.users.add(user)
|
||||
return Response(status=204)
|
||||
|
||||
@permission_required(None, ["authentik_core.add_user"])
|
||||
@permission_required("authentik_core.remove_user_from_group")
|
||||
@extend_schema(
|
||||
request=UserAccountSerializer,
|
||||
responses={
|
||||
@ -177,7 +206,13 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
|
||||
404: OpenApiResponse(description="User not found"),
|
||||
},
|
||||
)
|
||||
@action(detail=True, methods=["POST"], pagination_class=None, filter_backends=[])
|
||||
@action(
|
||||
detail=True,
|
||||
methods=["POST"],
|
||||
pagination_class=None,
|
||||
filter_backends=[],
|
||||
permission_classes=[],
|
||||
)
|
||||
def remove_user(self, request: Request, pk: str) -> Response:
|
||||
"""Add user to group"""
|
||||
group: Group = self.get_object()
|
||||
|
@ -113,13 +113,26 @@ class UserSerializer(ModelSerializer):
|
||||
queryset=Group.objects.all().order_by("name"),
|
||||
default=list,
|
||||
)
|
||||
groups_obj = ListSerializer(child=UserGroupSerializer(), read_only=True, source="ak_groups")
|
||||
groups_obj = SerializerMethodField(allow_null=True)
|
||||
uid = CharField(read_only=True)
|
||||
username = CharField(
|
||||
max_length=150,
|
||||
validators=[UniqueValidator(queryset=User.objects.all().order_by("username"))],
|
||||
)
|
||||
|
||||
@property
|
||||
def _should_include_groups(self) -> bool:
|
||||
request: Request = self.context.get("request", None)
|
||||
if not request:
|
||||
return True
|
||||
return str(request.query_params.get("include_groups", "true")).lower() == "true"
|
||||
|
||||
@extend_schema_field(UserGroupSerializer(many=True))
|
||||
def get_groups_obj(self, instance: User) -> list[UserGroupSerializer] | None:
|
||||
if not self._should_include_groups:
|
||||
return None
|
||||
return UserGroupSerializer(instance.ak_groups, many=True).data
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
|
||||
@ -397,6 +410,14 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
def get_queryset(self): # pragma: no cover
|
||||
return User.objects.all().exclude_anonymous().prefetch_related("ak_groups")
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter("include_groups", bool, default=True),
|
||||
]
|
||||
)
|
||||
def list(self, request, *args, **kwargs):
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
def _create_recovery_link(self) -> tuple[str, Token]:
|
||||
"""Create a recovery link (when the current brand has a recovery flow set),
|
||||
that can either be shown to an admin or sent to the user directly"""
|
||||
|
@ -0,0 +1,52 @@
|
||||
# Generated by Django 5.0.4 on 2024-04-15 11:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("auth", "0012_alter_user_first_name_max_length"),
|
||||
("authentik_core", "0034_alter_authenticatedsession_expires_and_more"),
|
||||
("authentik_rbac", "0003_alter_systempermission_options"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="group",
|
||||
options={
|
||||
"permissions": [
|
||||
("add_user_to_group", "Add user to group"),
|
||||
("remove_user_from_group", "Remove user from group"),
|
||||
],
|
||||
"verbose_name": "Group",
|
||||
"verbose_name_plural": "Groups",
|
||||
},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="group",
|
||||
index=models.Index(fields=["name"], name="authentik_c_name_9ba8e4_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="user",
|
||||
index=models.Index(fields=["last_login"], name="authentik_c_last_lo_f0179a_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="user",
|
||||
index=models.Index(
|
||||
fields=["password_change_date"], name="authentik_c_passwor_eec915_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="user",
|
||||
index=models.Index(fields=["uuid"], name="authentik_c_uuid_3dae2f_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="user",
|
||||
index=models.Index(fields=["path"], name="authentik_c_path_b1f502_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="user",
|
||||
index=models.Index(fields=["type"], name="authentik_c_type_ecf60d_idx"),
|
||||
),
|
||||
]
|
@ -185,8 +185,13 @@ class Group(SerializerModel):
|
||||
"parent",
|
||||
),
|
||||
)
|
||||
indexes = [models.Index(fields=["name"])]
|
||||
verbose_name = _("Group")
|
||||
verbose_name_plural = _("Groups")
|
||||
permissions = [
|
||||
("add_user_to_group", _("Add user to group")),
|
||||
("remove_user_from_group", _("Remove user from group")),
|
||||
]
|
||||
|
||||
|
||||
class UserQuerySet(models.QuerySet):
|
||||
@ -323,6 +328,13 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
|
||||
("preview_user", _("Can preview user data sent to providers")),
|
||||
("view_user_applications", _("View applications the user has access to")),
|
||||
]
|
||||
indexes = [
|
||||
models.Index(fields=["last_login"]),
|
||||
models.Index(fields=["password_change_date"]),
|
||||
models.Index(fields=["uuid"]),
|
||||
models.Index(fields=["path"]),
|
||||
models.Index(fields=["type"]),
|
||||
]
|
||||
authentik_signals_ignored_fields = [
|
||||
# Logged by the events `password_set`
|
||||
# the `password_set` action/signal doesn't currently convey which user
|
||||
@ -659,7 +671,7 @@ class ExpiringModel(models.Model):
|
||||
return self.delete(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def filter_not_expired(cls, **kwargs) -> QuerySet:
|
||||
def filter_not_expired(cls, **kwargs) -> QuerySet["Token"]:
|
||||
"""Filer for tokens which are not expired yet or are not expiring,
|
||||
and match filters in `kwargs`"""
|
||||
for obj in cls.objects.filter(**kwargs).filter(Q(expires__lt=now(), expiring=True)):
|
||||
|
@ -1,10 +1,11 @@
|
||||
"""Test Groups API"""
|
||||
|
||||
from django.urls.base import reverse
|
||||
from guardian.shortcuts import assign_perm
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import Group, User
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.core.tests.utils import create_test_user
|
||||
from authentik.lib.generators import generate_id
|
||||
|
||||
|
||||
@ -12,13 +13,15 @@ class TestGroupsAPI(APITestCase):
|
||||
"""Test Groups API"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.admin = create_test_admin_user()
|
||||
self.login_user = create_test_user()
|
||||
self.user = User.objects.create(username="test-user")
|
||||
|
||||
def test_add_user(self):
|
||||
"""Test add_user"""
|
||||
group = Group.objects.create(name=generate_id())
|
||||
self.client.force_login(self.admin)
|
||||
assign_perm("authentik_core.add_user_to_group", self.login_user, group)
|
||||
assign_perm("authentik_core.view_user", self.login_user)
|
||||
self.client.force_login(self.login_user)
|
||||
res = self.client.post(
|
||||
reverse("authentik_api:group-add-user", kwargs={"pk": group.pk}),
|
||||
data={
|
||||
@ -32,7 +35,9 @@ class TestGroupsAPI(APITestCase):
|
||||
def test_add_user_404(self):
|
||||
"""Test add_user"""
|
||||
group = Group.objects.create(name=generate_id())
|
||||
self.client.force_login(self.admin)
|
||||
assign_perm("authentik_core.add_user_to_group", self.login_user, group)
|
||||
assign_perm("authentik_core.view_user", self.login_user)
|
||||
self.client.force_login(self.login_user)
|
||||
res = self.client.post(
|
||||
reverse("authentik_api:group-add-user", kwargs={"pk": group.pk}),
|
||||
data={
|
||||
@ -44,8 +49,10 @@ class TestGroupsAPI(APITestCase):
|
||||
def test_remove_user(self):
|
||||
"""Test remove_user"""
|
||||
group = Group.objects.create(name=generate_id())
|
||||
assign_perm("authentik_core.remove_user_from_group", self.login_user, group)
|
||||
assign_perm("authentik_core.view_user", self.login_user)
|
||||
group.users.add(self.user)
|
||||
self.client.force_login(self.admin)
|
||||
self.client.force_login(self.login_user)
|
||||
res = self.client.post(
|
||||
reverse("authentik_api:group-remove-user", kwargs={"pk": group.pk}),
|
||||
data={
|
||||
@ -59,8 +66,10 @@ class TestGroupsAPI(APITestCase):
|
||||
def test_remove_user_404(self):
|
||||
"""Test remove_user"""
|
||||
group = Group.objects.create(name=generate_id())
|
||||
assign_perm("authentik_core.remove_user_from_group", self.login_user, group)
|
||||
assign_perm("authentik_core.view_user", self.login_user)
|
||||
group.users.add(self.user)
|
||||
self.client.force_login(self.admin)
|
||||
self.client.force_login(self.login_user)
|
||||
res = self.client.post(
|
||||
reverse("authentik_api:group-remove-user", kwargs={"pk": group.pk}),
|
||||
data={
|
||||
@ -72,11 +81,12 @@ class TestGroupsAPI(APITestCase):
|
||||
def test_parent_self(self):
|
||||
"""Test parent"""
|
||||
group = Group.objects.create(name=generate_id())
|
||||
self.client.force_login(self.admin)
|
||||
assign_perm("view_group", self.login_user, group)
|
||||
assign_perm("change_group", self.login_user, group)
|
||||
self.client.force_login(self.login_user)
|
||||
res = self.client.patch(
|
||||
reverse("authentik_api:group-detail", kwargs={"pk": group.pk}),
|
||||
data={
|
||||
"pk": self.user.pk + 3,
|
||||
"parent": group.pk,
|
||||
},
|
||||
)
|
||||
|
@ -0,0 +1,39 @@
|
||||
# Generated by Django 5.0.4 on 2024-04-15 16:17
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_events", "0006_alter_systemtask_expires"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name="event",
|
||||
index=models.Index(fields=["action"], name="authentik_e_action_9a9dd9_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="event",
|
||||
index=models.Index(fields=["user"], name="authentik_e_user_1be48d_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="event",
|
||||
index=models.Index(fields=["app"], name="authentik_e_app_6a05ce_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="event",
|
||||
index=models.Index(fields=["created"], name="authentik_e_created_6f0834_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="event",
|
||||
index=models.Index(fields=["client_ip"], name="authentik_e_client__51f4dd_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="event",
|
||||
index=models.Index(
|
||||
models.F("context__authorized_application"), name="authentik_e_ctx_app__idx"
|
||||
),
|
||||
),
|
||||
]
|
@ -305,6 +305,16 @@ class Event(SerializerModel, ExpiringModel):
|
||||
class Meta:
|
||||
verbose_name = _("Event")
|
||||
verbose_name_plural = _("Events")
|
||||
indexes = [
|
||||
models.Index(fields=["action"]),
|
||||
models.Index(fields=["user"]),
|
||||
models.Index(fields=["app"]),
|
||||
models.Index(fields=["created"]),
|
||||
models.Index(fields=["client_ip"]),
|
||||
models.Index(
|
||||
models.F("context__authorized_application"), name="authentik_e_ctx_app__idx"
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class TransportMode(models.TextChoices):
|
||||
|
@ -11,7 +11,7 @@ from django.http import HttpRequest, HttpResponseNotFound
|
||||
from django.templatetags.static import static
|
||||
from lxml import etree # nosec
|
||||
from lxml.etree import Element, SubElement # nosec
|
||||
from requests.exceptions import RequestException
|
||||
from requests.exceptions import ConnectionError, HTTPError, RequestException, Timeout
|
||||
|
||||
from authentik.lib.config import get_path_from_dict
|
||||
from authentik.lib.utils.http import get_http_session
|
||||
@ -23,6 +23,8 @@ if TYPE_CHECKING:
|
||||
GRAVATAR_URL = "https://secure.gravatar.com"
|
||||
DEFAULT_AVATAR = static("dist/assets/images/user_default.png")
|
||||
CACHE_KEY_GRAVATAR = "goauthentik.io/lib/avatars/"
|
||||
CACHE_KEY_GRAVATAR_AVAILABLE = "goauthentik.io/lib/avatars/gravatar_available"
|
||||
GRAVATAR_STATUS_TTL_SECONDS = 60 * 60 * 8 # 8 Hours
|
||||
|
||||
SVG_XML_NS = "http://www.w3.org/2000/svg"
|
||||
SVG_NS_MAP = {None: SVG_XML_NS}
|
||||
@ -50,6 +52,9 @@ def avatar_mode_attribute(user: "User", mode: str) -> str | None:
|
||||
|
||||
def avatar_mode_gravatar(user: "User", mode: str) -> str | None:
|
||||
"""Gravatar avatars"""
|
||||
if not cache.get(CACHE_KEY_GRAVATAR_AVAILABLE, True):
|
||||
return None
|
||||
|
||||
# gravatar uses md5 for their URLs, so md5 can't be avoided
|
||||
mail_hash = md5(user.email.lower().encode("utf-8")).hexdigest() # nosec
|
||||
parameters = [("size", "158"), ("rating", "g"), ("default", "404")]
|
||||
@ -69,6 +74,8 @@ def avatar_mode_gravatar(user: "User", mode: str) -> str | None:
|
||||
cache.set(full_key, None)
|
||||
return None
|
||||
res.raise_for_status()
|
||||
except (Timeout, ConnectionError, HTTPError):
|
||||
cache.set(CACHE_KEY_GRAVATAR_AVAILABLE, False, timeout=GRAVATAR_STATUS_TTL_SECONDS)
|
||||
except RequestException:
|
||||
return gravatar_url
|
||||
cache.set(full_key, gravatar_url)
|
||||
|
@ -39,6 +39,7 @@ class Migration(migrations.Migration):
|
||||
("authentik.sources.oauth", "authentik Sources.OAuth"),
|
||||
("authentik.sources.plex", "authentik Sources.Plex"),
|
||||
("authentik.sources.saml", "authentik Sources.SAML"),
|
||||
("authentik.sources.scim", "authentik Sources.SCIM"),
|
||||
("authentik.stages.authenticator_duo", "authentik Stages.Authenticator.Duo"),
|
||||
("authentik.stages.authenticator_sms", "authentik Stages.Authenticator.SMS"),
|
||||
(
|
||||
|
@ -208,7 +208,7 @@ class TestToken(OAuthTestCase):
|
||||
"token_type": TOKEN_TYPE,
|
||||
"expires_in": 3600,
|
||||
"id_token": provider.encode(
|
||||
refresh.id_token.to_dict(),
|
||||
access.id_token.to_dict(),
|
||||
),
|
||||
},
|
||||
)
|
||||
@ -267,7 +267,7 @@ class TestToken(OAuthTestCase):
|
||||
"token_type": TOKEN_TYPE,
|
||||
"expires_in": 3600,
|
||||
"id_token": provider.encode(
|
||||
refresh.id_token.to_dict(),
|
||||
access.id_token.to_dict(),
|
||||
),
|
||||
},
|
||||
)
|
||||
|
@ -651,7 +651,7 @@ class TokenView(View):
|
||||
"expires_in": int(
|
||||
timedelta_from_string(self.provider.access_token_validity).total_seconds()
|
||||
),
|
||||
"id_token": id_token.to_jwt(self.provider),
|
||||
"id_token": access_token.id_token.to_jwt(self.provider),
|
||||
}
|
||||
|
||||
def create_client_credentials_response(self) -> dict[str, Any]:
|
||||
|
@ -13,15 +13,21 @@ from pydanticscim.user import User as BaseUser
|
||||
class User(BaseUser):
|
||||
"""Modified User schema with added externalId field"""
|
||||
|
||||
schemas: tuple[str] = ("urn:ietf:params:scim:schemas:core:2.0:User",)
|
||||
schemas: list[str] = [
|
||||
"urn:ietf:params:scim:schemas:core:2.0:User",
|
||||
]
|
||||
externalId: str | None = None
|
||||
meta: dict | None = None
|
||||
|
||||
|
||||
class Group(BaseGroup):
|
||||
"""Modified Group schema with added externalId field"""
|
||||
|
||||
schemas: tuple[str] = ("urn:ietf:params:scim:schemas:core:2.0:Group",)
|
||||
schemas: list[str] = [
|
||||
"urn:ietf:params:scim:schemas:core:2.0:Group",
|
||||
]
|
||||
externalId: str | None = None
|
||||
meta: dict | None = None
|
||||
|
||||
|
||||
class ServiceProviderConfiguration(BaseServiceProviderConfiguration):
|
||||
|
@ -8,7 +8,16 @@ from rest_framework.request import Request
|
||||
class ObjectPermissions(DjangoObjectPermissions):
|
||||
"""RBAC Permissions"""
|
||||
|
||||
def has_object_permission(self, request: Request, view, obj: Model):
|
||||
def has_permission(self, request: Request, view) -> bool:
|
||||
"""Always grant permission for object-specific requests
|
||||
as view permission checking is done by `ObjectFilter`,
|
||||
and write permission checking is done by `has_object_permission`"""
|
||||
lookup = getattr(view, "lookup_url_kwarg", None) or getattr(view, "lookup_field", None)
|
||||
if lookup and lookup in view.kwargs:
|
||||
return True
|
||||
return super().has_permission(request, view)
|
||||
|
||||
def has_object_permission(self, request: Request, view, obj: Model) -> bool:
|
||||
queryset = self._queryset(view)
|
||||
model_cls = queryset.model
|
||||
perms = self.get_required_object_permissions(request.method, model_cls)
|
||||
|
@ -121,3 +121,29 @@ class TestAPIPerms(APITestCase):
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 403)
|
||||
|
||||
def test_update_simple(self):
|
||||
"""Test update with permission"""
|
||||
self.client.force_login(self.user)
|
||||
inv = Invitation.objects.create(name=generate_id(), created_by=self.superuser)
|
||||
self.role.assign_permission("authentik_stages_invitation.view_invitation", obj=inv)
|
||||
self.role.assign_permission("authentik_stages_invitation.change_invitation", obj=inv)
|
||||
res = self.client.patch(
|
||||
reverse("authentik_api:invitation-detail", kwargs={"pk": inv.pk}),
|
||||
data={
|
||||
"name": generate_id(),
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
def test_update_simple_denied(self):
|
||||
"""Test update without assigning permission"""
|
||||
self.client.force_login(self.user)
|
||||
inv = Invitation.objects.create(name=generate_id(), created_by=self.superuser)
|
||||
res = self.client.patch(
|
||||
reverse("authentik_api:invitation-detail", kwargs={"pk": inv.pk}),
|
||||
data={
|
||||
"name": generate_id(),
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 403)
|
||||
|
@ -90,6 +90,7 @@ TENANT_APPS = [
|
||||
"authentik.sources.oauth",
|
||||
"authentik.sources.plex",
|
||||
"authentik.sources.saml",
|
||||
"authentik.sources.scim",
|
||||
"authentik.stages.authenticator",
|
||||
"authentik.stages.authenticator_duo",
|
||||
"authentik.stages.authenticator_sms",
|
||||
@ -157,6 +158,9 @@ SPECTACULAR_SETTINGS = {
|
||||
},
|
||||
"ENUM_ADD_EXPLICIT_BLANK_NULL_CHOICE": False,
|
||||
"ENUM_GENERATE_CHOICE_DESCRIPTION": False,
|
||||
"PREPROCESSING_HOOKS": [
|
||||
"authentik.api.schema.preprocess_schema_exclude_non_api",
|
||||
],
|
||||
"POSTPROCESSING_HOOKS": [
|
||||
"authentik.api.schema.postprocess_schema_responses",
|
||||
"drf_spectacular.hooks.postprocess_schema_enums",
|
||||
|
0
authentik/sources/scim/__init__.py
Normal file
0
authentik/sources/scim/__init__.py
Normal file
0
authentik/sources/scim/api/__init__.py
Normal file
0
authentik/sources/scim/api/__init__.py
Normal file
35
authentik/sources/scim/api/groups.py
Normal file
35
authentik/sources/scim/api/groups.py
Normal file
@ -0,0 +1,35 @@
|
||||
"""SCIMSourceGroup 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.core.api.users import UserGroupSerializer
|
||||
from authentik.sources.scim.models import SCIMSourceGroup
|
||||
|
||||
|
||||
class SCIMSourceGroupSerializer(SourceSerializer):
|
||||
"""SCIMSourceGroup Serializer"""
|
||||
|
||||
group_obj = UserGroupSerializer(source="group", read_only=True)
|
||||
|
||||
class Meta:
|
||||
|
||||
model = SCIMSourceGroup
|
||||
fields = [
|
||||
"id",
|
||||
"group",
|
||||
"group_obj",
|
||||
"source",
|
||||
"attributes",
|
||||
]
|
||||
|
||||
|
||||
class SCIMSourceGroupViewSet(UsedByMixin, ModelViewSet):
|
||||
"""SCIMSourceGroup Viewset"""
|
||||
|
||||
queryset = SCIMSourceGroup.objects.all().select_related("group")
|
||||
serializer_class = SCIMSourceGroupSerializer
|
||||
filterset_fields = ["source__slug", "group__name", "group__group_uuid"]
|
||||
search_fields = ["source__slug", "group__name", "attributes"]
|
||||
ordering = ["group__name"]
|
77
authentik/sources/scim/api/sources.py
Normal file
77
authentik/sources/scim/api/sources.py
Normal file
@ -0,0 +1,77 @@
|
||||
"""SCIMSource API Views"""
|
||||
|
||||
from django.urls import reverse_lazy
|
||||
from rest_framework.fields import SerializerMethodField
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.sources import SourceSerializer
|
||||
from authentik.core.api.tokens import TokenSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.models import Token, TokenIntents, User, UserTypes
|
||||
from authentik.sources.scim.models import SCIMSource
|
||||
|
||||
|
||||
class SCIMSourceSerializer(SourceSerializer):
|
||||
"""SCIMSource Serializer"""
|
||||
|
||||
root_url = SerializerMethodField()
|
||||
token_obj = TokenSerializer(source="token", required=False, read_only=True)
|
||||
|
||||
def get_root_url(self, instance: SCIMSource) -> str:
|
||||
"""Get Root URL"""
|
||||
relative_url = reverse_lazy(
|
||||
"authentik_sources_scim:v2-root",
|
||||
kwargs={"source_slug": instance.slug},
|
||||
)
|
||||
if "request" not in self.context:
|
||||
return relative_url
|
||||
return self.context["request"].build_absolute_uri(relative_url)
|
||||
|
||||
def create(self, validated_data):
|
||||
instance: SCIMSource = super().create(validated_data)
|
||||
identifier = f"ak-source-scim-{instance.pk}"
|
||||
user = User.objects.create(
|
||||
username=identifier,
|
||||
name=f"SCIM Source {instance.name} Service-Account",
|
||||
type=UserTypes.SERVICE_ACCOUNT,
|
||||
)
|
||||
token = Token.objects.create(
|
||||
user=user,
|
||||
identifier=identifier,
|
||||
intent=TokenIntents.INTENT_API,
|
||||
expiring=False,
|
||||
managed=f"goauthentik.io/sources/scim/{instance.pk}",
|
||||
)
|
||||
instance.token = token
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
class Meta:
|
||||
|
||||
model = SCIMSource
|
||||
fields = [
|
||||
"pk",
|
||||
"name",
|
||||
"slug",
|
||||
"enabled",
|
||||
"component",
|
||||
"verbose_name",
|
||||
"verbose_name_plural",
|
||||
"meta_model_name",
|
||||
"user_matching_mode",
|
||||
"managed",
|
||||
"user_path_template",
|
||||
"root_url",
|
||||
"token_obj",
|
||||
]
|
||||
|
||||
|
||||
class SCIMSourceViewSet(UsedByMixin, ModelViewSet):
|
||||
"""SCIMSource Viewset"""
|
||||
|
||||
queryset = SCIMSource.objects.all()
|
||||
serializer_class = SCIMSourceSerializer
|
||||
lookup_field = "slug"
|
||||
filterset_fields = ["name", "slug"]
|
||||
search_fields = ["name", "slug", "token__identifier", "token__user__username"]
|
||||
ordering = ["name"]
|
35
authentik/sources/scim/api/users.py
Normal file
35
authentik/sources/scim/api/users.py
Normal file
@ -0,0 +1,35 @@
|
||||
"""SCIMSourceUser API Views"""
|
||||
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.groups import GroupMemberSerializer
|
||||
from authentik.core.api.sources import SourceSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.sources.scim.models import SCIMSourceUser
|
||||
|
||||
|
||||
class SCIMSourceUserSerializer(SourceSerializer):
|
||||
"""SCIMSourceUser Serializer"""
|
||||
|
||||
user_obj = GroupMemberSerializer(source="user", read_only=True)
|
||||
|
||||
class Meta:
|
||||
|
||||
model = SCIMSourceUser
|
||||
fields = [
|
||||
"id",
|
||||
"user",
|
||||
"user_obj",
|
||||
"source",
|
||||
"attributes",
|
||||
]
|
||||
|
||||
|
||||
class SCIMSourceUserViewSet(UsedByMixin, ModelViewSet):
|
||||
"""SCIMSourceUser Viewset"""
|
||||
|
||||
queryset = SCIMSourceUser.objects.all().select_related("user")
|
||||
serializer_class = SCIMSourceUserSerializer
|
||||
filterset_fields = ["source__slug", "user__username", "user__id"]
|
||||
search_fields = ["source__slug", "user__username", "attributes"]
|
||||
ordering = ["user__username"]
|
12
authentik/sources/scim/apps.py
Normal file
12
authentik/sources/scim/apps.py
Normal 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/"
|
8
authentik/sources/scim/errors.py
Normal file
8
authentik/sources/scim/errors.py
Normal file
@ -0,0 +1,8 @@
|
||||
"""SCIM Errors"""
|
||||
|
||||
from authentik.lib.sentry import SentryIgnoredException
|
||||
|
||||
|
||||
class PatchError(SentryIgnoredException):
|
||||
"""Error raised within an atomic block when an error happened
|
||||
so nothing is saved"""
|
94
authentik/sources/scim/migrations/0001_initial.py
Normal file
94
authentik/sources/scim/migrations/0001_initial.py
Normal file
@ -0,0 +1,94 @@
|
||||
# Generated by Django 5.0.4 on 2024-04-07 14:34
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0033_alter_user_options"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
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(
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="authentik_core.token",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "SCIM Source",
|
||||
"verbose_name_plural": "SCIM Sources",
|
||||
},
|
||||
bases=("authentik_core.source",),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="SCIMSourceGroup",
|
||||
fields=[
|
||||
("id", models.TextField(primary_key=True, serialize=False)),
|
||||
("attributes", models.JSONField(default=dict)),
|
||||
(
|
||||
"group",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="authentik_core.group"
|
||||
),
|
||||
),
|
||||
(
|
||||
"source",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="authentik_sources_scim.scimsource",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"unique_together": {("id", "group", "source")},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="SCIMSourceUser",
|
||||
fields=[
|
||||
("id", models.TextField(primary_key=True, serialize=False)),
|
||||
("attributes", models.JSONField(default=dict)),
|
||||
(
|
||||
"source",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="authentik_sources_scim.scimsource",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"unique_together": {("id", "user", "source")},
|
||||
},
|
||||
),
|
||||
]
|
0
authentik/sources/scim/migrations/__init__.py
Normal file
0
authentik/sources/scim/migrations/__init__.py
Normal file
76
authentik/sources/scim/models.py
Normal file
76
authentik/sources/scim/models.py
Normal file
@ -0,0 +1,76 @@
|
||||
"""SCIM Source"""
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.serializers import BaseSerializer
|
||||
|
||||
from authentik.core.models import Group, Source, Token, User
|
||||
from authentik.lib.models import SerializerModel
|
||||
|
||||
|
||||
class SCIMSource(Source):
|
||||
"""System for Cross-domain Identity Management Source, allows for
|
||||
cross-system user provisioning"""
|
||||
|
||||
token = models.ForeignKey(Token, on_delete=models.CASCADE, null=True, default=None)
|
||||
|
||||
@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.sources import SCIMSourceSerializer
|
||||
|
||||
return SCIMSourceSerializer
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"SCIM Source {self.name}"
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _("SCIM Source")
|
||||
verbose_name_plural = _("SCIM Sources")
|
||||
|
||||
|
||||
class SCIMSourceUser(SerializerModel):
|
||||
"""Mapping of a user and source to a SCIM user ID"""
|
||||
|
||||
id = models.TextField(primary_key=True)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
source = models.ForeignKey(SCIMSource, on_delete=models.CASCADE)
|
||||
attributes = models.JSONField(default=dict)
|
||||
|
||||
@property
|
||||
def serializer(self) -> BaseSerializer:
|
||||
from authentik.sources.scim.api.users import SCIMSourceUserSerializer
|
||||
|
||||
return SCIMSourceUserSerializer
|
||||
|
||||
class Meta:
|
||||
unique_together = (("id", "user", "source"),)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"SCIM User {self.user.username} to {self.source.name}"
|
||||
|
||||
|
||||
class SCIMSourceGroup(SerializerModel):
|
||||
"""Mapping of a group and source to a SCIM user ID"""
|
||||
|
||||
id = models.TextField(primary_key=True)
|
||||
group = models.ForeignKey(Group, on_delete=models.CASCADE)
|
||||
source = models.ForeignKey(SCIMSource, on_delete=models.CASCADE)
|
||||
attributes = models.JSONField(default=dict)
|
||||
|
||||
@property
|
||||
def serializer(self) -> BaseSerializer:
|
||||
from authentik.sources.scim.api.groups import SCIMSourceGroupSerializer
|
||||
|
||||
return SCIMSourceGroupSerializer
|
||||
|
||||
class Meta:
|
||||
unique_together = (("id", "group", "source"),)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"SCIM Group {self.group.name} to {self.source.name}"
|
1796
authentik/sources/scim/schemas/schema.json
Normal file
1796
authentik/sources/scim/schemas/schema.json
Normal file
File diff suppressed because it is too large
Load Diff
0
authentik/sources/scim/tests/__init__.py
Normal file
0
authentik/sources/scim/tests/__init__.py
Normal file
87
authentik/sources/scim/tests/test_auth.py
Normal file
87
authentik/sources/scim/tests/test_auth.py
Normal file
@ -0,0 +1,87 @@
|
||||
"""Test SCIM Auth"""
|
||||
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import Token, TokenIntents
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.sources.scim.models import SCIMSource
|
||||
|
||||
|
||||
class TestSCIMAuth(APITestCase):
|
||||
"""Test SCIM Auth view"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.user = create_test_admin_user()
|
||||
self.token = Token.objects.create(
|
||||
user=self.user,
|
||||
identifier=generate_id(),
|
||||
intent=TokenIntents.INTENT_API,
|
||||
)
|
||||
self.token2 = Token.objects.create(
|
||||
user=self.user,
|
||||
identifier=generate_id(),
|
||||
intent=TokenIntents.INTENT_API,
|
||||
)
|
||||
self.token3 = Token.objects.create(
|
||||
user=self.user,
|
||||
identifier=generate_id(),
|
||||
intent=TokenIntents.INTENT_API,
|
||||
)
|
||||
self.source = SCIMSource.objects.create(
|
||||
name=generate_id(), slug=generate_id(), token=self.token
|
||||
)
|
||||
self.source2 = SCIMSource.objects.create(
|
||||
name=generate_id(), slug=generate_id(), token=self.token2
|
||||
)
|
||||
|
||||
def test_auth_ok(self):
|
||||
"""Test successful auth"""
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_sources_scim:v2-schema",
|
||||
kwargs={
|
||||
"source_slug": self.source.slug,
|
||||
},
|
||||
),
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.token.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_auth_missing(self):
|
||||
"""Test without header"""
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_sources_scim:v2-schema",
|
||||
kwargs={
|
||||
"source_slug": self.source.slug,
|
||||
},
|
||||
),
|
||||
)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_auth_wrong_token(self):
|
||||
"""Test with wrong token"""
|
||||
# Token for wrong source
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_sources_scim:v2-schema",
|
||||
kwargs={
|
||||
"source_slug": self.source.slug,
|
||||
},
|
||||
),
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.token2.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
# Token for no source
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_sources_scim:v2-schema",
|
||||
kwargs={
|
||||
"source_slug": self.source.slug,
|
||||
},
|
||||
),
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.token3.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 403)
|
65
authentik/sources/scim/tests/test_resource_types.py
Normal file
65
authentik/sources/scim/tests/test_resource_types.py
Normal file
@ -0,0 +1,65 @@
|
||||
"""Test SCIM ResourceTypes"""
|
||||
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import Token, TokenIntents
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.sources.scim.models import SCIMSource
|
||||
|
||||
|
||||
class TestSCIMResourceTypes(APITestCase):
|
||||
"""Test SCIM ResourceTypes view"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.user = create_test_admin_user()
|
||||
self.token = Token.objects.create(
|
||||
user=self.user,
|
||||
identifier=generate_id(),
|
||||
intent=TokenIntents.INTENT_API,
|
||||
)
|
||||
self.source = SCIMSource.objects.create(
|
||||
name=generate_id(), slug=generate_id(), token=self.token
|
||||
)
|
||||
|
||||
def test_resource_type(self):
|
||||
"""Test full resource type view"""
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_sources_scim:v2-resource-types",
|
||||
kwargs={
|
||||
"source_slug": self.source.slug,
|
||||
},
|
||||
),
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.token.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_resource_type_single(self):
|
||||
"""Test single resource type"""
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_sources_scim:v2-resource-types",
|
||||
kwargs={
|
||||
"source_slug": self.source.slug,
|
||||
"resource_type": "ServiceProviderConfig",
|
||||
},
|
||||
),
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.token.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_resource_type_single_404(self):
|
||||
"""Test single resource type (404"""
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_sources_scim:v2-resource-types",
|
||||
kwargs={
|
||||
"source_slug": self.source.slug,
|
||||
"resource_type": "foo",
|
||||
},
|
||||
),
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.token.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
65
authentik/sources/scim/tests/test_schemas.py
Normal file
65
authentik/sources/scim/tests/test_schemas.py
Normal file
@ -0,0 +1,65 @@
|
||||
"""Test SCIM Schema"""
|
||||
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import Token, TokenIntents
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.sources.scim.models import SCIMSource
|
||||
|
||||
|
||||
class TestSCIMSchemas(APITestCase):
|
||||
"""Test SCIM Schema view"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.user = create_test_admin_user()
|
||||
self.token = Token.objects.create(
|
||||
user=self.user,
|
||||
identifier=generate_id(),
|
||||
intent=TokenIntents.INTENT_API,
|
||||
)
|
||||
self.source = SCIMSource.objects.create(
|
||||
name=generate_id(), slug=generate_id(), token=self.token
|
||||
)
|
||||
|
||||
def test_schema(self):
|
||||
"""Test full schema view"""
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_sources_scim:v2-schema",
|
||||
kwargs={
|
||||
"source_slug": self.source.slug,
|
||||
},
|
||||
),
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.token.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_schema_single(self):
|
||||
"""Test single schema"""
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_sources_scim:v2-schema",
|
||||
kwargs={
|
||||
"source_slug": self.source.slug,
|
||||
"schema_uri": "urn:ietf:params:scim:schemas:core:2.0:Meta",
|
||||
},
|
||||
),
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.token.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_schema_single_404(self):
|
||||
"""Test single schema (404"""
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_sources_scim:v2-schema",
|
||||
kwargs={
|
||||
"source_slug": self.source.slug,
|
||||
"schema_uri": "foo",
|
||||
},
|
||||
),
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.token.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
37
authentik/sources/scim/tests/test_service_provider_config.py
Normal file
37
authentik/sources/scim/tests/test_service_provider_config.py
Normal file
@ -0,0 +1,37 @@
|
||||
"""Test SCIM ServiceProviderConfig"""
|
||||
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import Token, TokenIntents
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.sources.scim.models import SCIMSource
|
||||
|
||||
|
||||
class TestSCIMServiceProviderConfig(APITestCase):
|
||||
"""Test SCIM ServiceProviderConfig view"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.user = create_test_admin_user()
|
||||
self.token = Token.objects.create(
|
||||
user=self.user,
|
||||
identifier=generate_id(),
|
||||
intent=TokenIntents.INTENT_API,
|
||||
)
|
||||
self.source = SCIMSource.objects.create(
|
||||
name=generate_id(), slug=generate_id(), token=self.token
|
||||
)
|
||||
|
||||
def test_config(self):
|
||||
"""Test full config view"""
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_sources_scim:v2-service-provider-config",
|
||||
kwargs={
|
||||
"source_slug": self.source.slug,
|
||||
},
|
||||
),
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.token.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
90
authentik/sources/scim/tests/test_users.py
Normal file
90
authentik/sources/scim/tests/test_users.py
Normal file
@ -0,0 +1,90 @@
|
||||
"""Test SCIM User"""
|
||||
|
||||
from json import dumps
|
||||
from uuid import uuid4
|
||||
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import Token, TokenIntents
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
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.views.v2.base import SCIM_CONTENT_TYPE
|
||||
|
||||
|
||||
class TestSCIMUsers(APITestCase):
|
||||
"""Test SCIM User view"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.user = create_test_admin_user()
|
||||
self.token = Token.objects.create(
|
||||
user=self.user,
|
||||
identifier=generate_id(),
|
||||
intent=TokenIntents.INTENT_API,
|
||||
)
|
||||
self.source = SCIMSource.objects.create(
|
||||
name=generate_id(), slug=generate_id(), token=self.token
|
||||
)
|
||||
|
||||
def test_user_list(self):
|
||||
"""Test full user list"""
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_sources_scim:v2-users",
|
||||
kwargs={
|
||||
"source_slug": self.source.slug,
|
||||
},
|
||||
),
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.token.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_user_list_single(self):
|
||||
"""Test full user list (single user)"""
|
||||
SCIMSourceUser.objects.create(
|
||||
source=self.source,
|
||||
user=self.user,
|
||||
id=str(uuid4()),
|
||||
)
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_sources_scim:v2-users",
|
||||
kwargs={
|
||||
"source_slug": self.source.slug,
|
||||
"user_id": str(self.user.uuid),
|
||||
},
|
||||
),
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.token.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
SCIMUserSchema.model_validate_json(response.content, strict=True)
|
||||
|
||||
def test_user_create(self):
|
||||
"""Test user create"""
|
||||
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": self.user.email,
|
||||
}
|
||||
],
|
||||
}
|
||||
),
|
||||
content_type=SCIM_CONTENT_TYPE,
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.token.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertTrue(SCIMSourceUser.objects.filter(source=self.source, id=ext_id).exists())
|
74
authentik/sources/scim/urls.py
Normal file
74
authentik/sources/scim/urls.py
Normal file
@ -0,0 +1,74 @@
|
||||
"""SCIM URLs"""
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from authentik.sources.scim.api.groups import SCIMSourceGroupViewSet
|
||||
from authentik.sources.scim.api.sources import SCIMSourceViewSet
|
||||
from authentik.sources.scim.api.users import SCIMSourceUserViewSet
|
||||
from authentik.sources.scim.views.v2 import (
|
||||
base,
|
||||
groups,
|
||||
resource_types,
|
||||
schemas,
|
||||
service_provider_config,
|
||||
users,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"<slug:source_slug>/v2",
|
||||
base.SCIMRootView.as_view(),
|
||||
name="v2-root",
|
||||
),
|
||||
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",
|
||||
),
|
||||
]
|
||||
|
||||
api_urlpatterns = [
|
||||
("sources/scim", SCIMSourceViewSet),
|
||||
("sources/scim_users", SCIMSourceUserViewSet),
|
||||
("sources/scim_groups", SCIMSourceGroupViewSet),
|
||||
]
|
0
authentik/sources/scim/views/__init__.py
Normal file
0
authentik/sources/scim/views/__init__.py
Normal file
0
authentik/sources/scim/views/v2/__init__.py
Normal file
0
authentik/sources/scim/views/v2/__init__.py
Normal file
55
authentik/sources/scim/views/v2/auth.py
Normal file
55
authentik/sources/scim/views/v2/auth.py
Normal file
@ -0,0 +1,55 @@
|
||||
"""SCIM Token auth"""
|
||||
|
||||
from base64 import b64decode
|
||||
from typing import Any
|
||||
|
||||
from django.conf import settings
|
||||
from rest_framework.authentication import BaseAuthentication, get_authorization_header
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from authentik.core.models import Token, TokenIntents, User
|
||||
from authentik.sources.scim.models import SCIMSource
|
||||
|
||||
|
||||
class SCIMTokenAuth(BaseAuthentication):
|
||||
"""SCIM Token auth"""
|
||||
|
||||
def __init__(self, view: APIView) -> None:
|
||||
super().__init__()
|
||||
self.view = view
|
||||
|
||||
def legacy(self, key: str, source_slug: str) -> Token | None: # pragma: no cover
|
||||
"""Legacy HTTP-Basic auth for testing"""
|
||||
if not settings.TEST and not settings.DEBUG:
|
||||
return None
|
||||
_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) -> Token | None:
|
||||
"""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
|
||||
source: SCIMSource = token.scimsource_set.first()
|
||||
if not source:
|
||||
return None
|
||||
if source.slug != source_slug:
|
||||
return None
|
||||
self.view.source = source
|
||||
return token
|
||||
|
||||
def authenticate(self, request: Request) -> 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)
|
||||
if not token:
|
||||
return None
|
||||
return (token.user, token)
|
120
authentik/sources/scim/views/v2/base.py
Normal file
120
authentik/sources/scim/views/v2/base.py
Normal file
@ -0,0 +1,120 @@
|
||||
"""SCIM Utils"""
|
||||
|
||||
from typing import Any
|
||||
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.http import HttpRequest
|
||||
from django.urls import resolve
|
||||
from rest_framework.parsers import JSONParser
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.renderers import JSONRenderer
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from scim2_filter_parser.transpilers.django_q_object import get_query
|
||||
from structlog import BoundLogger
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import Group, User
|
||||
from authentik.sources.scim.models import SCIMSource
|
||||
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"""
|
||||
|
||||
source: SCIMSource
|
||||
logger: BoundLogger
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
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)
|
||||
|
||||
def get_authenticators(self):
|
||||
return [SCIMTokenAuth(self)]
|
||||
|
||||
def patch_resolve_value(self, raw_value: dict) -> User | Group | None:
|
||||
"""Attempt to resolve a raw `value` attribute of a patch operation into
|
||||
a database model"""
|
||||
model = User
|
||||
query = {}
|
||||
if "$ref" in raw_value:
|
||||
url = urlparse(raw_value["$ref"])
|
||||
if match := resolve(url.path):
|
||||
if match.url_name == "v2-users":
|
||||
model = User
|
||||
query = {"pk": int(match.kwargs["user_id"])}
|
||||
elif "type" in raw_value:
|
||||
match raw_value["type"]:
|
||||
case "User":
|
||||
model = User
|
||||
query = {"pk": int(raw_value["value"])}
|
||||
case "Group":
|
||||
model = Group
|
||||
else:
|
||||
return None
|
||||
return model.objects.filter(**query).first()
|
||||
|
||||
def filter_parse(self, request: Request):
|
||||
"""Parse the path of a Patch Operation"""
|
||||
path = request.query_params.get("filter")
|
||||
if not path:
|
||||
return Q()
|
||||
attr_map = {}
|
||||
if self.model == User:
|
||||
attr_map = {
|
||||
("userName", None, None): "user__username",
|
||||
("active", None, None): "user__is_active",
|
||||
("name", "familyName", None): "attributes__familyName",
|
||||
}
|
||||
elif self.model == Group:
|
||||
attr_map = {
|
||||
("displayName", None, None): "group__name",
|
||||
("members", None, None): "group__users",
|
||||
}
|
||||
return get_query(
|
||||
path,
|
||||
attr_map,
|
||||
)
|
||||
|
||||
def paginate_query(self, query: QuerySet) -> Page:
|
||||
per_page = 50
|
||||
start_index = 1
|
||||
try:
|
||||
per_page = int(settings.REST_FRAMEWORK["PAGE_SIZE"])
|
||||
start_index = int(self.request.query_params.get("startIndex", 1))
|
||||
except ValueError:
|
||||
pass
|
||||
paginator = Paginator(query, per_page=per_page)
|
||||
page = paginator.page(int(max(start_index / per_page, 1)))
|
||||
return page
|
||||
|
||||
|
||||
class SCIMRootView(SCIMView):
|
||||
"""Root SCIM View"""
|
||||
|
||||
def dispatch(self, request: Request, *args, **kwargs) -> Response:
|
||||
return Response({"message": "Use this base-URL with a SCIM-compatible system."})
|
141
authentik/sources/scim/views/v2/groups.py
Normal file
141
authentik/sources/scim/views/v2/groups.py
Normal file
@ -0,0 +1,141 @@
|
||||
"""SCIM Group Views"""
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
from django.db.models import Q
|
||||
from django.db.transaction import atomic
|
||||
from django.http import Http404, QueryDict
|
||||
from django.urls import reverse
|
||||
from pydantic import ValidationError as PydanticValidationError
|
||||
from pydanticscim.group import GroupMember
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
|
||||
from authentik.core.models import Group, User
|
||||
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
|
||||
|
||||
|
||||
class GroupsView(SCIMView):
|
||||
"""SCIM Group view"""
|
||||
|
||||
model = Group
|
||||
|
||||
def group_to_scim(self, scim_group: SCIMSourceGroup) -> dict:
|
||||
"""Convert Group to SCIM data"""
|
||||
payload = SCIMGroupModel(
|
||||
id=str(scim_group.group.pk),
|
||||
externalId=scim_group.id,
|
||||
displayName=scim_group.group.name,
|
||||
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(scim_group.group.pk),
|
||||
},
|
||||
)
|
||||
),
|
||||
},
|
||||
)
|
||||
return payload.model_dump(
|
||||
mode="json",
|
||||
exclude_unset=True,
|
||||
)
|
||||
|
||||
def get(self, request: Request, group_id: str | None = None, **kwargs) -> Response:
|
||||
"""List Group handler"""
|
||||
if group_id:
|
||||
connection = (
|
||||
SCIMSourceGroup.objects.filter(source=self.source, group__group_uuid=group_id)
|
||||
.select_related("group")
|
||||
.first()
|
||||
)
|
||||
if not connection:
|
||||
raise Http404
|
||||
return Response(self.group_to_scim(connection))
|
||||
connections = (
|
||||
SCIMSourceGroup.objects.filter(source=self.source)
|
||||
.select_related("group")
|
||||
.order_by("pk")
|
||||
)
|
||||
connections = connections.filter(self.filter_parse(request))
|
||||
page = self.paginate_query(connections)
|
||||
return Response(
|
||||
{
|
||||
"totalResults": page.paginator.count,
|
||||
"itemsPerPage": page.paginator.per_page,
|
||||
"startIndex": page.start_index(),
|
||||
"schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
|
||||
"Resources": [self.group_to_scim(connection) for connection in page],
|
||||
}
|
||||
)
|
||||
|
||||
@atomic
|
||||
def update_group(self, connection: SCIMSourceGroup | None, data: QueryDict):
|
||||
"""Partial update a group"""
|
||||
group = connection.group if connection else Group()
|
||||
if "displayName" in data:
|
||||
group.name = data.get("displayName")
|
||||
if group.name == "":
|
||||
raise ValidationError("Invalid group")
|
||||
group.save()
|
||||
if "members" in data:
|
||||
query = Q()
|
||||
for _member in data.get("members", []):
|
||||
try:
|
||||
member = GroupMember.model_validate(_member)
|
||||
except PydanticValidationError as exc:
|
||||
self.logger.warning("Invalid group member", exc=exc)
|
||||
continue
|
||||
query |= Q(uuid=member.value)
|
||||
group.users.set(User.objects.filter(query))
|
||||
if not connection:
|
||||
connection, _ = SCIMSourceGroup.objects.get_or_create(
|
||||
source=self.source,
|
||||
group=group,
|
||||
attributes=data,
|
||||
id=data.get("externalId") or str(uuid4()),
|
||||
)
|
||||
else:
|
||||
connection.attributes = data
|
||||
connection.save()
|
||||
return connection
|
||||
|
||||
def post(self, request: Request, **kwargs) -> Response:
|
||||
"""Create group handler"""
|
||||
connection = SCIMSourceGroup.objects.filter(
|
||||
source=self.source,
|
||||
group__group_uuid=request.data.get("id"),
|
||||
).first()
|
||||
if connection:
|
||||
self.logger.debug("Found existing group")
|
||||
return Response(status=409)
|
||||
connection = self.update_group(None, request.data)
|
||||
return Response(self.group_to_scim(connection), status=201)
|
||||
|
||||
def put(self, request: Request, group_id: str, **kwargs) -> Response:
|
||||
"""Update group handler"""
|
||||
connection = SCIMSourceGroup.objects.filter(
|
||||
source=self.source, group__group_uuid=group_id
|
||||
).first()
|
||||
if not connection:
|
||||
raise Http404
|
||||
connection = self.update_group(connection, request.data)
|
||||
return Response(self.group_to_scim(connection), status=200)
|
||||
|
||||
@atomic
|
||||
def delete(self, request: Request, group_id: str, **kwargs) -> Response:
|
||||
"""Delete group handler"""
|
||||
connection = SCIMSourceGroup.objects.filter(
|
||||
source=self.source, group__group_uuid=group_id
|
||||
).first()
|
||||
if not connection:
|
||||
raise Http404
|
||||
connection.group.delete()
|
||||
connection.delete()
|
||||
return Response(status=204)
|
150
authentik/sources/scim/views/v2/resource_types.py
Normal file
150
authentik/sources/scim/views/v2/resource_types.py
Normal file
@ -0,0 +1,150 @@
|
||||
"""SCIM Meta views"""
|
||||
|
||||
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: str | None = 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,
|
||||
}
|
||||
)
|
52
authentik/sources/scim/views/v2/schemas.py
Normal file
52
authentik/sources/scim/views/v2/schemas.py
Normal file
@ -0,0 +1,52 @@
|
||||
"""Schema Views"""
|
||||
|
||||
from json import loads
|
||||
|
||||
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", 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: str | None = 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,
|
||||
}
|
||||
)
|
46
authentik/sources/scim/views/v2/service_provider_config.py
Normal file
46
authentik/sources/scim/views/v2/service_provider_config.py
Normal file
@ -0,0 +1,46 @@
|
||||
"""SCIM Meta views"""
|
||||
|
||||
from django.conf import settings
|
||||
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"""
|
||||
auth_schemas = [
|
||||
{
|
||||
"type": "oauthbearertoken",
|
||||
"name": "OAuth Bearer Token",
|
||||
"description": "Authentication scheme using the OAuth Bearer Token Standard",
|
||||
"primary": True,
|
||||
},
|
||||
]
|
||||
if settings.TEST or settings.DEBUG:
|
||||
auth_schemas.append(
|
||||
{
|
||||
"type": "httpbasic",
|
||||
"name": "HTTP Basic",
|
||||
"description": "Authentication scheme using HTTP Basic authorization",
|
||||
},
|
||||
)
|
||||
return Response(
|
||||
{
|
||||
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
|
||||
"authenticationSchemes": auth_schemas,
|
||||
"patch": {"supported": False},
|
||||
"bulk": {"supported": False, "maxOperations": 0, "maxPayloadSize": 0},
|
||||
"filter": {
|
||||
"supported": True,
|
||||
"maxResults": int(settings.REST_FRAMEWORK["PAGE_SIZE"]),
|
||||
},
|
||||
"changePassword": {"supported": False},
|
||||
"sort": {"supported": False},
|
||||
"etag": {"supported": False},
|
||||
}
|
||||
)
|
154
authentik/sources/scim/views/v2/users.py
Normal file
154
authentik/sources/scim/views/v2/users.py
Normal file
@ -0,0 +1,154 @@
|
||||
"""SCIM User Views"""
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
from django.db.transaction import atomic
|
||||
from django.http import Http404, QueryDict
|
||||
from django.urls import reverse
|
||||
from pydanticscim.user import Email, EmailKind, Name
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
|
||||
from authentik.core.models import User
|
||||
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
|
||||
|
||||
|
||||
class UsersView(SCIMView):
|
||||
"""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(
|
||||
id=str(scim_user.user.uuid),
|
||||
externalId=scim_user.id,
|
||||
userName=scim_user.user.username,
|
||||
name=Name(
|
||||
formatted=scim_user.user.name,
|
||||
),
|
||||
displayName=scim_user.user.name,
|
||||
active=scim_user.user.is_active,
|
||||
emails=(
|
||||
[Email(value=scim_user.user.email, type=EmailKind.work, primary=True)]
|
||||
if scim_user.user.email
|
||||
else []
|
||||
),
|
||||
meta={
|
||||
"resourceType": "User",
|
||||
"created": scim_user.user.date_joined,
|
||||
# TODO: use events to find last edit?
|
||||
"lastModified": scim_user.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(scim_user.user.uuid),
|
||||
},
|
||||
)
|
||||
),
|
||||
},
|
||||
)
|
||||
final_payload = payload.model_dump(
|
||||
mode="json",
|
||||
exclude_unset=True,
|
||||
)
|
||||
final_payload.update(scim_user.attributes)
|
||||
return final_payload
|
||||
|
||||
def get(self, request: Request, user_id: str | None = None, **kwargs) -> Response:
|
||||
"""List User handler"""
|
||||
if user_id:
|
||||
connection = (
|
||||
SCIMSourceUser.objects.filter(source=self.source, user__uuid=user_id)
|
||||
.select_related("user")
|
||||
.first()
|
||||
)
|
||||
if not connection:
|
||||
raise Http404
|
||||
return Response(self.user_to_scim(connection))
|
||||
connections = (
|
||||
SCIMSourceUser.objects.filter(source=self.source).select_related("user").order_by("pk")
|
||||
)
|
||||
connections = connections.filter(self.filter_parse(request))
|
||||
page = self.paginate_query(connections)
|
||||
return Response(
|
||||
{
|
||||
"totalResults": page.paginator.count,
|
||||
"itemsPerPage": page.paginator.per_page,
|
||||
"startIndex": page.start_index(),
|
||||
"schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
|
||||
"Resources": [self.user_to_scim(connection) for connection in page],
|
||||
}
|
||||
)
|
||||
|
||||
@atomic
|
||||
def update_user(self, connection: SCIMSourceUser | None, data: QueryDict):
|
||||
"""Partial update a user"""
|
||||
user = connection.user if connection else 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 == "":
|
||||
raise ValidationError("Invalid user")
|
||||
user.save()
|
||||
if not connection:
|
||||
connection, _ = SCIMSourceUser.objects.get_or_create(
|
||||
source=self.source,
|
||||
user=user,
|
||||
attributes=data,
|
||||
id=data.get("externalId") or str(uuid4()),
|
||||
)
|
||||
else:
|
||||
connection.attributes = data
|
||||
connection.save()
|
||||
return connection
|
||||
|
||||
def post(self, request: Request, **kwargs) -> Response:
|
||||
"""Create user handler"""
|
||||
connection = SCIMSourceUser.objects.filter(
|
||||
source=self.source,
|
||||
user__uuid=request.data.get("id"),
|
||||
).first()
|
||||
if connection:
|
||||
self.logger.debug("Found existing user")
|
||||
return Response(status=409)
|
||||
connection = self.update_user(None, request.data)
|
||||
return Response(self.user_to_scim(connection), status=201)
|
||||
|
||||
def put(self, request: Request, user_id: str, **kwargs) -> Response:
|
||||
"""Update user handler"""
|
||||
connection = SCIMSourceUser.objects.filter(source=self.source, user__uuid=user_id).first()
|
||||
if not connection:
|
||||
raise Http404
|
||||
self.update_user(connection, request.data)
|
||||
return Response(self.user_to_scim(connection), status=200)
|
||||
|
||||
@atomic
|
||||
def delete(self, request: Request, user_id: str, **kwargs) -> Response:
|
||||
"""Delete user handler"""
|
||||
connection = SCIMSourceUser.objects.filter(source=self.source, user__uuid=user_id).first()
|
||||
if not connection:
|
||||
raise Http404
|
||||
connection.user.delete()
|
||||
connection.delete()
|
||||
return Response(status=204)
|
File diff suppressed because one or more lines are too long
@ -1225,6 +1225,43 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"model",
|
||||
"identifiers"
|
||||
],
|
||||
"properties": {
|
||||
"model": {
|
||||
"const": "authentik_sources_scim.scimsource"
|
||||
},
|
||||
"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.scimsource"
|
||||
},
|
||||
"identifiers": {
|
||||
"$ref": "#/$defs/model_authentik_sources_scim.scimsource"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
@ -3274,6 +3311,7 @@
|
||||
"authentik.sources.oauth",
|
||||
"authentik.sources.plex",
|
||||
"authentik.sources.saml",
|
||||
"authentik.sources.scim",
|
||||
"authentik.stages.authenticator",
|
||||
"authentik.stages.authenticator_duo",
|
||||
"authentik.stages.authenticator_sms",
|
||||
@ -3345,6 +3383,7 @@
|
||||
"authentik_sources_plex.plexsourceconnection",
|
||||
"authentik_sources_saml.samlsource",
|
||||
"authentik_sources_saml.usersamlsourceconnection",
|
||||
"authentik_sources_scim.scimsource",
|
||||
"authentik_stages_authenticator_duo.authenticatorduostage",
|
||||
"authentik_stages_authenticator_duo.duodevice",
|
||||
"authentik_stages_authenticator_sms.authenticatorsmsstage",
|
||||
@ -4929,6 +4968,52 @@
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"model_authentik_sources_scim.scimsource": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Name",
|
||||
"description": "Source's display Name."
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"maxLength": 50,
|
||||
"minLength": 1,
|
||||
"pattern": "^[-a-zA-Z0-9_]+$",
|
||||
"title": "Slug",
|
||||
"description": "Internal source name, used in URLs."
|
||||
},
|
||||
"enabled": {
|
||||
"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_path_template": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "User path template"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Icon"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"model_authentik_stages_authenticator_duo.authenticatorduostage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
2
go.mod
2
go.mod
@ -30,7 +30,7 @@ require (
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/wwt/guac v1.3.2
|
||||
goauthentik.io/api/v3 v3.2024022.8
|
||||
goauthentik.io/api/v3 v3.2024022.11
|
||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||
golang.org/x/oauth2 v0.19.0
|
||||
golang.org/x/sync v0.7.0
|
||||
|
4
go.sum
4
go.sum
@ -294,8 +294,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
|
||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
|
||||
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
|
||||
goauthentik.io/api/v3 v3.2024022.8 h1:bHKUZgQXf4/qjL4VkITc/HK0pjMtX9X5Dlob8yTg7K4=
|
||||
goauthentik.io/api/v3 v3.2024022.8/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||
goauthentik.io/api/v3 v3.2024022.11 h1:MlsaBwyMM9NtDvZcoaWvuNznPHXA0a5olnDLyr24REA=
|
||||
goauthentik.io/api/v3 v3.2024022.11/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
|
@ -113,7 +113,7 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||
errs.Go(func() error {
|
||||
if flags.CanSearch {
|
||||
uapisp := sentry.StartSpan(errCtx, "authentik.providers.ldap.search.api_user")
|
||||
searchReq, skip := utils.ParseFilterForUser(c.CoreApi.CoreUsersList(uapisp.Context()), parsedFilter, false)
|
||||
searchReq, skip := utils.ParseFilterForUser(c.CoreApi.CoreUsersList(uapisp.Context()).IncludeGroups(true), parsedFilter, false)
|
||||
|
||||
if skip {
|
||||
req.Log().Trace("Skip backend request")
|
||||
@ -150,7 +150,7 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||
if needGroups {
|
||||
errs.Go(func() error {
|
||||
gapisp := sentry.StartSpan(errCtx, "authentik.providers.ldap.search.api_group")
|
||||
searchReq, skip := utils.ParseFilterForGroup(c.CoreApi.CoreGroupsList(gapisp.Context()), parsedFilter, false)
|
||||
searchReq, skip := utils.ParseFilterForGroup(c.CoreApi.CoreGroupsList(gapisp.Context()).IncludeUsers(true), parsedFilter, false)
|
||||
if skip {
|
||||
req.Log().Trace("Skip backend request")
|
||||
return nil
|
||||
|
@ -38,8 +38,8 @@ func NewMemorySearcher(si server.LDAPServerInstance) *MemorySearcher {
|
||||
ds: direct.NewDirectSearcher(si),
|
||||
}
|
||||
ms.log.Debug("initialised memory searcher")
|
||||
ms.users = paginator.FetchUsers(ms.si.GetAPIClient().CoreApi.CoreUsersList(context.TODO()))
|
||||
ms.groups = paginator.FetchGroups(ms.si.GetAPIClient().CoreApi.CoreGroupsList(context.TODO()))
|
||||
ms.users = paginator.FetchUsers(ms.si.GetAPIClient().CoreApi.CoreUsersList(context.TODO()).IncludeGroups(true))
|
||||
ms.groups = paginator.FetchGroups(ms.si.GetAPIClient().CoreApi.CoreGroupsList(context.TODO()).IncludeUsers(true))
|
||||
return ms
|
||||
}
|
||||
|
||||
|
@ -40,7 +40,7 @@ bind = f"unix://{str(_tmp.joinpath('authentik-core.sock'))}"
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "authentik.root.settings")
|
||||
os.environ.setdefault("PROMETHEUS_MULTIPROC_DIR", prometheus_tmp_dir)
|
||||
|
||||
preload = True
|
||||
preload_app = True
|
||||
|
||||
max_requests = 1000
|
||||
max_requests_jitter = 50
|
||||
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-04-12 00:07+0000\n"
|
||||
"POT-Creation-Date: 2024-04-16 00:07+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -119,6 +119,14 @@ msgstr ""
|
||||
msgid "Groups"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Add user to group"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Remove user from group"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "User's display name."
|
||||
msgstr ""
|
||||
@ -2075,6 +2083,14 @@ msgstr ""
|
||||
msgid "User SAML Source Connections"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/sources/scim/models.py
|
||||
msgid "SCIM Source"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/sources/scim/models.py
|
||||
msgid "SCIM Sources"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/authenticator_duo/models.py
|
||||
msgid "Duo Authenticator Setup Stage"
|
||||
msgstr ""
|
||||
|
108
poetry.lock
generated
108
poetry.lock
generated
@ -392,33 +392,33 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "24.3.0"
|
||||
version = "24.4.0"
|
||||
description = "The uncompromising code formatter."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "black-24.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395"},
|
||||
{file = "black-24.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995"},
|
||||
{file = "black-24.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7"},
|
||||
{file = "black-24.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0"},
|
||||
{file = "black-24.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9"},
|
||||
{file = "black-24.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597"},
|
||||
{file = "black-24.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d"},
|
||||
{file = "black-24.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5"},
|
||||
{file = "black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f"},
|
||||
{file = "black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11"},
|
||||
{file = "black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4"},
|
||||
{file = "black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5"},
|
||||
{file = "black-24.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837"},
|
||||
{file = "black-24.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd"},
|
||||
{file = "black-24.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213"},
|
||||
{file = "black-24.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959"},
|
||||
{file = "black-24.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb"},
|
||||
{file = "black-24.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7"},
|
||||
{file = "black-24.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7"},
|
||||
{file = "black-24.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f"},
|
||||
{file = "black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93"},
|
||||
{file = "black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f"},
|
||||
{file = "black-24.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6ad001a9ddd9b8dfd1b434d566be39b1cd502802c8d38bbb1ba612afda2ef436"},
|
||||
{file = "black-24.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3a3a092b8b756c643fe45f4624dbd5a389f770a4ac294cf4d0fce6af86addaf"},
|
||||
{file = "black-24.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dae79397f367ac8d7adb6c779813328f6d690943f64b32983e896bcccd18cbad"},
|
||||
{file = "black-24.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:71d998b73c957444fb7c52096c3843875f4b6b47a54972598741fe9a7f737fcb"},
|
||||
{file = "black-24.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8e5537f456a22cf5cfcb2707803431d2feeb82ab3748ade280d6ccd0b40ed2e8"},
|
||||
{file = "black-24.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:64e60a7edd71fd542a10a9643bf369bfd2644de95ec71e86790b063aa02ff745"},
|
||||
{file = "black-24.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cd5b4f76056cecce3e69b0d4c228326d2595f506797f40b9233424e2524c070"},
|
||||
{file = "black-24.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:64578cf99b6b46a6301bc28bdb89f9d6f9b592b1c5837818a177c98525dbe397"},
|
||||
{file = "black-24.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f95cece33329dc4aa3b0e1a771c41075812e46cf3d6e3f1dfe3d91ff09826ed2"},
|
||||
{file = "black-24.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4396ca365a4310beef84d446ca5016f671b10f07abdba3e4e4304218d2c71d33"},
|
||||
{file = "black-24.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44d99dfdf37a2a00a6f7a8dcbd19edf361d056ee51093b2445de7ca09adac965"},
|
||||
{file = "black-24.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:21f9407063ec71c5580b8ad975653c66508d6a9f57bd008bb8691d273705adcd"},
|
||||
{file = "black-24.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:652e55bb722ca026299eb74e53880ee2315b181dfdd44dca98e43448620ddec1"},
|
||||
{file = "black-24.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7f2966b9b2b3b7104fca9d75b2ee856fe3fdd7ed9e47c753a4bb1a675f2caab8"},
|
||||
{file = "black-24.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bb9ca06e556a09f7f7177bc7cb604e5ed2d2df1e9119e4f7d2f1f7071c32e5d"},
|
||||
{file = "black-24.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:d4e71cdebdc8efeb6deaf5f2deb28325f8614d48426bed118ecc2dcaefb9ebf3"},
|
||||
{file = "black-24.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6644f97a7ef6f401a150cca551a1ff97e03c25d8519ee0bbc9b0058772882665"},
|
||||
{file = "black-24.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:75a2d0b4f5eb81f7eebc31f788f9830a6ce10a68c91fbe0fade34fff7a2836e6"},
|
||||
{file = "black-24.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb949f56a63c5e134dfdca12091e98ffb5fd446293ebae123d10fc1abad00b9e"},
|
||||
{file = "black-24.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:7852b05d02b5b9a8c893ab95863ef8986e4dda29af80bbbda94d7aee1abf8702"},
|
||||
{file = "black-24.4.0-py3-none-any.whl", hash = "sha256:74eb9b5420e26b42c00a3ff470dc0cd144b80a766128b1771d07643165e08d0e"},
|
||||
{file = "black-24.4.0.tar.gz", hash = "sha256:f07b69fda20578367eaebbd670ff8fc653ab181e1ff95d84497f9fa20e7d0641"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -1289,22 +1289,23 @@ djangorestframework = "*"
|
||||
|
||||
[[package]]
|
||||
name = "dnspython"
|
||||
version = "2.4.2"
|
||||
version = "2.6.1"
|
||||
description = "DNS toolkit"
|
||||
optional = false
|
||||
python-versions = ">=3.8,<4.0"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "dnspython-2.4.2-py3-none-any.whl", hash = "sha256:57c6fbaaeaaf39c891292012060beb141791735dbb4004798328fc2c467402d8"},
|
||||
{file = "dnspython-2.4.2.tar.gz", hash = "sha256:8dcfae8c7460a2f84b4072e26f1c9f4101ca20c071649cb7c34e8b6a93d58984"},
|
||||
{file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"},
|
||||
{file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dnssec = ["cryptography (>=2.6,<42.0)"]
|
||||
doh = ["h2 (>=4.1.0)", "httpcore (>=0.17.3)", "httpx (>=0.24.1)"]
|
||||
doq = ["aioquic (>=0.9.20)"]
|
||||
idna = ["idna (>=2.1,<4.0)"]
|
||||
trio = ["trio (>=0.14,<0.23)"]
|
||||
wmi = ["wmi (>=1.5.1,<2.0.0)"]
|
||||
dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "sphinx (>=7.2.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"]
|
||||
dnssec = ["cryptography (>=41)"]
|
||||
doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"]
|
||||
doq = ["aioquic (>=0.9.25)"]
|
||||
idna = ["idna (>=3.6)"]
|
||||
trio = ["trio (>=0.23)"]
|
||||
wmi = ["wmi (>=1.5.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "docker"
|
||||
@ -3565,6 +3566,23 @@ botocore = ">=1.33.2,<2.0a.0"
|
||||
[package.extras]
|
||||
crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "scim2-filter-parser"
|
||||
version = "0.5.0"
|
||||
description = "A customizable parser/transpiler for SCIM2.0 filters."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "scim2_filter_parser-0.5.0-py3-none-any.whl", hash = "sha256:4aca1b3b64655dc038a973a9659056a103a919fb0218614e36bf19d3b5de5b48"},
|
||||
{file = "scim2_filter_parser-0.5.0.tar.gz", hash = "sha256:104c72e6faeb9a6b873950f66b0e3b69134fb19debf67e1d3714e91a6dafd8af"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
sly = "0.5"
|
||||
|
||||
[package.extras]
|
||||
django-query = ["django (>=3.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "selenium"
|
||||
version = "4.19.0"
|
||||
@ -3781,6 +3799,17 @@ files = [
|
||||
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sly"
|
||||
version = "0.5"
|
||||
description = "\"SLY - Sly Lex Yacc\""
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "sly-0.5-py3-none-any.whl", hash = "sha256:20485483259eec7f6ba85ff4d2e96a4e50c6621902667fc2695cc8bc2a3e5133"},
|
||||
{file = "sly-0.5.tar.gz", hash = "sha256:251d42015e8507158aec2164f06035df4a82b0314ce6450f457d7125e7649024"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.0"
|
||||
@ -3805,19 +3834,18 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlparse"
|
||||
version = "0.4.4"
|
||||
version = "0.5.0"
|
||||
description = "A non-validating SQL parser."
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "sqlparse-0.4.4-py3-none-any.whl", hash = "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3"},
|
||||
{file = "sqlparse-0.4.4.tar.gz", hash = "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c"},
|
||||
{file = "sqlparse-0.5.0-py3-none-any.whl", hash = "sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663"},
|
||||
{file = "sqlparse-0.5.0.tar.gz", hash = "sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["build", "flake8"]
|
||||
dev = ["build", "hatch"]
|
||||
doc = ["sphinx"]
|
||||
test = ["pytest", "pytest-cov"]
|
||||
|
||||
[[package]]
|
||||
name = "stevedore"
|
||||
@ -4653,4 +4681,4 @@ files = [
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "~3.12"
|
||||
content-hash = "4544b2a0b0065aa9e13d9a3b5a951fb5212921fe72f0fe259069e2e9205e9830"
|
||||
content-hash = "a5774b4e09217805c887700b8a0f457a39c7af40ca59823f00c1f6e8678469e1"
|
||||
|
@ -112,6 +112,7 @@ fido2 = "*"
|
||||
flower = "*"
|
||||
geoip2 = "*"
|
||||
gunicorn = "*"
|
||||
jsonpatch = "*"
|
||||
kubernetes = "*"
|
||||
ldap3 = "*"
|
||||
lxml = [
|
||||
@ -120,7 +121,6 @@ lxml = [
|
||||
# 4.9.x works with previous libxml2 versions, which is what we get on linux
|
||||
{ version = "4.9.4", platform = "linux" },
|
||||
]
|
||||
jsonpatch = "*"
|
||||
opencontainers = { extras = ["reggie"], version = "*" }
|
||||
packaging = "*"
|
||||
paramiko = "*"
|
||||
@ -132,6 +132,7 @@ pyjwt = "*"
|
||||
python = "~3.12"
|
||||
pyyaml = "*"
|
||||
requests-oauthlib = "*"
|
||||
scim2-filter-parser = "*"
|
||||
sentry-sdk = "*"
|
||||
service_identity = "*"
|
||||
setproctitle = "*"
|
||||
|
1111
schema.yml
1111
schema.yml
File diff suppressed because it is too large
Load Diff
@ -129,6 +129,7 @@ class TestFlowsEnroll(SeleniumTestCase):
|
||||
prompt_stage.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
|
||||
|
||||
# Second prompt stage
|
||||
sleep(1)
|
||||
flow_executor = self.get_shadow_root("ak-flow-executor")
|
||||
prompt_stage = self.get_shadow_root("ak-stage-prompt", flow_executor)
|
||||
wait = WebDriverWait(prompt_stage, self.wait_timeout)
|
||||
|
90
tests/e2e/test_source_scim.py
Normal file
90
tests/e2e/test_source_scim.py
Normal file
@ -0,0 +1,90 @@
|
||||
"""test SCIM Source"""
|
||||
|
||||
from pprint import pformat
|
||||
from time import sleep
|
||||
from typing import Any
|
||||
|
||||
from docker.types import Healthcheck
|
||||
|
||||
from authentik.core.models import Token, TokenIntents, User
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.utils.http import get_http_session
|
||||
from authentik.sources.scim.models import SCIMSource
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
|
||||
TEST_POLL_MAX = 25
|
||||
|
||||
|
||||
class TestSourceSCIM(SeleniumTestCase):
|
||||
"""test SCIM Source flow"""
|
||||
|
||||
def setUp(self):
|
||||
self.slug = generate_id()
|
||||
super().setUp()
|
||||
|
||||
def get_container_specs(self) -> dict[str, Any] | None:
|
||||
return {
|
||||
"image": (
|
||||
"ghcr.io/suvera/scim2-compliance-test-utility@sha256:eca913bb73"
|
||||
"c46892cd1fb2dfd2fef1c5881e6abc5cb0eec7e92fb78c1b933ece"
|
||||
),
|
||||
"detach": True,
|
||||
"ports": {"8080": "8080"},
|
||||
"auto_remove": True,
|
||||
"healthcheck": Healthcheck(
|
||||
test=["CMD", "curl", "http://localhost:8080"],
|
||||
interval=5 * 1_000 * 1_000_000,
|
||||
start_period=1 * 1_000 * 1_000_000,
|
||||
),
|
||||
}
|
||||
|
||||
@retry()
|
||||
def test_scim_conformance(self):
|
||||
user = User.objects.create(
|
||||
username=generate_id(),
|
||||
)
|
||||
token = Token.objects.create(
|
||||
user=user,
|
||||
intent=TokenIntents.INTENT_API,
|
||||
expiring=False,
|
||||
)
|
||||
source = SCIMSource.objects.create(
|
||||
name=generate_id(),
|
||||
slug=generate_id(),
|
||||
token=token,
|
||||
)
|
||||
session = get_http_session()
|
||||
test_launch = session.post(
|
||||
"http://localhost:8080/test/run",
|
||||
data={
|
||||
"endPoint": self.live_server_url + f"/source/scim/{source.slug}/v2",
|
||||
"username": "foo",
|
||||
"password": token.key,
|
||||
"jwtToken": None,
|
||||
"usersCheck": 1,
|
||||
"groupsCheck": 1,
|
||||
"checkIndResLocation": 1,
|
||||
},
|
||||
)
|
||||
self.assertEqual(test_launch.status_code, 200)
|
||||
test_id = test_launch.json()["id"]
|
||||
attempt = 0
|
||||
while attempt <= TEST_POLL_MAX:
|
||||
test_status = session.get(
|
||||
"http://localhost:8080/test/status",
|
||||
params={"runId": test_id},
|
||||
)
|
||||
self.assertEqual(test_status.status_code, 200)
|
||||
body = test_status.json()
|
||||
if any([data["title"] == "--DONE--" for data in body["data"]]):
|
||||
break
|
||||
attempt += 1
|
||||
sleep(1)
|
||||
for test in body["data"]:
|
||||
# Workaround, the test expects DELETE requests to return 204 and have
|
||||
# the content type set to the JSON SCIM one, which is not what most HTTP servers do
|
||||
if test["requestMethod"] == "DELETE" and test["responseCode"] == 204: # noqa: PLR2004
|
||||
continue
|
||||
if test["title"] == "--DONE--":
|
||||
break
|
||||
self.assertTrue(test["success"], pformat(test))
|
@ -119,7 +119,9 @@ class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase):
|
||||
"""Output the container logs to our STDOUT"""
|
||||
_container = container or self.container
|
||||
if IS_CI:
|
||||
print(f"::group::Container logs - {_container.image.tags[0]}")
|
||||
image = _container.image
|
||||
tags = image.tags[0] if len(image.tags) > 0 else str(image)
|
||||
print(f"::group::Container logs - {tags}")
|
||||
for log in _container.logs().decode().split("\n"):
|
||||
print(log)
|
||||
if IS_CI:
|
||||
|
146
tests/wdio/package-lock.json
generated
146
tests/wdio/package-lock.json
generated
@ -12,10 +12,10 @@
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.5.0",
|
||||
"@typescript-eslint/parser": "^7.5.0",
|
||||
"@wdio/cli": "^8.35.1",
|
||||
"@wdio/local-runner": "^8.35.1",
|
||||
"@wdio/mocha-framework": "^8.35.0",
|
||||
"@wdio/spec-reporter": "^8.32.4",
|
||||
"@wdio/cli": "^8.36.0",
|
||||
"@wdio/local-runner": "^8.36.0",
|
||||
"@wdio/mocha-framework": "^8.36.0",
|
||||
"@wdio/spec-reporter": "^8.36.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-sonarjs": "^0.25.1",
|
||||
@ -1189,19 +1189,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@wdio/cli": {
|
||||
"version": "8.35.1",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-8.35.1.tgz",
|
||||
"integrity": "sha512-cdFmd6P/eQJdP2lChQ+Fa9b1c2p0bDIPmetVHGCuHiW8ZPkanrvBFtHMUhMu44a1koni9LvN/hu7vIJ/aAC+Rg==",
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-8.36.0.tgz",
|
||||
"integrity": "sha512-B8iEwz9DRzHquPihT74nKUzN9s+rCd1TkBp+JGmdgm7pJqiWTe4FORrzaxWjdiCO78jbYK9LgaMORpCcAzjwIA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "^20.1.1",
|
||||
"@vitest/snapshot": "^1.2.1",
|
||||
"@wdio/config": "8.35.0",
|
||||
"@wdio/globals": "8.35.1",
|
||||
"@wdio/config": "8.36.0",
|
||||
"@wdio/globals": "8.36.0",
|
||||
"@wdio/logger": "8.28.0",
|
||||
"@wdio/protocols": "8.32.0",
|
||||
"@wdio/types": "8.32.4",
|
||||
"@wdio/utils": "8.35.0",
|
||||
"@wdio/types": "8.36.0",
|
||||
"@wdio/utils": "8.36.0",
|
||||
"async-exit-hook": "^2.0.1",
|
||||
"chalk": "^5.2.0",
|
||||
"chokidar": "^3.5.3",
|
||||
@ -1216,7 +1216,7 @@
|
||||
"lodash.union": "^4.6.0",
|
||||
"read-pkg-up": "10.0.0",
|
||||
"recursive-readdir": "^2.2.3",
|
||||
"webdriverio": "8.35.1",
|
||||
"webdriverio": "8.36.0",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
@ -1239,14 +1239,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@wdio/config": {
|
||||
"version": "8.35.0",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/config/-/config-8.35.0.tgz",
|
||||
"integrity": "sha512-I36sBPMl/+LCyQ3Pwb8gGQM6KxwmUfhOPp16TxN21Qo/Bc0fZfyGIg6KevmRu4DuqpGUm5MMVSfyPhLUkMk3Cg==",
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/config/-/config-8.36.0.tgz",
|
||||
"integrity": "sha512-sAbqnx/G+OsrMquIncFXjM4U0/E0ULMP0jDHZND75r0e1DYYCHmyacrvIHu3Jyxinl9f6+4XQdev6vqdTqPdNg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@wdio/logger": "8.28.0",
|
||||
"@wdio/types": "8.32.4",
|
||||
"@wdio/utils": "8.35.0",
|
||||
"@wdio/types": "8.36.0",
|
||||
"@wdio/utils": "8.36.0",
|
||||
"decamelize": "^6.0.0",
|
||||
"deepmerge-ts": "^5.0.0",
|
||||
"glob": "^10.2.2",
|
||||
@ -1257,29 +1257,29 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@wdio/globals": {
|
||||
"version": "8.35.1",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-8.35.1.tgz",
|
||||
"integrity": "sha512-T3IUFcKXRU9WWleAV72DGFWUiXSSr8SBvpc2cUJrvZ5Je9R2gEsrts5eHCY7amXtfeylfMgy5EayGMajgcna6A==",
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-8.36.0.tgz",
|
||||
"integrity": "sha512-vqMq1hR+iF0lqMNJpk9z+QB9l/QfL1DbvOfNhPtQ13NgctfNg42ffuhEObbzTLQN0MftcnPBu6O3pai79y8bUA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^16.13 || >=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"expect-webdriverio": "^4.11.2",
|
||||
"webdriverio": "8.35.1"
|
||||
"webdriverio": "8.36.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@wdio/local-runner": {
|
||||
"version": "8.35.1",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/local-runner/-/local-runner-8.35.1.tgz",
|
||||
"integrity": "sha512-PG+bADoY5VoWPmAfRi030rtxbFj68MVPlcwEN0dN1lDdYKz1ATzzGUK12sqCgGz1ktcC7sQzmJZVBklzbvn3mQ==",
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/local-runner/-/local-runner-8.36.0.tgz",
|
||||
"integrity": "sha512-MIzbWcXgRQGQQK4H5N39/JFoikOg5cu34l1U6rgw74D6hO79L4RwBg2Oo4TJJYgHUL/4RbVwyeLdb5WDTdluTQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "^20.1.0",
|
||||
"@wdio/logger": "8.28.0",
|
||||
"@wdio/repl": "8.24.12",
|
||||
"@wdio/runner": "8.35.1",
|
||||
"@wdio/types": "8.32.4",
|
||||
"@wdio/runner": "8.36.0",
|
||||
"@wdio/types": "8.36.0",
|
||||
"async-exit-hook": "^2.0.1",
|
||||
"split2": "^4.1.0",
|
||||
"stream-buffers": "^3.0.2"
|
||||
@ -1316,16 +1316,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@wdio/mocha-framework": {
|
||||
"version": "8.35.0",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/mocha-framework/-/mocha-framework-8.35.0.tgz",
|
||||
"integrity": "sha512-riO3aMgvGdFFRMpyMk5m480V+mi5EcKk6cjT1TB9L5XEN7Mo/8qthBw9CLgFCZkr4KlR40hgPKSZFHE0rH2GpQ==",
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/mocha-framework/-/mocha-framework-8.36.0.tgz",
|
||||
"integrity": "sha512-5wZgh1apbSKTtgGwvd//L4kxdaXe30AQ3y9YeJD+OuAJUTYFRjTpMS13bO3pX518imQeB8HCm4aUc2kxs7J81Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/mocha": "^10.0.0",
|
||||
"@types/node": "^20.1.0",
|
||||
"@wdio/logger": "8.28.0",
|
||||
"@wdio/types": "8.32.4",
|
||||
"@wdio/utils": "8.35.0",
|
||||
"@wdio/types": "8.36.0",
|
||||
"@wdio/utils": "8.36.0",
|
||||
"mocha": "^10.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@ -1351,14 +1351,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@wdio/reporter": {
|
||||
"version": "8.32.4",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/reporter/-/reporter-8.32.4.tgz",
|
||||
"integrity": "sha512-kZXbyNuZSSpk4kBavDb+ac25ODu9NVZED6WwZafrlMSnBHcDkoMt26Q0Jp3RKUj+FTyuKH0HvfeLrwVkk6QKDw==",
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/reporter/-/reporter-8.36.0.tgz",
|
||||
"integrity": "sha512-pkAxqiMC+ljmksOKlK9g6y2NRvrdQiKtxD11rsMwJ6CH4kVDSGIvENw7u3kxg7Qwp0j1rCKf5Hp51npqKQgeDQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "^20.1.0",
|
||||
"@wdio/logger": "8.28.0",
|
||||
"@wdio/types": "8.32.4",
|
||||
"@wdio/types": "8.36.0",
|
||||
"diff": "^5.0.0",
|
||||
"object-inspect": "^1.12.0"
|
||||
},
|
||||
@ -1367,35 +1367,35 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@wdio/runner": {
|
||||
"version": "8.35.1",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/runner/-/runner-8.35.1.tgz",
|
||||
"integrity": "sha512-5F6cbOYeZjF34Vsnycp5JPnDljI52fmyxsV2O/L3h6F2+83YXpbsqBplw/2G24JtIUudV7VOY/38bUicn1OyXg==",
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/runner/-/runner-8.36.0.tgz",
|
||||
"integrity": "sha512-M2ZDL0gmR2VvVMchi3Pkonva6Gn6eFh6IwVCpT0np7zioaqOksy3IM7Aki8kPKKS88Osip5dAfoKIrY7JpHovA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "^20.11.28",
|
||||
"@wdio/config": "8.35.0",
|
||||
"@wdio/globals": "8.35.1",
|
||||
"@wdio/config": "8.36.0",
|
||||
"@wdio/globals": "8.36.0",
|
||||
"@wdio/logger": "8.28.0",
|
||||
"@wdio/types": "8.32.4",
|
||||
"@wdio/utils": "8.35.0",
|
||||
"@wdio/types": "8.36.0",
|
||||
"@wdio/utils": "8.36.0",
|
||||
"deepmerge-ts": "^5.1.0",
|
||||
"expect-webdriverio": "^4.12.0",
|
||||
"gaze": "^1.1.3",
|
||||
"webdriver": "8.35.0",
|
||||
"webdriverio": "8.35.1"
|
||||
"webdriver": "8.36.0",
|
||||
"webdriverio": "8.36.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.13 || >=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@wdio/spec-reporter": {
|
||||
"version": "8.32.4",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/spec-reporter/-/spec-reporter-8.32.4.tgz",
|
||||
"integrity": "sha512-3TbD/KrK+EhUex5d5/11qSEKqyNiMHqm27my86tdiK0Ltt9pc/9Ybg1YBiWKlzV9U9MI4seVBRZCXltG17ky/A==",
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/spec-reporter/-/spec-reporter-8.36.0.tgz",
|
||||
"integrity": "sha512-GVOiWqVYvzoAo4/4hNVxvyVWVoHyEmAywYhkykyJGL05YpO0oDOZY2kINPePEX5Z+nIsXsiKPmtsGGqWsfQwTw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@wdio/reporter": "8.32.4",
|
||||
"@wdio/types": "8.32.4",
|
||||
"@wdio/reporter": "8.36.0",
|
||||
"@wdio/types": "8.36.0",
|
||||
"chalk": "^5.1.2",
|
||||
"easy-table": "^1.2.0",
|
||||
"pretty-ms": "^7.0.0"
|
||||
@ -1417,9 +1417,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@wdio/types": {
|
||||
"version": "8.32.4",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.32.4.tgz",
|
||||
"integrity": "sha512-pDPGcCvq0MQF8u0sjw9m4aMI2gAKn6vphyBB2+1IxYriL777gbbxd7WQ+PygMBvYVprCYIkLPvhUFwF85WakmA==",
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.36.0.tgz",
|
||||
"integrity": "sha512-0hw/PaJHqDrbIMvU08w3oMDGg89udSkqWF2hFlGAjOc20quRrhn0F1L+NhFpYdezeRKz5gpgTDIqaQs9RWKq1A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "^20.1.0"
|
||||
@ -1429,14 +1429,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@wdio/utils": {
|
||||
"version": "8.35.0",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-8.35.0.tgz",
|
||||
"integrity": "sha512-9KCyn4aS+9tWfthnUkNFVe52AM6QrLGAeIxgGxNlzTAcQGl7jjwdDM7aSK0RjLkWI3a/88DRH21mN/t2LGDmPQ==",
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-8.36.0.tgz",
|
||||
"integrity": "sha512-3VAbavN206qkvm6lITtOtTgscFChax7shzqHjUNln+QWMRyELtT81iw32ux2ld+Bg3F60LAmhbGodu0lJH7k2w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@puppeteer/browsers": "^1.6.0",
|
||||
"@wdio/logger": "8.28.0",
|
||||
"@wdio/types": "8.32.4",
|
||||
"@wdio/types": "8.36.0",
|
||||
"decamelize": "^6.0.0",
|
||||
"deepmerge-ts": "^5.1.0",
|
||||
"edgedriver": "^5.3.5",
|
||||
@ -2691,9 +2691,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/devtools-protocol": {
|
||||
"version": "0.0.1273771",
|
||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1273771.tgz",
|
||||
"integrity": "sha512-QDbb27xcTVReQQW/GHJsdQqGKwYBE7re7gxehj467kKP2DKuYBUj6i2k5LRiAC66J1yZG/9gsxooz/s9pcm0Og==",
|
||||
"version": "0.0.1282316",
|
||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1282316.tgz",
|
||||
"integrity": "sha512-i7eIqWdVxeXBY/M+v83yRkOV1sTHnr3XYiC0YNBivLIE6hBfE2H0c2o8VC5ynT44yjy+Ei0kLrBQFK/RUKaAHQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/diff": {
|
||||
@ -8886,18 +8886,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/webdriver": {
|
||||
"version": "8.35.0",
|
||||
"resolved": "https://registry.npmjs.org/webdriver/-/webdriver-8.35.0.tgz",
|
||||
"integrity": "sha512-D13EroddIXDqdq3jgO8j6sorgTWqTwEiTqwlDoJizpRIgHGBy+UjkNM7XW1yVcvt8gsD2Dei2LQth2tJEnu5Ng==",
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/webdriver/-/webdriver-8.36.0.tgz",
|
||||
"integrity": "sha512-6fmZI1+OCGbhuGMLBLvA7m9TJvHU1Cyzxqd8rGzIyb8hocR53yh/olfOL1BPcjU1NXmKuU1BePSGF+yiKajiEA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "^20.1.0",
|
||||
"@types/ws": "^8.5.3",
|
||||
"@wdio/config": "8.35.0",
|
||||
"@wdio/config": "8.36.0",
|
||||
"@wdio/logger": "8.28.0",
|
||||
"@wdio/protocols": "8.32.0",
|
||||
"@wdio/types": "8.32.4",
|
||||
"@wdio/utils": "8.35.0",
|
||||
"@wdio/types": "8.36.0",
|
||||
"@wdio/utils": "8.36.0",
|
||||
"deepmerge-ts": "^5.1.0",
|
||||
"got": "^12.6.1",
|
||||
"ky": "^0.33.0",
|
||||
@ -8908,23 +8908,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/webdriverio": {
|
||||
"version": "8.35.1",
|
||||
"resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-8.35.1.tgz",
|
||||
"integrity": "sha512-YAuKR4JERGiMqCJmm5fEVZ160iiFPyupwALqfXfzrYVcEmKltKPFY/oUCArmi6Uzqd+Sa2Kp9WZtz2Eu1R76JA==",
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-8.36.0.tgz",
|
||||
"integrity": "sha512-4WnEI+OxslHpfSnDXuADaR6bL1M7QxBUEF1mTN56AroOCJelyPvt94yRhszwQnLcJJB2OLn49eUz8M4yBCB51w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "^20.1.0",
|
||||
"@wdio/config": "8.35.0",
|
||||
"@wdio/config": "8.36.0",
|
||||
"@wdio/logger": "8.28.0",
|
||||
"@wdio/protocols": "8.32.0",
|
||||
"@wdio/repl": "8.24.12",
|
||||
"@wdio/types": "8.32.4",
|
||||
"@wdio/utils": "8.35.0",
|
||||
"@wdio/types": "8.36.0",
|
||||
"@wdio/utils": "8.36.0",
|
||||
"archiver": "^7.0.0",
|
||||
"aria-query": "^5.0.0",
|
||||
"css-shorthand-properties": "^1.1.1",
|
||||
"css-value": "^0.0.1",
|
||||
"devtools-protocol": "^0.0.1273771",
|
||||
"devtools-protocol": "^0.0.1282316",
|
||||
"grapheme-splitter": "^1.0.2",
|
||||
"import-meta-resolve": "^4.0.0",
|
||||
"is-plain-obj": "^4.1.0",
|
||||
@ -8936,7 +8936,7 @@
|
||||
"resq": "^1.9.1",
|
||||
"rgb2hex": "0.2.5",
|
||||
"serialize-error": "^11.0.1",
|
||||
"webdriver": "8.35.0"
|
||||
"webdriver": "8.36.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.13 || >=18"
|
||||
|
@ -6,10 +6,10 @@
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.5.0",
|
||||
"@typescript-eslint/parser": "^7.5.0",
|
||||
"@wdio/cli": "^8.35.1",
|
||||
"@wdio/local-runner": "^8.35.1",
|
||||
"@wdio/mocha-framework": "^8.35.0",
|
||||
"@wdio/spec-reporter": "^8.32.4",
|
||||
"@wdio/cli": "^8.36.0",
|
||||
"@wdio/local-runner": "^8.36.0",
|
||||
"@wdio/mocha-framework": "^8.36.0",
|
||||
"@wdio/spec-reporter": "^8.36.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-sonarjs": "^0.25.1",
|
||||
|
162
web/package-lock.json
generated
162
web/package-lock.json
generated
@ -9,7 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-html": "^6.4.8",
|
||||
"@codemirror/lang-html": "^6.4.9",
|
||||
"@codemirror/lang-javascript": "^6.2.2",
|
||||
"@codemirror/lang-python": "^6.1.5",
|
||||
"@codemirror/lang-xml": "^6.1.0",
|
||||
@ -17,15 +17,15 @@
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@formatjs/intl-listformat": "^7.5.5",
|
||||
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||
"@goauthentik/api": "^2024.2.2-1712922569",
|
||||
"@goauthentik/api": "^2024.2.2-1713289394",
|
||||
"@lit-labs/task": "^3.1.0",
|
||||
"@lit/context": "^1.1.0",
|
||||
"@lit/context": "^1.1.1",
|
||||
"@lit/localize": "^0.12.1",
|
||||
"@lit/reactive-element": "^2.0.4",
|
||||
"@open-wc/lit-helpers": "^0.7.0",
|
||||
"@patternfly/elements": "^3.0.0",
|
||||
"@patternfly/patternfly": "^4.224.2",
|
||||
"@sentry/browser": "^7.110.0",
|
||||
"@sentry/browser": "^7.110.1",
|
||||
"@webcomponents/webcomponentsjs": "^2.8.0",
|
||||
"base64-js": "^1.5.1",
|
||||
"chart.js": "^4.4.2",
|
||||
@ -36,7 +36,7 @@
|
||||
"country-flag-icons": "^1.5.11",
|
||||
"fuse.js": "^7.0.0",
|
||||
"guacamole-common-js": "^1.5.0",
|
||||
"lit": "^3.1.2",
|
||||
"lit": "^3.1.3",
|
||||
"md-front-matter": "^1.0.4",
|
||||
"mermaid": "^10.9.0",
|
||||
"rapidoc": "^9.3.4",
|
||||
@ -100,7 +100,7 @@
|
||||
"storybook-addon-mock": "^4.3.0",
|
||||
"ts-lit-plugin": "^2.0.2",
|
||||
"tslib": "^2.6.2",
|
||||
"turnstile-types": "^1.2.0",
|
||||
"turnstile-types": "^1.2.1",
|
||||
"typescript": "^5.4.5",
|
||||
"vite-tsconfig-paths": "^4.3.2"
|
||||
},
|
||||
@ -111,9 +111,9 @@
|
||||
"@esbuild/darwin-arm64": "^0.20.1",
|
||||
"@esbuild/linux-amd64": "^0.18.11",
|
||||
"@esbuild/linux-arm64": "^0.20.1",
|
||||
"@rollup/rollup-darwin-arm64": "4.14.1",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.14.1",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.14.1"
|
||||
"@rollup/rollup-darwin-arm64": "4.14.3",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.14.3",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@aashutoshrathi/word-wrap": {
|
||||
@ -2153,8 +2153,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-html": {
|
||||
"version": "6.4.8",
|
||||
"license": "MIT",
|
||||
"version": "6.4.9",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.9.tgz",
|
||||
"integrity": "sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/lang-css": "^6.0.0",
|
||||
@ -2840,9 +2841,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@goauthentik/api": {
|
||||
"version": "2024.2.2-1712922569",
|
||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.2.2-1712922569.tgz",
|
||||
"integrity": "sha512-CUCSUQ7Zr3waDJs8OL+CTyFDz7eWIhgZaBAWDJLCy3i954Uo2hYsq1RjBUtk4PS+LwtmcY8FD/YvVi04fxSreA=="
|
||||
"version": "2024.2.2-1713289394",
|
||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.2.2-1713289394.tgz",
|
||||
"integrity": "sha512-/OHJJtERQ13LMCXgwuPK8KR2WCsJwWA2mruyriOVwRs1caZH6uxq6H9dN/ybbvESQ6MekAhqz6KHhll3uGKdGg=="
|
||||
},
|
||||
"node_modules/@hcaptcha/types": {
|
||||
"version": "1.0.3",
|
||||
@ -3367,9 +3368,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@lit/context": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@lit/context/-/context-1.1.0.tgz",
|
||||
"integrity": "sha512-fCyv4dsH05wCNm3AKbB+PdYbXGJd/XT8OOwo4hVmD4COq5wOWJlQreGAMDvmHZ7osqxuu06Y4nmP6ooXpN7ErA==",
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@lit/context/-/context-1.1.1.tgz",
|
||||
"integrity": "sha512-q/Rw7oWSJidUP43f/RUPwqZ6f5VlY8HzinTWxL/gW1Hvm2S5q2hZvV+qM8WFcC+oLNNknc3JKsd5TwxLk1hbdg==",
|
||||
"dependencies": {
|
||||
"@lit/reactive-element": "^1.6.2 || ^2.0.0"
|
||||
}
|
||||
@ -4248,9 +4249,9 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.1.tgz",
|
||||
"integrity": "sha512-+kecg3FY84WadgcuSVm6llrABOdQAEbNdnpi5X3UwWiFVhZIZvKgGrF7kmLguvxHNQy+UuRV66cLVl3S+Rkt+Q==",
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.3.tgz",
|
||||
"integrity": "sha512-Od4vE6f6CTT53yM1jgcLqNfItTsLt5zE46fdPaEmeFHvPs5SjZYlLpHrSiHEKR1+HdRfxuzXHjDOIxQyC3ptBA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -4288,9 +4289,9 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.1.tgz",
|
||||
"integrity": "sha512-p9rGKYkHdFMzhckOTFubfxgyIO1vw//7IIjBBRVzyZebWlzRLeNhqxuSaZ7kCEKVkm/kuC9fVRW9HkC/zNRG2w==",
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.3.tgz",
|
||||
"integrity": "sha512-Eci2us9VTHm1eSyn5/eEpaC7eP/mp5n46gTRB3Aar3BgSvDQGJZuicyq6TsH4HngNBgVqC5sDYxOzTExSU+NjA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -4356,9 +4357,9 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.1.tgz",
|
||||
"integrity": "sha512-9Q7DGjZN+hTdJomaQ3Iub4m6VPu1r94bmK2z3UeWP3dGUecRC54tmVu9vKHTm1bOt3ASoYtEz6JSRLFzrysKlA==",
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.3.tgz",
|
||||
"integrity": "sha512-8ybV4Xjy59xLMyWo3GCfEGqtKV5M5gCSrZlxkPGvEPCGDLNla7v48S662HSGwRd6/2cSneMQWiv+QzcttLrrOA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -4424,102 +4425,102 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@sentry-internal/feedback": {
|
||||
"version": "7.110.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.110.0.tgz",
|
||||
"integrity": "sha512-hrfWa3WkSOiBO5Srcr1j4kuGOlbsQic+REpLOofllVIs56DOo9+Aj9svxT+dcvZERv/nlFSV/E0BfGy9g08IEg==",
|
||||
"version": "7.110.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.110.1.tgz",
|
||||
"integrity": "sha512-0aR3wuEW+SZKOVNamuy0pTQyPmqDjWPPLrB2GAXGT3ZjrVxjEzzVPqk6DVBYxSV2MuJaD507SZnvfoSPNgoBmw==",
|
||||
"dependencies": {
|
||||
"@sentry/core": "7.110.0",
|
||||
"@sentry/types": "7.110.0",
|
||||
"@sentry/utils": "7.110.0"
|
||||
"@sentry/core": "7.110.1",
|
||||
"@sentry/types": "7.110.1",
|
||||
"@sentry/utils": "7.110.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/replay-canvas": {
|
||||
"version": "7.110.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-7.110.0.tgz",
|
||||
"integrity": "sha512-SNa+AfyfX+vc6Xw0pIfDsa5Qnc9cpexU6M2D19gadtVhmep7qoFBuhBVZrSv6BtdCxvrb5EyYsHYGfjQdIDcvg==",
|
||||
"version": "7.110.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-7.110.1.tgz",
|
||||
"integrity": "sha512-zdcCmWFXM4DHOau/BCZVb6jf9zozdbAiJ1MzQ6azuZEuysOl00YfktoWZBbZjjjpWT6025s+wrmFz54t0O+enw==",
|
||||
"dependencies": {
|
||||
"@sentry/core": "7.110.0",
|
||||
"@sentry/replay": "7.110.0",
|
||||
"@sentry/types": "7.110.0",
|
||||
"@sentry/utils": "7.110.0"
|
||||
"@sentry/core": "7.110.1",
|
||||
"@sentry/replay": "7.110.1",
|
||||
"@sentry/types": "7.110.1",
|
||||
"@sentry/utils": "7.110.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/tracing": {
|
||||
"version": "7.110.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.110.0.tgz",
|
||||
"integrity": "sha512-IIHHa9e/mE7uOMJfNELI8adyoELxOy6u6TNCn5t6fphmq84w8FTc9adXkG/FY2AQpglkIvlILojfMROFB2aaAQ==",
|
||||
"version": "7.110.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.110.1.tgz",
|
||||
"integrity": "sha512-4kTd6EM0OP1SVWl2yLn3KIwlCpld1lyhNDeR8G1aKLm1PN+kVsR6YB/jy9KPPp4Q3lN3W9EkTSES3qhP4jVffQ==",
|
||||
"dependencies": {
|
||||
"@sentry/core": "7.110.0",
|
||||
"@sentry/types": "7.110.0",
|
||||
"@sentry/utils": "7.110.0"
|
||||
"@sentry/core": "7.110.1",
|
||||
"@sentry/types": "7.110.1",
|
||||
"@sentry/utils": "7.110.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/browser": {
|
||||
"version": "7.110.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.110.0.tgz",
|
||||
"integrity": "sha512-gIxedVm6ZgkjQfgCDgLWJgAsolq6OxV8hQ2j1+RaDL2RngvelFo/vlX5f2sD6EbjVp77Cri8u5GkMJF+v4p84g==",
|
||||
"version": "7.110.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.110.1.tgz",
|
||||
"integrity": "sha512-H3TZlbdsgxuoVxhotMtBDemvAofx3UPNcS+UjQ40Bd+hKX01IIbEN3i+9RQ0jmcbU6xjf+yhjwp+Ejpm4FmYMw==",
|
||||
"dependencies": {
|
||||
"@sentry-internal/feedback": "7.110.0",
|
||||
"@sentry-internal/replay-canvas": "7.110.0",
|
||||
"@sentry-internal/tracing": "7.110.0",
|
||||
"@sentry/core": "7.110.0",
|
||||
"@sentry/replay": "7.110.0",
|
||||
"@sentry/types": "7.110.0",
|
||||
"@sentry/utils": "7.110.0"
|
||||
"@sentry-internal/feedback": "7.110.1",
|
||||
"@sentry-internal/replay-canvas": "7.110.1",
|
||||
"@sentry-internal/tracing": "7.110.1",
|
||||
"@sentry/core": "7.110.1",
|
||||
"@sentry/replay": "7.110.1",
|
||||
"@sentry/types": "7.110.1",
|
||||
"@sentry/utils": "7.110.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/core": {
|
||||
"version": "7.110.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.110.0.tgz",
|
||||
"integrity": "sha512-g4suCQO94mZsKVaAbyD1zLFC5YSuBQCIPHXx9fdgtfoPib7BWjWWePkllkrvsKAv4u8Oq05RfnKOhOMRHpOKqg==",
|
||||
"version": "7.110.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.110.1.tgz",
|
||||
"integrity": "sha512-yC1yeUFQlmHj9u/KxKmwOMVanBmgfX+4MZnZU31QPqN95adyZTwpaYFZl4fH5kDVnz7wXJI0qRP8SxuMePtqhw==",
|
||||
"dependencies": {
|
||||
"@sentry/types": "7.110.0",
|
||||
"@sentry/utils": "7.110.0"
|
||||
"@sentry/types": "7.110.1",
|
||||
"@sentry/utils": "7.110.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/replay": {
|
||||
"version": "7.110.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.110.0.tgz",
|
||||
"integrity": "sha512-EEpGPf3iBJjWejvoxKLVMnLtLNwPTUxHJV1oxUkbcSi3B/tG5hW7LArYDjAcvkfa4VmA8JLCwj2vYU5MQ8tj6g==",
|
||||
"version": "7.110.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.110.1.tgz",
|
||||
"integrity": "sha512-R49fGOuKYsJ97EujPTzMjs3ZSuSkLTFFQmVBbsu/o6beRp4kK9l8H7r2BfLEcWJOXdWO5EU4KpRWgIxHaDK2aw==",
|
||||
"dependencies": {
|
||||
"@sentry-internal/tracing": "7.110.0",
|
||||
"@sentry/core": "7.110.0",
|
||||
"@sentry/types": "7.110.0",
|
||||
"@sentry/utils": "7.110.0"
|
||||
"@sentry-internal/tracing": "7.110.1",
|
||||
"@sentry/core": "7.110.1",
|
||||
"@sentry/types": "7.110.1",
|
||||
"@sentry/utils": "7.110.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/types": {
|
||||
"version": "7.110.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.110.0.tgz",
|
||||
"integrity": "sha512-DqYBLyE8thC5P5MuPn+sj8tL60nCd/f5cerFFPcudn5nJ4Zs1eI6lKlwwyHYTEu5c4KFjCB0qql6kXfwAHmTyA==",
|
||||
"version": "7.110.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.110.1.tgz",
|
||||
"integrity": "sha512-sZxOpM5gfyxvJeWVvNpHnxERTnlqcozjqNcIv29SZ6wonlkekmxDyJ3uCuPv85VO54WLyA4uzskPKnNFHacI8A==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/utils": {
|
||||
"version": "7.110.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.110.0.tgz",
|
||||
"integrity": "sha512-VBsdLLN+5tf73fhf/Cm7JIsUJ6y9DkJj8h4I6Mxx0rszrvOyH6S5px40K+V4jdLBzMEvVinC7q2Cbf1YM18BSw==",
|
||||
"version": "7.110.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.110.1.tgz",
|
||||
"integrity": "sha512-eibLo2m1a7sHkOHxYYmRujr3D7ek2l9sv26F1SLoQBVDF7Afw5AKyzPmtA1D+4M9P/ux1okj7cGj3SaBrVpxXA==",
|
||||
"dependencies": {
|
||||
"@sentry/types": "7.110.0"
|
||||
"@sentry/types": "7.110.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@ -12621,9 +12622,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lit": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lit/-/lit-3.1.2.tgz",
|
||||
"integrity": "sha512-VZx5iAyMtX7CV4K8iTLdCkMaYZ7ipjJZ0JcSdJ0zIdGxxyurjIn7yuuSxNBD7QmjvcNJwr0JS4cAdAtsy7gZ6w==",
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/lit/-/lit-3.1.3.tgz",
|
||||
"integrity": "sha512-l4slfspEsnCcHVRTvaP7YnkTZEZggNFywLEIhQaGhYDczG+tu/vlgm/KaWIEjIp+ZyV20r2JnZctMb8LeLCG7Q==",
|
||||
"dependencies": {
|
||||
"@lit/reactive-element": "^2.0.4",
|
||||
"lit-element": "^4.0.4",
|
||||
@ -17009,9 +17010,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/turnstile-types": {
|
||||
"version": "1.2.0",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/turnstile-types/-/turnstile-types-1.2.1.tgz",
|
||||
"integrity": "sha512-PZFcUDFvPvmmwb885JA/N+8Pg5xNWw/UGMABRb/vI9P8cZ4pLDCpBDzgw7oKQ67DYvboTxNhfTAu93gjX4uNbQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
|
@ -30,7 +30,7 @@
|
||||
"storybook:build-import-map": "node scripts/build-storybook-import-maps.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-html": "^6.4.8",
|
||||
"@codemirror/lang-html": "^6.4.9",
|
||||
"@codemirror/lang-javascript": "^6.2.2",
|
||||
"@codemirror/lang-python": "^6.1.5",
|
||||
"@codemirror/lang-xml": "^6.1.0",
|
||||
@ -38,15 +38,15 @@
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@formatjs/intl-listformat": "^7.5.5",
|
||||
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||
"@goauthentik/api": "^2024.2.2-1712922569",
|
||||
"@goauthentik/api": "^2024.2.2-1713289394",
|
||||
"@lit-labs/task": "^3.1.0",
|
||||
"@lit/context": "^1.1.0",
|
||||
"@lit/context": "^1.1.1",
|
||||
"@lit/localize": "^0.12.1",
|
||||
"@lit/reactive-element": "^2.0.4",
|
||||
"@open-wc/lit-helpers": "^0.7.0",
|
||||
"@patternfly/elements": "^3.0.0",
|
||||
"@patternfly/patternfly": "^4.224.2",
|
||||
"@sentry/browser": "^7.110.0",
|
||||
"@sentry/browser": "^7.110.1",
|
||||
"@webcomponents/webcomponentsjs": "^2.8.0",
|
||||
"base64-js": "^1.5.1",
|
||||
"chart.js": "^4.4.2",
|
||||
@ -57,7 +57,7 @@
|
||||
"country-flag-icons": "^1.5.11",
|
||||
"fuse.js": "^7.0.0",
|
||||
"guacamole-common-js": "^1.5.0",
|
||||
"lit": "^3.1.2",
|
||||
"lit": "^3.1.3",
|
||||
"md-front-matter": "^1.0.4",
|
||||
"mermaid": "^10.9.0",
|
||||
"rapidoc": "^9.3.4",
|
||||
@ -121,7 +121,7 @@
|
||||
"storybook-addon-mock": "^4.3.0",
|
||||
"ts-lit-plugin": "^2.0.2",
|
||||
"tslib": "^2.6.2",
|
||||
"turnstile-types": "^1.2.0",
|
||||
"turnstile-types": "^1.2.1",
|
||||
"typescript": "^5.4.5",
|
||||
"vite-tsconfig-paths": "^4.3.2"
|
||||
},
|
||||
@ -129,9 +129,9 @@
|
||||
"@esbuild/darwin-arm64": "^0.20.1",
|
||||
"@esbuild/linux-amd64": "^0.18.11",
|
||||
"@esbuild/linux-arm64": "^0.20.1",
|
||||
"@rollup/rollup-darwin-arm64": "4.14.1",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.14.1",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.14.1"
|
||||
"@rollup/rollup-darwin-arm64": "4.14.3",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.14.3",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.14.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
|
@ -74,7 +74,7 @@ export class AkBackchannelProvidersInput extends AKElement {
|
||||
<ak-chip-group> ${map(this.providers, renderOneChip)} </ak-chip-group>
|
||||
</div>
|
||||
</div>
|
||||
${this.help ? html`<p class="pf-c-form__helper-radio">${this.help}</p>` : nothing}
|
||||
${this.help ? html`<p class="pf-c-form__helper-text">${this.help}</p>` : nothing}
|
||||
</ak-form-element-horizontal>
|
||||
`;
|
||||
}
|
||||
|
@ -42,6 +42,7 @@ export class GroupListPage extends TablePage<Group> {
|
||||
page: page,
|
||||
pageSize: (await uiConfig()).pagination.perPage,
|
||||
search: this.search || "",
|
||||
includeUsers: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -33,6 +33,7 @@ export class MemberSelectTable extends TableModal<User> {
|
||||
page: page,
|
||||
pageSize: (await uiConfig()).pagination.perPage,
|
||||
search: this.search || "",
|
||||
includeGroups: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -105,6 +105,7 @@ export class RelatedGroupList extends Table<Group> {
|
||||
pageSize: (await uiConfig()).pagination.perPage,
|
||||
search: this.search || "",
|
||||
membersByPk: this.targetUser ? [this.targetUser.pk] : [],
|
||||
includeUsers: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -145,6 +145,7 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
|
||||
type: this.hideServiceAccounts
|
||||
? [CoreUsersListTypeEnum.External, CoreUsersListTypeEnum.Internal]
|
||||
: undefined,
|
||||
includeGroups: false,
|
||||
});
|
||||
this.me = await me();
|
||||
return users;
|
||||
@ -164,6 +165,7 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("User(s)")}
|
||||
actionLabel=${msg("Remove Users(s)")}
|
||||
action=${msg("removed")}
|
||||
actionSubtext=${msg(
|
||||
str`Are you sure you want to remove the selected users from the group ${this.targetGroup?.name}?`,
|
||||
)}
|
||||
|
@ -8,6 +8,7 @@ import "@goauthentik/elements/Markdown";
|
||||
import "@goauthentik/elements/Tabs";
|
||||
import "@goauthentik/elements/buttons/ActionButton";
|
||||
import "@goauthentik/elements/buttons/ModalButton";
|
||||
import "@goauthentik/elements/events/LogViewer";
|
||||
import "@goauthentik/elements/rbac/ObjectPermissionsPage";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
@ -155,9 +156,7 @@ export class SCIMProviderViewPage extends AKElement {
|
||||
<p>${task.name}</p>
|
||||
<ul class="pf-c-list">
|
||||
<li>${header}</li>
|
||||
${task.messages.map((m) => {
|
||||
return html`<li>${m}</li>`;
|
||||
})}
|
||||
<ak-log-viewer .logs=${task?.messages}></ak-log-viewer>
|
||||
</ul>
|
||||
</li> `;
|
||||
})}
|
||||
|
@ -85,6 +85,10 @@ export class RoleAssignedGlobalPermissionsTable extends Table<Permission> {
|
||||
}
|
||||
|
||||
row(item: Permission): TemplateResult[] {
|
||||
return [html`${item.modelVerbose}`, html`${item.name}`, html`✓`];
|
||||
return [
|
||||
html`${item.modelVerbose}`,
|
||||
html`${item.name}`,
|
||||
html`<i class="fas fa-check pf-m-success"></i>`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -90,7 +90,7 @@ export class RoleAssignedObjectPermissionTable extends Table<ExtraRoleObjectPerm
|
||||
>
|
||||
<pre>${item.objectPk}</pre>
|
||||
</pf-tooltip>`}`,
|
||||
html`✓`,
|
||||
html`<i class="fas fa-check pf-m-success"></i>`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import "@goauthentik/admin/sources/ldap/LDAPSourceViewPage";
|
||||
import "@goauthentik/admin/sources/oauth/OAuthSourceViewPage";
|
||||
import "@goauthentik/admin/sources/plex/PlexSourceViewPage";
|
||||
import "@goauthentik/admin/sources/saml/SAMLSourceViewPage";
|
||||
import "@goauthentik/admin/sources/scim/SCIMSourceViewPage";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/EmptyState";
|
||||
@ -51,6 +52,10 @@ export class SourceViewPage extends AKElement {
|
||||
return html`<ak-source-plex-view
|
||||
sourceSlug=${this.source.slug}
|
||||
></ak-source-plex-view>`;
|
||||
case "ak-source-scim-form":
|
||||
return html`<ak-source-scim-view
|
||||
sourceSlug=${this.source.slug}
|
||||
></ak-source-scim-view>`;
|
||||
default:
|
||||
return html`<p>Invalid source type ${this.source.component}</p>`;
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import "@goauthentik/admin/sources/ldap/LDAPSourceForm";
|
||||
import "@goauthentik/admin/sources/oauth/OAuthSourceForm";
|
||||
import "@goauthentik/admin/sources/plex/PlexSourceForm";
|
||||
import "@goauthentik/admin/sources/saml/SAMLSourceForm";
|
||||
import "@goauthentik/admin/sources/scim/SCIMSourceForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/forms/ProxyForm";
|
||||
|
86
web/src/admin/sources/scim/SCIMSourceForm.ts
Normal file
86
web/src/admin/sources/scim/SCIMSourceForm.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { BaseSourceForm } from "@goauthentik/admin/sources/BaseSourceForm";
|
||||
import { placeholderHelperText } from "@goauthentik/authentik/admin/helperText";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
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 { SCIMSource, SCIMSourceRequest, SourcesApi } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-source-scim-form")
|
||||
export class SCIMSourceForm extends BaseSourceForm<SCIMSource> {
|
||||
async loadInstance(pk: string): Promise<SCIMSource> {
|
||||
return new SourcesApi(DEFAULT_CONFIG)
|
||||
.sourcesScimRetrieve({
|
||||
slug: pk,
|
||||
})
|
||||
.then((source) => {
|
||||
return source;
|
||||
});
|
||||
}
|
||||
|
||||
async 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=${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("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"> ${msg("Enabled")} </label>
|
||||
</div>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal label=${msg("User path")} name="userPathTemplate">
|
||||
<input
|
||||
type="text"
|
||||
value="${first(
|
||||
this.instance?.userPathTemplate,
|
||||
"goauthentik.io/sources/%(slug)s",
|
||||
)}"
|
||||
class="pf-c-form-control"
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">${placeholderHelperText}</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
</form>`;
|
||||
}
|
||||
}
|
51
web/src/admin/sources/scim/SCIMSourceGroups.ts
Normal file
51
web/src/admin/sources/scim/SCIMSourceGroups.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { uiConfig } from "@goauthentik/common/ui/config";
|
||||
import { PaginatedResponse, Table, TableColumn } from "@goauthentik/elements/table/Table";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import { SCIMSourceGroup, SourcesApi } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-source-scim-groups-list")
|
||||
export class SCIMSourceGroupList extends Table<SCIMSourceGroup> {
|
||||
@property()
|
||||
sourceSlug?: string;
|
||||
|
||||
expandable = true;
|
||||
searchEnabled(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
async apiEndpoint(page: number): Promise<PaginatedResponse<SCIMSourceGroup>> {
|
||||
return new SourcesApi(DEFAULT_CONFIG).sourcesScimGroupsList({
|
||||
page: page,
|
||||
pageSize: (await uiConfig()).pagination.perPage,
|
||||
ordering: this.order,
|
||||
search: this.search || "",
|
||||
sourceSlug: this.sourceSlug,
|
||||
});
|
||||
}
|
||||
|
||||
columns(): TableColumn[] {
|
||||
return [new TableColumn(msg("Name")), new TableColumn(msg("ID"))];
|
||||
}
|
||||
|
||||
renderExpanded(item: SCIMSourceGroup): TemplateResult {
|
||||
return html`<td role="cell" colspan="4">
|
||||
<div class="pf-c-table__expandable-row-content">
|
||||
<pre>${JSON.stringify(item.attributes, null, 4)}</pre>
|
||||
</div>
|
||||
</td>`;
|
||||
}
|
||||
|
||||
row(item: SCIMSourceGroup): TemplateResult[] {
|
||||
return [
|
||||
html`<a href="#/identity/groups/${item.groupObj.pk}">
|
||||
<div>${item.groupObj.name}</div>
|
||||
</a>`,
|
||||
html`${item.id}`,
|
||||
];
|
||||
}
|
||||
}
|
52
web/src/admin/sources/scim/SCIMSourceUsers.ts
Normal file
52
web/src/admin/sources/scim/SCIMSourceUsers.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { uiConfig } from "@goauthentik/common/ui/config";
|
||||
import { PaginatedResponse, Table, TableColumn } from "@goauthentik/elements/table/Table";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import { SCIMSourceUser, SourcesApi } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-source-scim-users-list")
|
||||
export class SCIMSourceUserList extends Table<SCIMSourceUser> {
|
||||
@property()
|
||||
sourceSlug?: string;
|
||||
|
||||
expandable = true;
|
||||
searchEnabled(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
async apiEndpoint(page: number): Promise<PaginatedResponse<SCIMSourceUser>> {
|
||||
return new SourcesApi(DEFAULT_CONFIG).sourcesScimUsersList({
|
||||
page: page,
|
||||
pageSize: (await uiConfig()).pagination.perPage,
|
||||
ordering: this.order,
|
||||
search: this.search || "",
|
||||
sourceSlug: this.sourceSlug,
|
||||
});
|
||||
}
|
||||
|
||||
columns(): TableColumn[] {
|
||||
return [new TableColumn(msg("Username")), new TableColumn(msg("ID"))];
|
||||
}
|
||||
|
||||
renderExpanded(item: SCIMSourceUser): TemplateResult {
|
||||
return html`<td role="cell" colspan="4">
|
||||
<div class="pf-c-table__expandable-row-content">
|
||||
<pre>${JSON.stringify(item.attributes, null, 4)}</pre>
|
||||
</div>
|
||||
</td>`;
|
||||
}
|
||||
|
||||
row(item: SCIMSourceUser): TemplateResult[] {
|
||||
return [
|
||||
html`<a href="#/identity/users/${item.userObj.pk}">
|
||||
<div>${item.userObj.username}</div>
|
||||
<small>${item.userObj.name}</small>
|
||||
</a>`,
|
||||
html`${item.id}`,
|
||||
];
|
||||
}
|
||||
}
|
215
web/src/admin/sources/scim/SCIMSourceViewPage.ts
Normal file
215
web/src/admin/sources/scim/SCIMSourceViewPage.ts
Normal file
@ -0,0 +1,215 @@
|
||||
import "@goauthentik/admin/sources/scim/SCIMSourceForm";
|
||||
import "@goauthentik/admin/sources/scim/SCIMSourceGroups";
|
||||
import "@goauthentik/admin/sources/scim/SCIMSourceUsers";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||
import "@goauthentik/components/events/ObjectChangelog";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/Tabs";
|
||||
import "@goauthentik/elements/buttons/ActionButton";
|
||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||
import "@goauthentik/elements/buttons/TokenCopyButton";
|
||||
import "@goauthentik/elements/forms/ModalForm";
|
||||
import "@goauthentik/elements/rbac/ObjectPermissionsPage";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import PFBanner from "@patternfly/patternfly/components/Banner/banner.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 PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.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 {
|
||||
RbacPermissionsAssignedByUsersListModelEnum,
|
||||
SCIMSource,
|
||||
SourcesApi,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-source-scim-view")
|
||||
export class SCIMSourceViewPage extends AKElement {
|
||||
@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,
|
||||
PFForm,
|
||||
PFFormControl,
|
||||
PFGrid,
|
||||
PFContent,
|
||||
PFCard,
|
||||
PFDescriptionList,
|
||||
PFBanner,
|
||||
];
|
||||
}
|
||||
|
||||
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="${msg("Overview")}">
|
||||
<div slot="header" class="pf-c-banner pf-m-info">
|
||||
${msg("SCIM Source is in preview.")}
|
||||
<a href="mailto:hello+feature/scim-source@goauthentik.io"
|
||||
>${msg("Send us feedback!")}</a
|
||||
>
|
||||
</div>
|
||||
<div class="pf-c-page__main-section pf-m-no-padding-mobile 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"
|
||||
>${msg("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"
|
||||
>${msg("Slug")}</span
|
||||
>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
${this.source.slug}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="pf-c-card__footer">
|
||||
<ak-forms-modal>
|
||||
<span slot="submit"> ${msg("Update")} </span>
|
||||
<span slot="header"> ${msg("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">
|
||||
${msg("Edit")}
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
|
||||
<div class="pf-c-card">
|
||||
<div class="pf-c-card__body">
|
||||
<form class="pf-c-form">
|
||||
<div class="pf-c-form__group">
|
||||
<label class="pf-c-form__label">
|
||||
<span class="pf-c-form__label-text"
|
||||
>${msg("SCIM Base URL")}</span
|
||||
>
|
||||
</label>
|
||||
<input
|
||||
class="pf-c-form-control"
|
||||
readonly
|
||||
type="text"
|
||||
value="${this.source.rootUrl}"
|
||||
/>
|
||||
</div>
|
||||
<div class="pf-c-form__group">
|
||||
<label class="pf-c-form__label">
|
||||
<span class="pf-c-form__label-text"
|
||||
>${msg("Token")}</span
|
||||
>
|
||||
</label>
|
||||
<div>
|
||||
<ak-token-copy-button
|
||||
class="pf-m-primary"
|
||||
identifier="${this.source?.tokenObj.identifier}"
|
||||
>
|
||||
${msg("Click to copy token")}
|
||||
</ak-token-copy-button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
slot="page-changelog"
|
||||
data-tab-title="${msg("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>
|
||||
<section
|
||||
slot="page-users"
|
||||
data-tab-title="${msg("Provisioned Users")}"
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-l-grid pf-m-gutter">
|
||||
<ak-source-scim-users-list
|
||||
sourceSlug=${this.source.slug}
|
||||
></ak-source-scim-users-list>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
slot="page-groups"
|
||||
data-tab-title="${msg("Provisioned Groups")}"
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-l-grid pf-m-gutter">
|
||||
<ak-source-scim-groups-list
|
||||
sourceSlug=${this.source.slug}
|
||||
></ak-source-scim-groups-list>
|
||||
</div>
|
||||
</section>
|
||||
<ak-rbac-object-permission-page
|
||||
slot="page-permissions"
|
||||
data-tab-title="${msg("Permissions")}"
|
||||
model=${RbacPermissionsAssignedByUsersListModelEnum.SourcesScimScimsource}
|
||||
objectPk=${this.source.pk}
|
||||
></ak-rbac-object-permission-page>
|
||||
</ak-tabs>`;
|
||||
}
|
||||
}
|
@ -38,6 +38,7 @@ export class GroupSelectModal extends TableModal<Group> {
|
||||
page: page,
|
||||
pageSize: (await uiConfig()).pagination.perPage,
|
||||
search: this.search || "",
|
||||
includeUsers: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -84,6 +84,10 @@ export class UserAssignedGlobalPermissionsTable extends Table<Permission> {
|
||||
}
|
||||
|
||||
row(item: Permission): TemplateResult[] {
|
||||
return [html`${item.modelVerbose}`, html`${item.name}`, html`✓`];
|
||||
return [
|
||||
html`${item.modelVerbose}`,
|
||||
html`${item.name}`,
|
||||
html`<i class="fas fa-check pf-m-success"></i>`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -86,7 +86,7 @@ export class UserAssignedObjectPermissionsTable extends Table<ExtraUserObjectPer
|
||||
>
|
||||
<pre>${item.objectPk}</pre>
|
||||
</pf-tooltip>`}`,
|
||||
html`✓`,
|
||||
html`<i class="fas fa-check pf-m-success"></i>`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -146,6 +146,7 @@ export class UserListPage extends WithBrandConfig(WithCapabilitiesConfig(TablePa
|
||||
search: this.search || "",
|
||||
pathStartswith: getURLParam("path", ""),
|
||||
isActive: this.hideDeactivated ? true : undefined,
|
||||
includeGroups: false,
|
||||
});
|
||||
this.userPaths = await new CoreApi(DEFAULT_CONFIG).coreUsersPathsRetrieve({
|
||||
search: this.search,
|
||||
|
@ -134,6 +134,12 @@ export class DeleteBulkForm<T> extends ModalButton {
|
||||
@property()
|
||||
buttonLabel = msg("Delete");
|
||||
|
||||
/**
|
||||
* Action shown in messages, for example `deleted` or `removed`
|
||||
*/
|
||||
@property()
|
||||
action = msg("deleted");
|
||||
|
||||
@property({ attribute: false })
|
||||
metadata: (item: T) => BulkDeleteMetadata = (item: T) => {
|
||||
const rec = item as Record<string, unknown>;
|
||||
|
@ -67,7 +67,7 @@ export class PermissionSelectModal extends TableModal<Permission> {
|
||||
renderModalInner(): TemplateResult {
|
||||
return html`<section class="pf-c-modal-box__header pf-c-page__main-section pf-m-light">
|
||||
<div class="pf-c-content">
|
||||
<h1 class="pf-c-title pf-m-2xl">${msg("Select permissions to grant")}</h1>
|
||||
<h1 class="pf-c-title pf-m-2xl">${msg("Select permissions to assign")}</h1>
|
||||
</div>
|
||||
</section>
|
||||
<section class="pf-c-modal-box__body pf-m-light">${this.renderTable()}</section>
|
||||
|
@ -113,9 +113,9 @@ export class RoleAssignedObjectPermissionTable extends Table<RoleAssignedObjectP
|
||||
baseRow.push(
|
||||
html`${granted
|
||||
? html`<pf-tooltip position="top" content=${msg("Directly assigned")}
|
||||
>✓</pf-tooltip
|
||||
>`
|
||||
: html`X`} `,
|
||||
><i class="fas fa-check pf-m-success"></i
|
||||
></pf-tooltip>`
|
||||
: html`<i class="fas fa-times pf-m-danger"></i>`} `,
|
||||
);
|
||||
});
|
||||
return baseRow;
|
||||
|
@ -93,19 +93,24 @@ export class UserObjectPermissionForm extends ModelForm<UserAssignData, number>
|
||||
>
|
||||
</ak-search-select>
|
||||
</ak-form-element-horizontal>
|
||||
${this.modelPermissions?.results.map((perm) => {
|
||||
return html` <ak-form-element-horizontal name="permissions.${perm.codename}">
|
||||
<label class="pf-c-switch">
|
||||
<input class="pf-c-switch__input" type="checkbox" />
|
||||
<span class="pf-c-switch__toggle">
|
||||
<span class="pf-c-switch__toggle-icon">
|
||||
<i class="fas fa-check" aria-hidden="true"></i>
|
||||
${this.modelPermissions?.results
|
||||
.filter((perm) => {
|
||||
const [_app, model] = this.model?.split(".") || "";
|
||||
return perm.codename !== `add_${model}`;
|
||||
})
|
||||
.map((perm) => {
|
||||
return html` <ak-form-element-horizontal name="permissions.${perm.codename}">
|
||||
<label class="pf-c-switch">
|
||||
<input class="pf-c-switch__input" type="checkbox" />
|
||||
<span class="pf-c-switch__toggle">
|
||||
<span class="pf-c-switch__toggle-icon">
|
||||
<i class="fas fa-check" aria-hidden="true"></i>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="pf-c-switch__label">${perm.name}</span>
|
||||
</label>
|
||||
</ak-form-element-horizontal>`;
|
||||
})}
|
||||
<span class="pf-c-switch__label">${perm.name}</span>
|
||||
</label>
|
||||
</ak-form-element-horizontal>`;
|
||||
})}
|
||||
</form>`;
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ export class UserAssignedObjectPermissionTable extends Table<UserAssignedObjectP
|
||||
ordering: "codename",
|
||||
});
|
||||
modelPermissions.results = modelPermissions.results.filter((value) => {
|
||||
return !value.codename.startsWith("add_");
|
||||
return value.codename !== `add_${this.model?.split(".")[1]}`;
|
||||
});
|
||||
this.modelPermissions = modelPermissions;
|
||||
return perms;
|
||||
@ -113,13 +113,15 @@ export class UserAssignedObjectPermissionTable extends Table<UserAssignedObjectP
|
||||
row(item: UserAssignedObjectPermission): TemplateResult[] {
|
||||
const baseRow = [html` <a href="#/identity/users/${item.pk}"> ${item.username} </a> `];
|
||||
this.modelPermissions?.results.forEach((perm) => {
|
||||
let cell = html`X`;
|
||||
let cell = html`<i class="fas fa-times pf-m-danger"></i>`;
|
||||
if (item.permissions.filter((uperm) => uperm.codename === perm.codename).length > 0) {
|
||||
cell = html`<pf-tooltip position="top" content=${msg("Directly assigned")}
|
||||
>✓</pf-tooltip
|
||||
>`;
|
||||
><i class="fas fa-check pf-m-success"></i
|
||||
></pf-tooltip>`;
|
||||
} else if (item.isSuperuser) {
|
||||
cell = html`<pf-tooltip position="top" content=${msg("Superuser")}>✓</pf-tooltip>`;
|
||||
cell = html`<pf-tooltip position="top" content=${msg("Superuser")}
|
||||
><i class="fas fa-check pf-m-success"></i
|
||||
></pf-tooltip>`;
|
||||
}
|
||||
baseRow.push(cell);
|
||||
});
|
||||
|
@ -131,7 +131,7 @@ export abstract class Table<T> extends AKElement implements TableLike {
|
||||
order?: string;
|
||||
|
||||
@property({ type: String })
|
||||
search: string = getURLParam("search", "");
|
||||
search: string = "";
|
||||
|
||||
@property({ type: Boolean })
|
||||
checkbox = false;
|
||||
@ -198,6 +198,9 @@ export abstract class Table<T> extends AKElement implements TableLike {
|
||||
this.selectedElements = [];
|
||||
}
|
||||
});
|
||||
if (this.searchEnabled()) {
|
||||
this.search = getURLParam("search", "");
|
||||
}
|
||||
}
|
||||
|
||||
public groupBy(items: T[]): [string, T[]][] {
|
||||
|
@ -440,13 +440,14 @@ export class FlowExecutor extends Interface implements StageHost {
|
||||
const logo = html`<div class="pf-c-login__main-header pf-c-brand ak-brand">
|
||||
<img src="${first(this.brand?.brandingLogo, "")}" alt="authentik Logo" />
|
||||
</div>`;
|
||||
const fallbackLoadSpinner = html`<ak-empty-state ?loading=${true} header=${msg("Loading")}>
|
||||
</ak-empty-state>`;
|
||||
if (!this.challenge) {
|
||||
return html`${logo}<ak-empty-state ?loading=${true} header=${msg("Loading")}>
|
||||
</ak-empty-state>`;
|
||||
return html`${logo}${fallbackLoadSpinner}`;
|
||||
}
|
||||
return html`
|
||||
${this.loading ? html`<ak-loading-overlay></ak-loading-overlay>` : nothing} ${logo}
|
||||
${until(this.renderChallenge())}
|
||||
${until(this.renderChallenge(), fallbackLoadSpinner)}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@ -1,72 +1,26 @@
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/EmptyState";
|
||||
import "@goauthentik/flow/FormStatic";
|
||||
import { BaseStage } from "@goauthentik/flow/stages/base";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { CSSResult, TemplateResult, html, nothing } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import PFDivider from "@patternfly/patternfly/components/Divider/divider.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import PFList from "@patternfly/patternfly/components/List/list.css";
|
||||
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
|
||||
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import { AccessDeniedChallenge, FlowChallengeResponseRequest } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-stage-access-denied-icon")
|
||||
export class AccessDeniedIcon extends AKElement {
|
||||
@property()
|
||||
errorMessage?: string;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
PFBase,
|
||||
PFTitle,
|
||||
PFDivider,
|
||||
css`
|
||||
.big-icon {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
height: 5rem;
|
||||
}
|
||||
.big-icon i {
|
||||
font-size: 3rem;
|
||||
}
|
||||
.reason {
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html` <div class="pf-c-form__group">
|
||||
<p class="big-icon">
|
||||
<i class="pf-icon pf-icon-error-circle-o"></i>
|
||||
</p>
|
||||
<h3 class="pf-c-title pf-m-3xl reason">${msg("Request has been denied.")}</h3>
|
||||
${this.errorMessage
|
||||
? html` <hr class="pf-c-divider" />
|
||||
<p>${this.errorMessage}</p>`
|
||||
: html``}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("ak-stage-access-denied")
|
||||
export class AccessDeniedStage extends BaseStage<
|
||||
AccessDeniedChallenge,
|
||||
FlowChallengeResponseRequest
|
||||
> {
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFLogin, PFForm, PFList, PFFormControl, PFTitle];
|
||||
return [PFBase, PFLogin, PFForm, PFFormControl];
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
@ -90,10 +44,15 @@ export class AccessDeniedStage extends BaseStage<
|
||||
>
|
||||
</div>
|
||||
</ak-form-static>
|
||||
<ak-stage-access-denied-icon
|
||||
errorMessage=${ifDefined(this.challenge.errorMessage)}
|
||||
>
|
||||
</ak-stage-access-denied-icon>
|
||||
<ak-empty-state icon="fa-times" header=${msg("Request has been denied.")}>
|
||||
${this.challenge.errorMessage
|
||||
? html`
|
||||
<div slot="body">
|
||||
<p>${this.challenge.errorMessage}</p>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</ak-empty-state>
|
||||
</form>
|
||||
</div>
|
||||
<footer class="pf-c-login__main-footer">
|
||||
|
119
web/src/flow/stages/captcha/CaptchaStage.stories.ts
Normal file
119
web/src/flow/stages/captcha/CaptchaStage.stories.ts
Normal file
@ -0,0 +1,119 @@
|
||||
import type { StoryObj } from "@storybook/web-components";
|
||||
|
||||
import { html } from "lit";
|
||||
|
||||
import "@patternfly/patternfly/components/Login/login.css";
|
||||
|
||||
import { CaptchaChallenge, ChallengeChoices, UiThemeEnum } from "@goauthentik/api";
|
||||
|
||||
import "../../../stories/flow-interface";
|
||||
import "./CaptchaStage";
|
||||
|
||||
export default {
|
||||
title: "Flow / Stages / CaptchaStage",
|
||||
};
|
||||
|
||||
export const LoadingNoChallenge = () => {
|
||||
return html`<ak-storybook-interface theme=${UiThemeEnum.Dark}>
|
||||
<div class="pf-c-login">
|
||||
<div class="pf-c-login__container">
|
||||
<div class="pf-c-login__main">
|
||||
<ak-stage-captcha></ak-stage-captcha>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ak-storybook-interface>`;
|
||||
};
|
||||
|
||||
export const ChallengeGoogleReCaptcha: StoryObj = {
|
||||
render: ({ theme, challenge }) => {
|
||||
return html`<ak-storybook-interface theme=${theme}>
|
||||
<div class="pf-c-login">
|
||||
<div class="pf-c-login__container">
|
||||
<div class="pf-c-login__main">
|
||||
<ak-stage-captcha .challenge=${challenge}></ak-stage-captcha>
|
||||
</div>
|
||||
</div></div
|
||||
></ak-storybook-interface>`;
|
||||
},
|
||||
args: {
|
||||
theme: "automatic",
|
||||
challenge: {
|
||||
type: ChallengeChoices.Native,
|
||||
pendingUser: "foo",
|
||||
pendingUserAvatar: "https://picsum.photos/64",
|
||||
jsUrl: "https://www.google.com/recaptcha/api.js",
|
||||
siteKey: "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI",
|
||||
} as CaptchaChallenge,
|
||||
},
|
||||
argTypes: {
|
||||
theme: {
|
||||
options: [UiThemeEnum.Automatic, UiThemeEnum.Light, UiThemeEnum.Dark],
|
||||
control: {
|
||||
type: "select",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const ChallengeHCaptcha: StoryObj = {
|
||||
render: ({ theme, challenge }) => {
|
||||
return html`<ak-storybook-interface theme=${theme}>
|
||||
<div class="pf-c-login">
|
||||
<div class="pf-c-login__container">
|
||||
<div class="pf-c-login__main">
|
||||
<ak-stage-captcha .challenge=${challenge}></ak-stage-captcha>
|
||||
</div>
|
||||
</div></div
|
||||
></ak-storybook-interface>`;
|
||||
},
|
||||
args: {
|
||||
theme: "automatic",
|
||||
challenge: {
|
||||
type: ChallengeChoices.Native,
|
||||
pendingUser: "foo",
|
||||
pendingUserAvatar: "https://picsum.photos/64",
|
||||
jsUrl: "https://js.hcaptcha.com/1/api.js",
|
||||
siteKey: "10000000-ffff-ffff-ffff-000000000001",
|
||||
} as CaptchaChallenge,
|
||||
},
|
||||
argTypes: {
|
||||
theme: {
|
||||
options: [UiThemeEnum.Automatic, UiThemeEnum.Light, UiThemeEnum.Dark],
|
||||
control: {
|
||||
type: "select",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const ChallengeTurnstile: StoryObj = {
|
||||
render: ({ theme, challenge }) => {
|
||||
return html`<ak-storybook-interface theme=${theme}>
|
||||
<div class="pf-c-login">
|
||||
<div class="pf-c-login__container">
|
||||
<div class="pf-c-login__main">
|
||||
<ak-stage-captcha .challenge=${challenge}></ak-stage-captcha>
|
||||
</div>
|
||||
</div></div
|
||||
></ak-storybook-interface>`;
|
||||
},
|
||||
args: {
|
||||
theme: "automatic",
|
||||
challenge: {
|
||||
type: ChallengeChoices.Native,
|
||||
pendingUser: "foo",
|
||||
pendingUserAvatar: "https://picsum.photos/64",
|
||||
jsUrl: "https://challenges.cloudflare.com/turnstile/v0/api.js",
|
||||
siteKey: "1x00000000000000000000BB",
|
||||
} as CaptchaChallenge,
|
||||
},
|
||||
argTypes: {
|
||||
theme: {
|
||||
options: [UiThemeEnum.Automatic, UiThemeEnum.Light, UiThemeEnum.Dark],
|
||||
control: {
|
||||
type: "select",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
@ -1,9 +1,7 @@
|
||||
///<reference types="@hcaptcha/types"/>
|
||||
import { PFSize } from "@goauthentik/common/enums.js";
|
||||
import "@goauthentik/elements/EmptyState";
|
||||
import "@goauthentik/elements/forms/FormElement";
|
||||
import "@goauthentik/flow/FormStatic";
|
||||
import "@goauthentik/flow/stages/access_denied/AccessDeniedStage";
|
||||
import { BaseStage } from "@goauthentik/flow/stages/base";
|
||||
import type { TurnstileObject } from "turnstile-types";
|
||||
|
||||
@ -25,6 +23,8 @@ interface TurnstileWindow extends Window {
|
||||
turnstile: TurnstileObject;
|
||||
}
|
||||
|
||||
const captchaContainerID = "captcha-container";
|
||||
|
||||
@customElement("ak-stage-captcha")
|
||||
export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeResponseRequest> {
|
||||
static get styles(): CSSResult[] {
|
||||
@ -36,13 +36,23 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
@state()
|
||||
error?: string;
|
||||
|
||||
@state()
|
||||
captchaInteractive: boolean = true;
|
||||
|
||||
@state()
|
||||
captchaContainer: HTMLDivElement;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.captchaContainer = document.createElement("div");
|
||||
this.captchaContainer.id = captchaContainerID;
|
||||
}
|
||||
|
||||
firstUpdated(): void {
|
||||
const script = document.createElement("script");
|
||||
script.src = this.challenge.jsUrl;
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
const captchaContainer = document.createElement("div");
|
||||
document.body.appendChild(captchaContainer);
|
||||
script.onload = () => {
|
||||
console.debug("authentik/stages/captcha: script loaded");
|
||||
let found = false;
|
||||
@ -51,7 +61,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
let handlerFound = false;
|
||||
try {
|
||||
console.debug(`authentik/stages/captcha[${handler.name}]: trying handler`);
|
||||
handlerFound = handler.apply(this, [captchaContainer]);
|
||||
handlerFound = handler.apply(this);
|
||||
if (handlerFound) {
|
||||
console.debug(
|
||||
`authentik/stages/captcha[${handler.name}]: handler succeeded`,
|
||||
@ -74,12 +84,14 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
handleGReCaptcha(container: HTMLDivElement): boolean {
|
||||
handleGReCaptcha(): boolean {
|
||||
if (!Object.hasOwn(window, "grecaptcha")) {
|
||||
return false;
|
||||
}
|
||||
this.captchaInteractive = false;
|
||||
document.body.appendChild(this.captchaContainer);
|
||||
grecaptcha.ready(() => {
|
||||
const captchaId = grecaptcha.render(container, {
|
||||
const captchaId = grecaptcha.render(this.captchaContainer, {
|
||||
sitekey: this.challenge.siteKey,
|
||||
callback: (token) => {
|
||||
this.host?.submit({
|
||||
@ -93,11 +105,13 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
return true;
|
||||
}
|
||||
|
||||
handleHCaptcha(container: HTMLDivElement): boolean {
|
||||
handleHCaptcha(): boolean {
|
||||
if (!Object.hasOwn(window, "hcaptcha")) {
|
||||
return false;
|
||||
}
|
||||
const captchaId = hcaptcha.render(container, {
|
||||
this.captchaInteractive = false;
|
||||
document.body.appendChild(this.captchaContainer);
|
||||
const captchaId = hcaptcha.render(this.captchaContainer, {
|
||||
sitekey: this.challenge.siteKey,
|
||||
size: "invisible",
|
||||
callback: (token) => {
|
||||
@ -110,11 +124,13 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
return true;
|
||||
}
|
||||
|
||||
handleTurnstile(container: HTMLDivElement): boolean {
|
||||
handleTurnstile(): boolean {
|
||||
if (!Object.hasOwn(window, "turnstile")) {
|
||||
return false;
|
||||
}
|
||||
(window as unknown as TurnstileWindow).turnstile.render(container, {
|
||||
this.captchaInteractive = false;
|
||||
document.body.appendChild(this.captchaContainer);
|
||||
(window as unknown as TurnstileWindow).turnstile.render(`#${captchaContainerID}`, {
|
||||
sitekey: this.challenge.siteKey,
|
||||
callback: (token) => {
|
||||
this.host?.submit({
|
||||
@ -125,6 +141,19 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
return true;
|
||||
}
|
||||
|
||||
renderBody(): TemplateResult {
|
||||
if (this.error) {
|
||||
return html`<ak-empty-state icon="fa-times" header=${this.error}> </ak-empty-state>`;
|
||||
}
|
||||
if (this.captchaInteractive) {
|
||||
return html`${this.captchaContainer}`;
|
||||
}
|
||||
return html`<ak-empty-state
|
||||
?loading=${true}
|
||||
header=${msg("Verifying...")}
|
||||
></ak-empty-state>`;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
if (!this.challenge) {
|
||||
return html`<ak-empty-state ?loading="${true}" header=${msg("Loading")}>
|
||||
@ -146,12 +175,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
>
|
||||
</div>
|
||||
</ak-form-static>
|
||||
${this.error
|
||||
? html`<ak-stage-access-denied-icon errorMessage=${ifDefined(this.error)}>
|
||||
</ak-stage-access-denied-icon>`
|
||||
: html`<div>
|
||||
<ak-spinner size=${PFSize.XLarge}></ak-spinner>
|
||||
</div>`}
|
||||
${this.renderBody()}
|
||||
</form>
|
||||
</div>
|
||||
<footer class="pf-c-login__main-footer">
|
||||
|
@ -194,14 +194,14 @@ export class IdentificationStage extends BaseStage<
|
||||
${msg("Need an account?")}
|
||||
<a id="enroll" href="${this.challenge.enrollUrl}">${msg("Sign up.")}</a>
|
||||
</p>`
|
||||
: html``}
|
||||
: nothing}
|
||||
${this.challenge.recoveryUrl
|
||||
? html`<p class="pf-c-login__main-footer-band-item">
|
||||
<a id="recovery" href="${this.challenge.recoveryUrl}"
|
||||
>${msg("Forgot username or password?")}</a
|
||||
>
|
||||
</p>`
|
||||
: html``}
|
||||
: nothing}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@ -265,26 +265,18 @@ export class IdentificationStage extends BaseStage<
|
||||
/>
|
||||
</ak-form-element>
|
||||
`
|
||||
: html``}
|
||||
: nothing}
|
||||
${"non_field_errors" in (this.challenge?.responseErrors || {})
|
||||
? this.renderNonFieldErrors(this.challenge?.responseErrors?.non_field_errors || [])
|
||||
: html``}
|
||||
: nothing}
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||
${this.challenge.primaryAction}
|
||||
</button>
|
||||
</div>
|
||||
${this.challenge.passwordlessUrl
|
||||
? html`<ak-divider>${msg("Or")}</ak-divider>
|
||||
<div>
|
||||
<a
|
||||
href=${this.challenge.passwordlessUrl}
|
||||
class="pf-c-button pf-m-secondary pf-m-block"
|
||||
>
|
||||
${msg("Use a security key")}
|
||||
</a>
|
||||
</div>`
|
||||
: html``}`;
|
||||
? html`<ak-divider>${msg("Or")}</ak-divider>`
|
||||
: nothing}`;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
@ -306,8 +298,20 @@ export class IdentificationStage extends BaseStage<
|
||||
? html`<p>
|
||||
${msg(str`Login to continue to ${this.challenge.applicationPre}.`)}
|
||||
</p>`
|
||||
: html``}
|
||||
: nothing}
|
||||
${this.renderInput()}
|
||||
${this.challenge.passwordlessUrl
|
||||
? html`
|
||||
<div>
|
||||
<a
|
||||
href=${this.challenge.passwordlessUrl}
|
||||
class="pf-c-button pf-m-secondary pf-m-block"
|
||||
>
|
||||
${msg("Use a security key")}
|
||||
</a>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</form>
|
||||
</div>
|
||||
<footer class="pf-c-login__main-footer">
|
||||
|
@ -5768,9 +5768,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="se9c07cf256774d81">
|
||||
<source>Editing is disabled for managed tokens</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78ab26da7f067de8">
|
||||
<source>Select permissions to grant</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdeb90bfd8a80b86b">
|
||||
<source>Permissions to add</source>
|
||||
</trans-unit>
|
||||
@ -6507,6 +6504,33 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa6b2c110466d1754">
|
||||
<source>Default length of generated tokens</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1f25e1e20469a9ea">
|
||||
<source>deleted</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0ca04e397298bc43">
|
||||
<source>Select permissions to assign</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s676d94e7e31a8075">
|
||||
<source>SCIM Source is in preview.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s31f1afc0a81977c1">
|
||||
<source>Update SCIM Source</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4bb356adc8a7f85b">
|
||||
<source>SCIM Base URL</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb23304fc42c5d6d9">
|
||||
<source>Provisioned Users</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6a81ee82b2e5ecbb">
|
||||
<source>Provisioned Groups</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s10154dbd4fbc697b">
|
||||
<source>removed</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s30d6ff9e15e0a40a">
|
||||
<source>Verifying...</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -6037,9 +6037,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="se9c07cf256774d81">
|
||||
<source>Editing is disabled for managed tokens</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78ab26da7f067de8">
|
||||
<source>Select permissions to grant</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdeb90bfd8a80b86b">
|
||||
<source>Permissions to add</source>
|
||||
</trans-unit>
|
||||
@ -6776,6 +6773,33 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa6b2c110466d1754">
|
||||
<source>Default length of generated tokens</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1f25e1e20469a9ea">
|
||||
<source>deleted</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0ca04e397298bc43">
|
||||
<source>Select permissions to assign</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s676d94e7e31a8075">
|
||||
<source>SCIM Source is in preview.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s31f1afc0a81977c1">
|
||||
<source>Update SCIM Source</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4bb356adc8a7f85b">
|
||||
<source>SCIM Base URL</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb23304fc42c5d6d9">
|
||||
<source>Provisioned Users</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6a81ee82b2e5ecbb">
|
||||
<source>Provisioned Groups</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s10154dbd4fbc697b">
|
||||
<source>removed</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s30d6ff9e15e0a40a">
|
||||
<source>Verifying...</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -5685,9 +5685,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="se9c07cf256774d81">
|
||||
<source>Editing is disabled for managed tokens</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78ab26da7f067de8">
|
||||
<source>Select permissions to grant</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdeb90bfd8a80b86b">
|
||||
<source>Permissions to add</source>
|
||||
</trans-unit>
|
||||
@ -6424,6 +6421,33 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa6b2c110466d1754">
|
||||
<source>Default length of generated tokens</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1f25e1e20469a9ea">
|
||||
<source>deleted</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0ca04e397298bc43">
|
||||
<source>Select permissions to assign</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s676d94e7e31a8075">
|
||||
<source>SCIM Source is in preview.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s31f1afc0a81977c1">
|
||||
<source>Update SCIM Source</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4bb356adc8a7f85b">
|
||||
<source>SCIM Base URL</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb23304fc42c5d6d9">
|
||||
<source>Provisioned Users</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6a81ee82b2e5ecbb">
|
||||
<source>Provisioned Groups</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s10154dbd4fbc697b">
|
||||
<source>removed</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s30d6ff9e15e0a40a">
|
||||
<source>Verifying...</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -7575,10 +7575,6 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
|
||||
<source>Editing is disabled for managed tokens</source>
|
||||
<target>L'édition est désactivée pour les jetons gérés</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78ab26da7f067de8">
|
||||
<source>Select permissions to grant</source>
|
||||
<target>Sélectionner les permissions à attribuer</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdeb90bfd8a80b86b">
|
||||
<source>Permissions to add</source>
|
||||
<target>Permissions à ajouter</target>
|
||||
@ -8535,6 +8531,33 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
|
||||
</trans-unit>
|
||||
<trans-unit id="sa6b2c110466d1754">
|
||||
<source>Default length of generated tokens</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1f25e1e20469a9ea">
|
||||
<source>deleted</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0ca04e397298bc43">
|
||||
<source>Select permissions to assign</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s676d94e7e31a8075">
|
||||
<source>SCIM Source is in preview.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s31f1afc0a81977c1">
|
||||
<source>Update SCIM Source</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4bb356adc8a7f85b">
|
||||
<source>SCIM Base URL</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb23304fc42c5d6d9">
|
||||
<source>Provisioned Users</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6a81ee82b2e5ecbb">
|
||||
<source>Provisioned Groups</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s10154dbd4fbc697b">
|
||||
<source>removed</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s30d6ff9e15e0a40a">
|
||||
<source>Verifying...</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -7540,10 +7540,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<source>Editing is disabled for managed tokens</source>
|
||||
<target>관리되는 토큰의 경우 편집이 비활성화됩니다.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78ab26da7f067de8">
|
||||
<source>Select permissions to grant</source>
|
||||
<target>부여할 권한 선택</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdeb90bfd8a80b86b">
|
||||
<source>Permissions to add</source>
|
||||
<target>추가할 권한</target>
|
||||
@ -8363,6 +8359,33 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa6b2c110466d1754">
|
||||
<source>Default length of generated tokens</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1f25e1e20469a9ea">
|
||||
<source>deleted</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0ca04e397298bc43">
|
||||
<source>Select permissions to assign</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s676d94e7e31a8075">
|
||||
<source>SCIM Source is in preview.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s31f1afc0a81977c1">
|
||||
<source>Update SCIM Source</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4bb356adc8a7f85b">
|
||||
<source>SCIM Base URL</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb23304fc42c5d6d9">
|
||||
<source>Provisioned Users</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6a81ee82b2e5ecbb">
|
||||
<source>Provisioned Groups</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s10154dbd4fbc697b">
|
||||
<source>removed</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s30d6ff9e15e0a40a">
|
||||
<source>Verifying...</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -7677,9 +7677,6 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
|
||||
<trans-unit id="s895514dda9cb9c94">
|
||||
<source>Create recovery link</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s78ab26da7f067de8">
|
||||
<source>Select permissions to grant</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdeb90bfd8a80b86b">
|
||||
<source>Permissions to add</source>
|
||||
</trans-unit>
|
||||
@ -8206,6 +8203,33 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
|
||||
</trans-unit>
|
||||
<trans-unit id="sa6b2c110466d1754">
|
||||
<source>Default length of generated tokens</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1f25e1e20469a9ea">
|
||||
<source>deleted</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0ca04e397298bc43">
|
||||
<source>Select permissions to assign</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s676d94e7e31a8075">
|
||||
<source>SCIM Source is in preview.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s31f1afc0a81977c1">
|
||||
<source>Update SCIM Source</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4bb356adc8a7f85b">
|
||||
<source>SCIM Base URL</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb23304fc42c5d6d9">
|
||||
<source>Provisioned Users</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6a81ee82b2e5ecbb">
|
||||
<source>Provisioned Groups</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s10154dbd4fbc697b">
|
||||
<source>removed</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s30d6ff9e15e0a40a">
|
||||
<source>Verifying...</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user