Compare commits
46 Commits
blueprint-
...
reduce-mem
Author | SHA1 | Date | |
---|---|---|---|
7d0abbf072 | |||
640d0a4a95 | |||
6b8782556c | |||
7f6f3b6602 | |||
3367ac0e08 | |||
d5ea0ffdc6 | |||
93f1638b39 | |||
37525175fa | |||
0db1e52f90 | |||
3e8620b686 | |||
6687ffc6d2 | |||
e265ee253b | |||
7763a3673c | |||
d99005e130 | |||
c61f96e770 | |||
83622dd934 | |||
2eebd0eaa1 | |||
b61d918c5c | |||
076a4f4772 | |||
b3872b35f8 | |||
f06534cdf0 | |||
c528a6c336 | |||
821f06ffdf | |||
e83d040a48 | |||
9affd90850 | |||
80d84cb03f | |||
a9cc5fdafe | |||
b45109afce | |||
c8711d9f8f | |||
40a7135c0c | |||
675a4a6788 | |||
98b5b75f29 | |||
22b0a1bd23 | |||
1a1d499833 | |||
1573cfbaa1 | |||
b88ce32111 | |||
a1965ceada | |||
9c536a1b4b | |||
f3e0ff2833 | |||
06dc47b582 | |||
a4bf24a039 | |||
1715c3e268 | |||
feb3be7cee | |||
db05232f12 | |||
ebfa7dbcfc | |||
8c4dab7399 |
@ -1,16 +1,16 @@
|
||||
[bumpversion]
|
||||
current_version = 2024.10.5
|
||||
current_version = 2024.12.0
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
|
||||
serialize =
|
||||
serialize =
|
||||
{major}.{minor}.{patch}-{rc_t}{rc_n}
|
||||
{major}.{minor}.{patch}
|
||||
message = release: {new_version}
|
||||
tag_name = version/{new_version}
|
||||
|
||||
[bumpversion:part:rc_t]
|
||||
values =
|
||||
values =
|
||||
rc
|
||||
final
|
||||
optional_value = final
|
||||
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -33,7 +33,8 @@
|
||||
"!If sequence",
|
||||
"!Index scalar",
|
||||
"!KeyOf scalar",
|
||||
"!Value scalar"
|
||||
"!Value scalar",
|
||||
"!AtIndex scalar"
|
||||
],
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
"typescript.preferences.importModuleSpecifierEnding": "index",
|
||||
|
@ -25,6 +25,9 @@ CODEOWNERS @goauthentik/infrastructure
|
||||
# Web
|
||||
web/ @goauthentik/frontend
|
||||
tests/wdio/ @goauthentik/frontend
|
||||
# Locale
|
||||
locale/ @goauthentik/backend @goauthentik/frontend
|
||||
web/xliff/ @goauthentik/backend @goauthentik/frontend
|
||||
# Docs & Website
|
||||
website/ @goauthentik/docs
|
||||
CODE_OF_CONDUCT.md @goauthentik/docs
|
||||
|
@ -20,8 +20,8 @@ Even if the issue is not a CVE, we still greatly appreciate your help in hardeni
|
||||
|
||||
| Version | Supported |
|
||||
| --------- | --------- |
|
||||
| 2024.8.x | ✅ |
|
||||
| 2024.10.x | ✅ |
|
||||
| 2024.12.x | ✅ |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
from os import environ
|
||||
|
||||
__version__ = "2024.10.5"
|
||||
__version__ = "2024.12.0"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
|
||||
|
@ -126,7 +126,7 @@ class Command(BaseCommand):
|
||||
def_name_perm = f"model_{model_path}_permissions"
|
||||
def_path_perm = f"#/$defs/{def_name_perm}"
|
||||
self.schema["$defs"][def_name_perm] = self.model_permissions(model)
|
||||
return {
|
||||
template = {
|
||||
"type": "object",
|
||||
"required": ["model", "identifiers"],
|
||||
"properties": {
|
||||
@ -143,6 +143,11 @@ class Command(BaseCommand):
|
||||
"identifiers": {"$ref": def_path},
|
||||
},
|
||||
}
|
||||
# Meta models don't require identifiers, as there's no matching database model to find
|
||||
if issubclass(model, BaseMetaModel):
|
||||
del template["properties"]["identifiers"]
|
||||
template["required"].remove("identifiers")
|
||||
return template
|
||||
|
||||
def field_to_jsonschema(self, field: Field) -> dict:
|
||||
"""Convert a single field to json schema"""
|
||||
|
@ -146,6 +146,10 @@ entries:
|
||||
]
|
||||
]
|
||||
nested_context: !Context context2
|
||||
at_index_sequence: !AtIndex [!Context sequence, 0]
|
||||
at_index_sequence_default: !AtIndex [!Context sequence, 100, "non existent"]
|
||||
at_index_mapping: !AtIndex [!Context mapping, "key2"]
|
||||
at_index_mapping_default: !AtIndex [!Context mapping, "invalid", "non existent"]
|
||||
identifiers:
|
||||
name: test
|
||||
conditions:
|
||||
|
@ -215,6 +215,10 @@ class TestBlueprintsV1(TransactionTestCase):
|
||||
},
|
||||
"nested_context": "context-nested-value",
|
||||
"env_null": None,
|
||||
"at_index_sequence": "foo",
|
||||
"at_index_sequence_default": "non existent",
|
||||
"at_index_mapping": 2,
|
||||
"at_index_mapping_default": "non existent",
|
||||
}
|
||||
).exists()
|
||||
)
|
||||
|
@ -24,6 +24,10 @@ from authentik.lib.sentry import SentryIgnoredException
|
||||
from authentik.policies.models import PolicyBindingModel
|
||||
|
||||
|
||||
class UNSET:
|
||||
"""Used to test whether a key has not been set."""
|
||||
|
||||
|
||||
def get_attrs(obj: SerializerModel) -> dict[str, Any]:
|
||||
"""Get object's attributes via their serializer, and convert it to a normal dict"""
|
||||
serializer: Serializer = obj.serializer(obj)
|
||||
@ -556,6 +560,53 @@ class Value(EnumeratedItem):
|
||||
raise EntryInvalidError.from_entry(f"Empty/invalid context: {context}", entry) from exc
|
||||
|
||||
|
||||
class AtIndex(YAMLTag):
|
||||
"""Get value at index of a sequence or mapping"""
|
||||
|
||||
obj: YAMLTag | dict | list | tuple
|
||||
attribute: int | str | YAMLTag
|
||||
default: Any | UNSET
|
||||
|
||||
def __init__(self, loader: "BlueprintLoader", node: SequenceNode) -> None:
|
||||
super().__init__()
|
||||
self.obj = loader.construct_object(node.value[0])
|
||||
self.attribute = loader.construct_object(node.value[1])
|
||||
if len(node.value) == 2: # noqa: PLR2004
|
||||
self.default = UNSET
|
||||
else:
|
||||
self.default = loader.construct_object(node.value[2])
|
||||
|
||||
def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any:
|
||||
if isinstance(self.obj, YAMLTag):
|
||||
obj = self.obj.resolve(entry, blueprint)
|
||||
else:
|
||||
obj = self.obj
|
||||
if isinstance(self.attribute, YAMLTag):
|
||||
attribute = self.attribute.resolve(entry, blueprint)
|
||||
else:
|
||||
attribute = self.attribute
|
||||
|
||||
if isinstance(obj, list | tuple):
|
||||
try:
|
||||
return obj[attribute]
|
||||
except TypeError as exc:
|
||||
raise EntryInvalidError.from_entry(
|
||||
f"Invalid index for list: {attribute}", entry
|
||||
) from exc
|
||||
except IndexError as exc:
|
||||
if self.default is UNSET:
|
||||
raise EntryInvalidError.from_entry(
|
||||
f"Index out of range: {attribute}", entry
|
||||
) from exc
|
||||
return self.default
|
||||
if attribute in obj:
|
||||
return obj[attribute]
|
||||
else:
|
||||
if self.default is UNSET:
|
||||
raise EntryInvalidError.from_entry(f"Key does not exist: {attribute}", entry)
|
||||
return self.default
|
||||
|
||||
|
||||
class BlueprintDumper(SafeDumper):
|
||||
"""Dump dataclasses to yaml"""
|
||||
|
||||
@ -606,6 +657,7 @@ class BlueprintLoader(SafeLoader):
|
||||
self.add_constructor("!Enumerate", Enumerate)
|
||||
self.add_constructor("!Value", Value)
|
||||
self.add_constructor("!Index", Index)
|
||||
self.add_constructor("!AtIndex", AtIndex)
|
||||
|
||||
|
||||
class EntryInvalidError(SentryIgnoredException):
|
||||
|
54
authentik/core/api/application_entitlements.py
Normal file
54
authentik/core/api/application_entitlements.py
Normal file
@ -0,0 +1,54 @@
|
||||
"""Application Roles API Viewset"""
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import ModelSerializer
|
||||
from authentik.core.models import (
|
||||
Application,
|
||||
ApplicationEntitlement,
|
||||
User,
|
||||
)
|
||||
|
||||
|
||||
class ApplicationEntitlementSerializer(ModelSerializer):
|
||||
"""ApplicationEntitlement Serializer"""
|
||||
|
||||
def validate_app(self, app: Application) -> Application:
|
||||
"""Ensure user has permission to view"""
|
||||
user: User = self._context["request"].user
|
||||
if user.has_perm("view_application", app) or user.has_perm(
|
||||
"authentik_core.view_application"
|
||||
):
|
||||
return app
|
||||
raise ValidationError(_("User does not have access to application."), code="invalid")
|
||||
|
||||
class Meta:
|
||||
model = ApplicationEntitlement
|
||||
fields = [
|
||||
"pbm_uuid",
|
||||
"name",
|
||||
"app",
|
||||
"attributes",
|
||||
]
|
||||
|
||||
|
||||
class ApplicationEntitlementViewSet(UsedByMixin, ModelViewSet):
|
||||
"""ApplicationEntitlement Viewset"""
|
||||
|
||||
queryset = ApplicationEntitlement.objects.all()
|
||||
serializer_class = ApplicationEntitlementSerializer
|
||||
search_fields = [
|
||||
"pbm_uuid",
|
||||
"name",
|
||||
"app",
|
||||
"attributes",
|
||||
]
|
||||
filterset_fields = [
|
||||
"pbm_uuid",
|
||||
"name",
|
||||
"app",
|
||||
]
|
||||
ordering = ["name"]
|
@ -159,9 +159,9 @@ class SourceViewSet(
|
||||
|
||||
|
||||
class UserSourceConnectionSerializer(SourceSerializer):
|
||||
"""OAuth Source Serializer"""
|
||||
"""User source connection"""
|
||||
|
||||
source = SourceSerializer(read_only=True)
|
||||
source_obj = SourceSerializer(read_only=True, source="source")
|
||||
|
||||
class Meta:
|
||||
model = UserSourceConnection
|
||||
@ -169,10 +169,10 @@ class UserSourceConnectionSerializer(SourceSerializer):
|
||||
"pk",
|
||||
"user",
|
||||
"source",
|
||||
"source_obj",
|
||||
"created",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"user": {"read_only": True},
|
||||
"created": {"read_only": True},
|
||||
}
|
||||
|
||||
@ -197,9 +197,9 @@ class UserSourceConnectionViewSet(
|
||||
|
||||
|
||||
class GroupSourceConnectionSerializer(SourceSerializer):
|
||||
"""Group Source Connection Serializer"""
|
||||
"""Group Source Connection"""
|
||||
|
||||
source = SourceSerializer(read_only=True)
|
||||
source_obj = SourceSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = GroupSourceConnection
|
||||
@ -207,12 +207,11 @@ class GroupSourceConnectionSerializer(SourceSerializer):
|
||||
"pk",
|
||||
"group",
|
||||
"source",
|
||||
"source_obj",
|
||||
"identifier",
|
||||
"created",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"group": {"read_only": True},
|
||||
"identifier": {"read_only": True},
|
||||
"created": {"read_only": True},
|
||||
}
|
||||
|
||||
|
@ -22,7 +22,7 @@ from authentik.blueprints.v1.common import (
|
||||
from authentik.blueprints.v1.importer import Importer
|
||||
from authentik.core.api.applications import ApplicationSerializer
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.core.models import Provider
|
||||
from authentik.core.models import Application, Provider
|
||||
from authentik.lib.utils.reflection import all_subclasses
|
||||
from authentik.policies.api.bindings import PolicyBindingSerializer
|
||||
|
||||
@ -51,6 +51,13 @@ class TransactionProviderField(DictField):
|
||||
class TransactionPolicyBindingSerializer(PolicyBindingSerializer):
|
||||
"""PolicyBindingSerializer which does not require target as target is set implicitly"""
|
||||
|
||||
def validate(self, attrs):
|
||||
# As the PolicyBindingSerializer checks that the correct things can be bound to a target
|
||||
# but we don't have a target here as that's set by the blueprint, pass in an empty app
|
||||
# which will have the correct allowed combination of group/user/policy.
|
||||
attrs["target"] = Application()
|
||||
return super().validate(attrs)
|
||||
|
||||
class Meta(PolicyBindingSerializer.Meta):
|
||||
fields = [x for x in PolicyBindingSerializer.Meta.fields if x != "target"]
|
||||
|
||||
|
45
authentik/core/migrations/0041_applicationentitlement.py
Normal file
45
authentik/core/migrations/0041_applicationentitlement.py
Normal file
@ -0,0 +1,45 @@
|
||||
# Generated by Django 5.0.9 on 2024-11-20 15:16
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0040_provider_invalidation_flow"),
|
||||
("authentik_policies", "0011_policybinding_failure_result_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="ApplicationEntitlement",
|
||||
fields=[
|
||||
(
|
||||
"policybindingmodel_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="authentik_policies.policybindingmodel",
|
||||
),
|
||||
),
|
||||
("attributes", models.JSONField(blank=True, default=dict)),
|
||||
("name", models.TextField()),
|
||||
(
|
||||
"app",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="authentik_core.application"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Application Entitlement",
|
||||
"verbose_name_plural": "Application Entitlements",
|
||||
"unique_together": {("app", "name")},
|
||||
},
|
||||
bases=("authentik_policies.policybindingmodel", models.Model),
|
||||
),
|
||||
]
|
@ -314,6 +314,32 @@ class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
|
||||
always_merger.merge(final_attributes, self.attributes)
|
||||
return final_attributes
|
||||
|
||||
def app_entitlements(self, app: "Application | None") -> QuerySet["ApplicationEntitlement"]:
|
||||
"""Get all entitlements this user has for `app`."""
|
||||
if not app:
|
||||
return []
|
||||
all_groups = self.all_groups()
|
||||
qs = app.applicationentitlement_set.filter(
|
||||
Q(
|
||||
Q(bindings__user=self) | Q(bindings__group__in=all_groups),
|
||||
bindings__negate=False,
|
||||
)
|
||||
| Q(
|
||||
Q(~Q(bindings__user=self), bindings__user__isnull=False)
|
||||
| Q(~Q(bindings__group__in=all_groups), bindings__group__isnull=False),
|
||||
bindings__negate=True,
|
||||
),
|
||||
bindings__enabled=True,
|
||||
).order_by("name")
|
||||
return qs
|
||||
|
||||
def app_entitlements_attributes(self, app: "Application | None") -> dict:
|
||||
"""Get a dictionary containing all merged attributes from app entitlements for `app`."""
|
||||
final_attributes = {}
|
||||
for attrs in self.app_entitlements(app).values_list("attributes", flat=True):
|
||||
always_merger.merge(final_attributes, attrs)
|
||||
return final_attributes
|
||||
|
||||
@property
|
||||
def serializer(self) -> Serializer:
|
||||
from authentik.core.api.users import UserSerializer
|
||||
@ -581,6 +607,31 @@ class Application(SerializerModel, PolicyBindingModel):
|
||||
verbose_name_plural = _("Applications")
|
||||
|
||||
|
||||
class ApplicationEntitlement(AttributesMixin, SerializerModel, PolicyBindingModel):
|
||||
"""Application-scoped entitlement to control authorization in an application"""
|
||||
|
||||
name = models.TextField()
|
||||
|
||||
app = models.ForeignKey(Application, on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Application Entitlement")
|
||||
verbose_name_plural = _("Application Entitlements")
|
||||
unique_together = (("app", "name"),)
|
||||
|
||||
def __str__(self):
|
||||
return f"Application Entitlement {self.name} for app {self.app_id}"
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
from authentik.core.api.application_entitlements import ApplicationEntitlementSerializer
|
||||
|
||||
return ApplicationEntitlementSerializer
|
||||
|
||||
def supported_policy_binding_targets(self):
|
||||
return ["group", "user"]
|
||||
|
||||
|
||||
class SourceUserMatchingModes(models.TextChoices):
|
||||
"""Different modes a source can handle new/returning users"""
|
||||
|
||||
|
@ -238,13 +238,7 @@ class SourceFlowManager:
|
||||
self.request.GET,
|
||||
flow_slug=flow_slug,
|
||||
)
|
||||
# Ensure redirect is carried through when user was trying to
|
||||
# authorize application
|
||||
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
|
||||
NEXT_ARG_NAME, "authentik_core:if-user"
|
||||
)
|
||||
if PLAN_CONTEXT_REDIRECT not in flow_context:
|
||||
flow_context[PLAN_CONTEXT_REDIRECT] = final_redirect
|
||||
flow_context.setdefault(PLAN_CONTEXT_REDIRECT, final_redirect)
|
||||
|
||||
if not flow:
|
||||
return bad_request_message(
|
||||
|
@ -18,6 +18,7 @@ from authentik.core.models import (
|
||||
)
|
||||
from authentik.events.system_tasks import SystemTask, TaskStatus, prefill_task
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.utils.db import qs_batch_iter
|
||||
from authentik.root.celery import CELERY_APP
|
||||
|
||||
LOGGER = get_logger()
|
||||
@ -34,14 +35,14 @@ def clean_expired_models(self: SystemTask):
|
||||
cls.objects.all().exclude(expiring=False).exclude(expiring=True, expires__gt=now())
|
||||
)
|
||||
amount = objects.count()
|
||||
for obj in objects:
|
||||
for obj in qs_batch_iter(objects):
|
||||
obj.expire_action()
|
||||
LOGGER.debug("Expired models", model=cls, amount=amount)
|
||||
messages.append(f"Expired {amount} {cls._meta.verbose_name_plural}")
|
||||
# Special case
|
||||
amount = 0
|
||||
|
||||
for session in AuthenticatedSession.objects.all():
|
||||
for session in qs_batch_iter(AuthenticatedSession.objects.all()):
|
||||
match CONFIG.get("session_storage", "cache"):
|
||||
case "cache":
|
||||
cache_key = f"{KEY_PREFIX}{session.session_key}"
|
||||
|
153
authentik/core/tests/test_application_entitlements.py
Normal file
153
authentik/core/tests/test_application_entitlements.py
Normal file
@ -0,0 +1,153 @@
|
||||
"""Test Application Entitlements API"""
|
||||
|
||||
from django.urls import reverse
|
||||
from guardian.shortcuts import assign_perm
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import Application, ApplicationEntitlement, Group
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_user
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.policies.dummy.models import DummyPolicy
|
||||
from authentik.policies.models import PolicyBinding
|
||||
from authentik.providers.oauth2.models import OAuth2Provider
|
||||
|
||||
|
||||
class TestApplicationEntitlements(APITestCase):
|
||||
"""Test application entitlements"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.user = create_test_user()
|
||||
self.other_user = create_test_user()
|
||||
self.provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
)
|
||||
self.app: Application = Application.objects.create(
|
||||
name=generate_id(),
|
||||
slug=generate_id(),
|
||||
provider=self.provider,
|
||||
)
|
||||
|
||||
def test_user(self):
|
||||
"""Test user-direct assignment"""
|
||||
ent = ApplicationEntitlement.objects.create(app=self.app, name=generate_id())
|
||||
PolicyBinding.objects.create(target=ent, user=self.user, order=0)
|
||||
ents = self.user.app_entitlements(self.app)
|
||||
self.assertEqual(len(ents), 1)
|
||||
self.assertEqual(ents[0].name, ent.name)
|
||||
|
||||
def test_group(self):
|
||||
"""Test direct group"""
|
||||
group = Group.objects.create(name=generate_id())
|
||||
self.user.ak_groups.add(group)
|
||||
ent = ApplicationEntitlement.objects.create(app=self.app, name=generate_id())
|
||||
PolicyBinding.objects.create(target=ent, group=group, order=0)
|
||||
ents = self.user.app_entitlements(self.app)
|
||||
self.assertEqual(len(ents), 1)
|
||||
self.assertEqual(ents[0].name, ent.name)
|
||||
|
||||
def test_group_indirect(self):
|
||||
"""Test indirect group"""
|
||||
parent = Group.objects.create(name=generate_id())
|
||||
group = Group.objects.create(name=generate_id(), parent=parent)
|
||||
self.user.ak_groups.add(group)
|
||||
ent = ApplicationEntitlement.objects.create(app=self.app, name=generate_id())
|
||||
PolicyBinding.objects.create(target=ent, group=parent, order=0)
|
||||
ents = self.user.app_entitlements(self.app)
|
||||
self.assertEqual(len(ents), 1)
|
||||
self.assertEqual(ents[0].name, ent.name)
|
||||
|
||||
def test_negate_user(self):
|
||||
"""Test with negate flag"""
|
||||
ent = ApplicationEntitlement.objects.create(app=self.app, name=generate_id())
|
||||
PolicyBinding.objects.create(target=ent, user=self.other_user, order=0, negate=True)
|
||||
ents = self.user.app_entitlements(self.app)
|
||||
self.assertEqual(len(ents), 1)
|
||||
self.assertEqual(ents[0].name, ent.name)
|
||||
|
||||
def test_negate_group(self):
|
||||
"""Test with negate flag"""
|
||||
other_group = Group.objects.create(name=generate_id())
|
||||
ent = ApplicationEntitlement.objects.create(app=self.app, name=generate_id())
|
||||
PolicyBinding.objects.create(target=ent, group=other_group, order=0, negate=True)
|
||||
ents = self.user.app_entitlements(self.app)
|
||||
self.assertEqual(len(ents), 1)
|
||||
self.assertEqual(ents[0].name, ent.name)
|
||||
|
||||
def test_api_perms_global(self):
|
||||
"""Test API creation with global permissions"""
|
||||
assign_perm("authentik_core.add_applicationentitlement", self.user)
|
||||
assign_perm("authentik_core.view_application", self.user)
|
||||
self.client.force_login(self.user)
|
||||
res = self.client.post(
|
||||
reverse("authentik_api:applicationentitlement-list"),
|
||||
data={
|
||||
"name": generate_id(),
|
||||
"app": self.app.pk,
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 201)
|
||||
|
||||
def test_api_perms_scoped(self):
|
||||
"""Test API creation with scoped permissions"""
|
||||
assign_perm("authentik_core.add_applicationentitlement", self.user)
|
||||
assign_perm("authentik_core.view_application", self.user, self.app)
|
||||
self.client.force_login(self.user)
|
||||
res = self.client.post(
|
||||
reverse("authentik_api:applicationentitlement-list"),
|
||||
data={
|
||||
"name": generate_id(),
|
||||
"app": self.app.pk,
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 201)
|
||||
|
||||
def test_api_perms_missing(self):
|
||||
"""Test API creation with no permissions"""
|
||||
assign_perm("authentik_core.add_applicationentitlement", self.user)
|
||||
self.client.force_login(self.user)
|
||||
res = self.client.post(
|
||||
reverse("authentik_api:applicationentitlement-list"),
|
||||
data={
|
||||
"name": generate_id(),
|
||||
"app": self.app.pk,
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 400)
|
||||
self.assertJSONEqual(res.content, {"app": ["User does not have access to application."]})
|
||||
|
||||
def test_api_bindings_policy(self):
|
||||
"""Test that API doesn't allow policies to be bound to this"""
|
||||
ent = ApplicationEntitlement.objects.create(app=self.app, name=generate_id())
|
||||
policy = DummyPolicy.objects.create(name=generate_id())
|
||||
admin = create_test_admin_user()
|
||||
self.client.force_login(admin)
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:policybinding-list"),
|
||||
data={
|
||||
"target": ent.pbm_uuid,
|
||||
"policy": policy.pk,
|
||||
"order": 0,
|
||||
},
|
||||
)
|
||||
self.assertJSONEqual(
|
||||
response.content.decode(),
|
||||
{"non_field_errors": ["One of 'group', 'user' must be set."]},
|
||||
)
|
||||
|
||||
def test_api_bindings_group(self):
|
||||
"""Test that API doesn't allow policies to be bound to this"""
|
||||
ent = ApplicationEntitlement.objects.create(app=self.app, name=generate_id())
|
||||
group = Group.objects.create(name=generate_id())
|
||||
admin = create_test_admin_user()
|
||||
self.client.force_login(admin)
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:policybinding-list"),
|
||||
data={
|
||||
"target": ent.pbm_uuid,
|
||||
"group": group.pk,
|
||||
"order": 0,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertTrue(PolicyBinding.objects.filter(target=ent.pbm_uuid).exists())
|
@ -6,6 +6,7 @@ from django.conf import settings
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.urls import path
|
||||
|
||||
from authentik.core.api.application_entitlements import ApplicationEntitlementViewSet
|
||||
from authentik.core.api.applications import ApplicationViewSet
|
||||
from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet
|
||||
from authentik.core.api.devices import AdminDeviceViewSet, DeviceViewSet
|
||||
@ -69,6 +70,7 @@ urlpatterns = [
|
||||
api_urlpatterns = [
|
||||
("core/authenticated_sessions", AuthenticatedSessionViewSet),
|
||||
("core/applications", ApplicationViewSet),
|
||||
("core/application_entitlements", ApplicationEntitlementViewSet),
|
||||
path(
|
||||
"core/transactional/applications/",
|
||||
TransactionalApplicationView.as_view(),
|
||||
|
@ -15,6 +15,7 @@ from authentik.events.models import (
|
||||
TaskStatus,
|
||||
)
|
||||
from authentik.events.system_tasks import SystemTask, prefill_task
|
||||
from authentik.lib.utils.db import qs_batch_iter
|
||||
from authentik.policies.engine import PolicyEngine
|
||||
from authentik.policies.models import PolicyBinding, PolicyEngineMode
|
||||
from authentik.root.celery import CELERY_APP
|
||||
@ -129,7 +130,8 @@ def gdpr_cleanup(user_pk: int):
|
||||
"""cleanup events from gdpr_compliance"""
|
||||
events = Event.objects.filter(user__pk=user_pk)
|
||||
LOGGER.debug("GDPR cleanup, removing events from user", events=events.count())
|
||||
events.delete()
|
||||
for event in qs_batch_iter(events):
|
||||
event.delete()
|
||||
|
||||
|
||||
@CELERY_APP.task(bind=True, base=SystemTask)
|
||||
@ -138,7 +140,7 @@ def notification_cleanup(self: SystemTask):
|
||||
"""Cleanup seen notifications and notifications whose event expired."""
|
||||
notifications = Notification.objects.filter(Q(event=None) | Q(seen=True))
|
||||
amount = notifications.count()
|
||||
for notification in notifications:
|
||||
for notification in qs_batch_iter(notifications):
|
||||
notification.delete()
|
||||
LOGGER.debug("Expired notifications", amount=amount)
|
||||
self.set_status(TaskStatus.SUCCESSFUL, f"Expired {amount} Notifications")
|
||||
|
@ -280,9 +280,24 @@ class ConfigLoader:
|
||||
self.log("warning", "Failed to parse config as int", path=path, exc=str(exc))
|
||||
return default
|
||||
|
||||
def get_optional_int(self, path: str, default=None) -> int | None:
|
||||
"""Wrapper for get that converts value into int or None if set"""
|
||||
value = self.get(path, default)
|
||||
|
||||
try:
|
||||
return int(value)
|
||||
except (ValueError, TypeError) as exc:
|
||||
if value is None or (isinstance(value, str) and value.lower() == "null"):
|
||||
return None
|
||||
self.log("warning", "Failed to parse config as int", path=path, exc=str(exc))
|
||||
return default
|
||||
|
||||
def get_bool(self, path: str, default=False) -> bool:
|
||||
"""Wrapper for get that converts value into boolean"""
|
||||
return str(self.get(path, default)).lower() == "true"
|
||||
value = self.get(path, UNSET)
|
||||
if value is UNSET:
|
||||
return default
|
||||
return str(self.get(path)).lower() == "true"
|
||||
|
||||
def get_keys(self, path: str, sep=".") -> list[str]:
|
||||
"""List attribute keys by using yaml path"""
|
||||
@ -354,20 +369,33 @@ def django_db_config(config: ConfigLoader | None = None) -> dict:
|
||||
"sslcert": config.get("postgresql.sslcert"),
|
||||
"sslkey": config.get("postgresql.sslkey"),
|
||||
},
|
||||
"CONN_MAX_AGE": CONFIG.get_optional_int("postgresql.conn_max_age", 0),
|
||||
"CONN_HEALTH_CHECKS": CONFIG.get_bool("postgresql.conn_health_checks", False),
|
||||
"DISABLE_SERVER_SIDE_CURSORS": CONFIG.get_bool(
|
||||
"postgresql.disable_server_side_cursors", False
|
||||
),
|
||||
"TEST": {
|
||||
"NAME": config.get("postgresql.test.name"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
conn_max_age = CONFIG.get_optional_int("postgresql.conn_max_age", UNSET)
|
||||
disable_server_side_cursors = CONFIG.get_bool("postgresql.disable_server_side_cursors", UNSET)
|
||||
if config.get_bool("postgresql.use_pgpool", False):
|
||||
db["default"]["DISABLE_SERVER_SIDE_CURSORS"] = True
|
||||
if disable_server_side_cursors is not UNSET:
|
||||
db["default"]["DISABLE_SERVER_SIDE_CURSORS"] = disable_server_side_cursors
|
||||
|
||||
if config.get_bool("postgresql.use_pgbouncer", False):
|
||||
# https://docs.djangoproject.com/en/4.0/ref/databases/#transaction-pooling-server-side-cursors
|
||||
db["default"]["DISABLE_SERVER_SIDE_CURSORS"] = True
|
||||
# https://docs.djangoproject.com/en/4.0/ref/databases/#persistent-connections
|
||||
db["default"]["CONN_MAX_AGE"] = None # persistent
|
||||
if disable_server_side_cursors is not UNSET:
|
||||
db["default"]["DISABLE_SERVER_SIDE_CURSORS"] = disable_server_side_cursors
|
||||
if conn_max_age is not UNSET:
|
||||
db["default"]["CONN_MAX_AGE"] = conn_max_age
|
||||
|
||||
for replica in config.get_keys("postgresql.read_replicas"):
|
||||
_database = deepcopy(db["default"])
|
||||
|
@ -6,8 +6,6 @@ postgresql:
|
||||
user: authentik
|
||||
port: 5432
|
||||
password: "env://POSTGRES_PASSWORD"
|
||||
use_pgbouncer: false
|
||||
use_pgpool: false
|
||||
test:
|
||||
name: test_authentik
|
||||
read_replicas: {}
|
||||
|
@ -214,6 +214,9 @@ class TestConfig(TestCase):
|
||||
"PORT": "foo",
|
||||
"TEST": {"NAME": "foo"},
|
||||
"USER": "foo",
|
||||
"CONN_MAX_AGE": 0,
|
||||
"CONN_HEALTH_CHECKS": False,
|
||||
"DISABLE_SERVER_SIDE_CURSORS": False,
|
||||
}
|
||||
},
|
||||
)
|
||||
@ -251,6 +254,9 @@ class TestConfig(TestCase):
|
||||
"PORT": "foo",
|
||||
"TEST": {"NAME": "foo"},
|
||||
"USER": "foo",
|
||||
"CONN_MAX_AGE": 0,
|
||||
"CONN_HEALTH_CHECKS": False,
|
||||
"DISABLE_SERVER_SIDE_CURSORS": False,
|
||||
},
|
||||
"replica_0": {
|
||||
"ENGINE": "authentik.root.db",
|
||||
@ -266,6 +272,72 @@ class TestConfig(TestCase):
|
||||
"PORT": "foo",
|
||||
"TEST": {"NAME": "foo"},
|
||||
"USER": "foo",
|
||||
"CONN_MAX_AGE": 0,
|
||||
"CONN_HEALTH_CHECKS": False,
|
||||
"DISABLE_SERVER_SIDE_CURSORS": False,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
def test_db_read_replicas_pgbouncer(self):
|
||||
"""Test read replicas"""
|
||||
config = ConfigLoader()
|
||||
config.set("postgresql.host", "foo")
|
||||
config.set("postgresql.name", "foo")
|
||||
config.set("postgresql.user", "foo")
|
||||
config.set("postgresql.password", "foo")
|
||||
config.set("postgresql.port", "foo")
|
||||
config.set("postgresql.sslmode", "foo")
|
||||
config.set("postgresql.sslrootcert", "foo")
|
||||
config.set("postgresql.sslcert", "foo")
|
||||
config.set("postgresql.sslkey", "foo")
|
||||
config.set("postgresql.test.name", "foo")
|
||||
config.set("postgresql.use_pgbouncer", True)
|
||||
# Read replica
|
||||
config.set("postgresql.read_replicas.0.host", "bar")
|
||||
# Override conn_max_age
|
||||
config.set("postgresql.read_replicas.0.conn_max_age", 10)
|
||||
# This isn't supported
|
||||
config.set("postgresql.read_replicas.0.use_pgbouncer", False)
|
||||
conf = django_db_config(config)
|
||||
self.assertEqual(
|
||||
conf,
|
||||
{
|
||||
"default": {
|
||||
"DISABLE_SERVER_SIDE_CURSORS": True,
|
||||
"CONN_MAX_AGE": None,
|
||||
"CONN_HEALTH_CHECKS": False,
|
||||
"ENGINE": "authentik.root.db",
|
||||
"HOST": "foo",
|
||||
"NAME": "foo",
|
||||
"OPTIONS": {
|
||||
"sslcert": "foo",
|
||||
"sslkey": "foo",
|
||||
"sslmode": "foo",
|
||||
"sslrootcert": "foo",
|
||||
},
|
||||
"PASSWORD": "foo",
|
||||
"PORT": "foo",
|
||||
"TEST": {"NAME": "foo"},
|
||||
"USER": "foo",
|
||||
},
|
||||
"replica_0": {
|
||||
"DISABLE_SERVER_SIDE_CURSORS": True,
|
||||
"CONN_MAX_AGE": 10,
|
||||
"CONN_HEALTH_CHECKS": False,
|
||||
"ENGINE": "authentik.root.db",
|
||||
"HOST": "bar",
|
||||
"NAME": "foo",
|
||||
"OPTIONS": {
|
||||
"sslcert": "foo",
|
||||
"sslkey": "foo",
|
||||
"sslmode": "foo",
|
||||
"sslrootcert": "foo",
|
||||
},
|
||||
"PASSWORD": "foo",
|
||||
"PORT": "foo",
|
||||
"TEST": {"NAME": "foo"},
|
||||
"USER": "foo",
|
||||
},
|
||||
},
|
||||
)
|
||||
@ -294,6 +366,8 @@ class TestConfig(TestCase):
|
||||
{
|
||||
"default": {
|
||||
"DISABLE_SERVER_SIDE_CURSORS": True,
|
||||
"CONN_MAX_AGE": 0,
|
||||
"CONN_HEALTH_CHECKS": False,
|
||||
"ENGINE": "authentik.root.db",
|
||||
"HOST": "foo",
|
||||
"NAME": "foo",
|
||||
@ -310,6 +384,8 @@ class TestConfig(TestCase):
|
||||
},
|
||||
"replica_0": {
|
||||
"DISABLE_SERVER_SIDE_CURSORS": True,
|
||||
"CONN_MAX_AGE": 0,
|
||||
"CONN_HEALTH_CHECKS": False,
|
||||
"ENGINE": "authentik.root.db",
|
||||
"HOST": "bar",
|
||||
"NAME": "foo",
|
||||
@ -362,6 +438,9 @@ class TestConfig(TestCase):
|
||||
"PORT": "foo",
|
||||
"TEST": {"NAME": "foo"},
|
||||
"USER": "foo",
|
||||
"DISABLE_SERVER_SIDE_CURSORS": False,
|
||||
"CONN_MAX_AGE": 0,
|
||||
"CONN_HEALTH_CHECKS": False,
|
||||
},
|
||||
"replica_0": {
|
||||
"ENGINE": "authentik.root.db",
|
||||
@ -377,6 +456,9 @@ class TestConfig(TestCase):
|
||||
"PORT": "foo",
|
||||
"TEST": {"NAME": "foo"},
|
||||
"USER": "foo",
|
||||
"DISABLE_SERVER_SIDE_CURSORS": False,
|
||||
"CONN_MAX_AGE": 0,
|
||||
"CONN_HEALTH_CHECKS": False,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
22
authentik/lib/utils/db.py
Normal file
22
authentik/lib/utils/db.py
Normal file
@ -0,0 +1,22 @@
|
||||
"""authentik database utilities"""
|
||||
|
||||
import gc
|
||||
|
||||
from django.db.models import QuerySet
|
||||
|
||||
|
||||
def qs_batch_iter(qs: QuerySet, batch_size: int = 10_000, gc_collect: bool = True):
|
||||
pk_iter = qs.values_list("pk", flat=True).order_by("pk").distinct().iterator()
|
||||
eof = False
|
||||
while not eof:
|
||||
pk_buffer = []
|
||||
i = 0
|
||||
try:
|
||||
while i < batch_size:
|
||||
pk_buffer.append(pk_iter.next())
|
||||
i += 1
|
||||
except StopIteration:
|
||||
eof = True
|
||||
yield from qs.filter(pk__in=pk_buffer).order_by("pk").iterator()
|
||||
if gc_collect:
|
||||
gc.collect()
|
@ -84,19 +84,17 @@ class PolicyBindingSerializer(ModelSerializer):
|
||||
|
||||
def validate(self, attrs: OrderedDict) -> OrderedDict:
|
||||
"""Check that either policy, group or user is set."""
|
||||
count = sum(
|
||||
[
|
||||
bool(attrs.get("policy", None)),
|
||||
bool(attrs.get("group", None)),
|
||||
bool(attrs.get("user", None)),
|
||||
]
|
||||
)
|
||||
target: PolicyBindingModel = attrs.get("target")
|
||||
supported = target.supported_policy_binding_targets()
|
||||
supported.sort()
|
||||
count = sum([bool(attrs.get(x, None)) for x in supported])
|
||||
invalid = count > 1
|
||||
empty = count < 1
|
||||
warning = ", ".join(f"'{x}'" for x in supported)
|
||||
if invalid:
|
||||
raise ValidationError("Only one of 'policy', 'group' or 'user' can be set.")
|
||||
raise ValidationError(f"Only one of {warning} can be set.")
|
||||
if empty:
|
||||
raise ValidationError("One of 'policy', 'group' or 'user' must be set.")
|
||||
raise ValidationError(f"One of {warning} must be set.")
|
||||
return attrs
|
||||
|
||||
|
||||
|
@ -1,4 +1,6 @@
|
||||
# Generated by Django 4.2.5 on 2023-09-13 18:07
|
||||
import authentik.lib.models
|
||||
import django.db.models.deletion
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
@ -23,4 +25,13 @@ class Migration(migrations.Migration):
|
||||
default=30, help_text="Timeout after which Policy execution is terminated."
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="policybinding",
|
||||
name="target",
|
||||
field=authentik.lib.models.InheritanceForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="bindings",
|
||||
to="authentik_policies.policybindingmodel",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -47,6 +47,10 @@ class PolicyBindingModel(models.Model):
|
||||
def __str__(self) -> str:
|
||||
return f"PolicyBindingModel {self.pbm_uuid}"
|
||||
|
||||
def supported_policy_binding_targets(self):
|
||||
"""Return the list of objects that can be bound to this object."""
|
||||
return ["policy", "user", "group"]
|
||||
|
||||
|
||||
class PolicyBinding(SerializerModel):
|
||||
"""Relationship between a Policy and a PolicyBindingModel."""
|
||||
@ -81,7 +85,9 @@ class PolicyBinding(SerializerModel):
|
||||
blank=True,
|
||||
)
|
||||
|
||||
target = InheritanceForeignKey(PolicyBindingModel, on_delete=models.CASCADE, related_name="+")
|
||||
target = InheritanceForeignKey(
|
||||
PolicyBindingModel, on_delete=models.CASCADE, related_name="bindings"
|
||||
)
|
||||
negate = models.BooleanField(
|
||||
default=False,
|
||||
help_text=_("Negates the outcome of the policy. Messages are unaffected."),
|
||||
|
@ -38,7 +38,7 @@ class TestBindingsAPI(APITestCase):
|
||||
)
|
||||
self.assertJSONEqual(
|
||||
response.content.decode(),
|
||||
{"non_field_errors": ["Only one of 'policy', 'group' or 'user' can be set."]},
|
||||
{"non_field_errors": ["Only one of 'group', 'policy', 'user' can be set."]},
|
||||
)
|
||||
|
||||
def test_invalid_too_little(self):
|
||||
@ -49,5 +49,5 @@ class TestBindingsAPI(APITestCase):
|
||||
)
|
||||
self.assertJSONEqual(
|
||||
response.content.decode(),
|
||||
{"non_field_errors": ["One of 'policy', 'group' or 'user' must be set."]},
|
||||
{"non_field_errors": ["One of 'group', 'policy', 'user' must be set."]},
|
||||
)
|
||||
|
@ -127,6 +127,7 @@ class Traefik3MiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware]
|
||||
authResponseHeaders=[
|
||||
"X-authentik-username",
|
||||
"X-authentik-groups",
|
||||
"X-authentik-entitlements",
|
||||
"X-authentik-email",
|
||||
"X-authentik-name",
|
||||
"X-authentik-uid",
|
||||
|
@ -147,6 +147,7 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
|
||||
"goauthentik.io/providers/oauth2/scope-openid",
|
||||
"goauthentik.io/providers/oauth2/scope-profile",
|
||||
"goauthentik.io/providers/oauth2/scope-email",
|
||||
"goauthentik.io/providers/oauth2/scope-entitlements",
|
||||
"goauthentik.io/providers/proxy/scope-proxy",
|
||||
]
|
||||
)
|
||||
|
@ -54,9 +54,23 @@ class SAMLProviderSerializer(ProviderSerializer):
|
||||
if "request" not in self._context:
|
||||
return ""
|
||||
request: HttpRequest = self._context["request"]._request
|
||||
return request.build_absolute_uri(
|
||||
reverse("authentik_api:samlprovider-metadata", kwargs={"pk": instance.pk}) + "?download"
|
||||
)
|
||||
try:
|
||||
return request.build_absolute_uri(
|
||||
reverse(
|
||||
"authentik_providers_saml:metadata-download",
|
||||
kwargs={"application_slug": instance.application.slug},
|
||||
)
|
||||
)
|
||||
except Provider.application.RelatedObjectDoesNotExist:
|
||||
return request.build_absolute_uri(
|
||||
reverse(
|
||||
"authentik_api:samlprovider-metadata",
|
||||
kwargs={
|
||||
"pk": instance.pk,
|
||||
},
|
||||
)
|
||||
+ "?download"
|
||||
)
|
||||
|
||||
def get_url_sso_post(self, instance: SAMLProvider) -> str:
|
||||
"""Get SSO Post URL"""
|
||||
|
@ -17,6 +17,7 @@ class TestMetadataProcessor(TestCase):
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
self.source = SAMLSource.objects.create(
|
||||
name=generate_id(),
|
||||
slug=generate_id(),
|
||||
issuer="authentik",
|
||||
signing_kp=create_test_cert(),
|
||||
|
@ -28,6 +28,7 @@ class TestPropertyMappings(TestCase):
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
self.source = SAMLSource.objects.create(
|
||||
name=generate_id(),
|
||||
slug=generate_id(),
|
||||
issuer="authentik",
|
||||
allow_idp_initiated=True,
|
||||
|
@ -20,6 +20,7 @@ class TestResponseProcessor(TestCase):
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
self.source = SAMLSource.objects.create(
|
||||
name=generate_id(),
|
||||
slug=generate_id(),
|
||||
issuer="authentik",
|
||||
allow_idp_initiated=True,
|
||||
|
88
authentik/sources/saml/tests/test_views.py
Normal file
88
authentik/sources/saml/tests/test_views.py
Normal file
@ -0,0 +1,88 @@
|
||||
"""SAML Source tests"""
|
||||
|
||||
from base64 import b64encode
|
||||
|
||||
from django.test import RequestFactory, TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from authentik.core.tests.utils import create_test_flow
|
||||
from authentik.flows.planner import PLAN_CONTEXT_REDIRECT, FlowPlan
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.tests.utils import load_fixture
|
||||
from authentik.sources.saml.models import SAMLSource
|
||||
|
||||
|
||||
class TestViews(TestCase):
|
||||
"""Test SAML Views"""
|
||||
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
self.source = SAMLSource.objects.create(
|
||||
name=generate_id(),
|
||||
slug=generate_id(),
|
||||
issuer="authentik",
|
||||
allow_idp_initiated=True,
|
||||
pre_authentication_flow=create_test_flow(),
|
||||
)
|
||||
|
||||
def test_enroll(self):
|
||||
"""Enroll"""
|
||||
flow = create_test_flow()
|
||||
self.source.enrollment_flow = flow
|
||||
self.source.save()
|
||||
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
"authentik_sources_saml:acs",
|
||||
kwargs={
|
||||
"source_slug": self.source.slug,
|
||||
},
|
||||
),
|
||||
data={
|
||||
"SAMLResponse": b64encode(
|
||||
load_fixture("fixtures/response_success.xml").encode()
|
||||
).decode()
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertRedirects(
|
||||
response, reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
|
||||
)
|
||||
plan: FlowPlan = self.client.session.get(SESSION_KEY_PLAN)
|
||||
self.assertIsNotNone(plan)
|
||||
|
||||
def test_enroll_redirect(self):
|
||||
"""Enroll when attempting to access a provider"""
|
||||
initial_redirect = f"http://{generate_id()}"
|
||||
|
||||
session = self.client.session
|
||||
old_plan = FlowPlan(generate_id())
|
||||
old_plan.context[PLAN_CONTEXT_REDIRECT] = initial_redirect
|
||||
session[SESSION_KEY_PLAN] = old_plan
|
||||
session.save()
|
||||
|
||||
flow = create_test_flow()
|
||||
self.source.enrollment_flow = flow
|
||||
self.source.save()
|
||||
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
"authentik_sources_saml:acs",
|
||||
kwargs={
|
||||
"source_slug": self.source.slug,
|
||||
},
|
||||
),
|
||||
data={
|
||||
"SAMLResponse": b64encode(
|
||||
load_fixture("fixtures/response_success.xml").encode()
|
||||
).decode()
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertRedirects(
|
||||
response, reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
|
||||
)
|
||||
plan: FlowPlan = self.client.session.get(SESSION_KEY_PLAN)
|
||||
self.assertIsNotNone(plan)
|
||||
self.assertEqual(plan.context.get(PLAN_CONTEXT_REDIRECT), initial_redirect)
|
@ -28,10 +28,11 @@ from authentik.flows.planner import (
|
||||
PLAN_CONTEXT_REDIRECT,
|
||||
PLAN_CONTEXT_SOURCE,
|
||||
PLAN_CONTEXT_SSO,
|
||||
FlowPlan,
|
||||
FlowPlanner,
|
||||
)
|
||||
from authentik.flows.stage import ChallengeStageView
|
||||
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET
|
||||
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
|
||||
from authentik.lib.views import bad_request_message
|
||||
from authentik.providers.saml.utils.encoding import nice64
|
||||
from authentik.sources.saml.exceptions import MissingSAMLResponse, UnsupportedNameIDFormat
|
||||
@ -148,12 +149,15 @@ class ACSView(View):
|
||||
processor = ResponseProcessor(source, request)
|
||||
try:
|
||||
processor.parse()
|
||||
except MissingSAMLResponse as exc:
|
||||
return bad_request_message(request, str(exc))
|
||||
except VerificationError as exc:
|
||||
except (MissingSAMLResponse, VerificationError) as exc:
|
||||
return bad_request_message(request, str(exc))
|
||||
|
||||
try:
|
||||
if SESSION_KEY_PLAN in request.session:
|
||||
plan: FlowPlan = self.request.session[SESSION_KEY_PLAN]
|
||||
plan_redirect = plan.context.get(PLAN_CONTEXT_REDIRECT)
|
||||
if plan_redirect:
|
||||
self.request.session[SESSION_KEY_GET] = {NEXT_ARG_NAME: plan_redirect}
|
||||
return processor.prepare_flow_manager().get_flow()
|
||||
except (UnsupportedNameIDFormat, ValueError) as exc:
|
||||
return bad_request_message(request, str(exc))
|
||||
|
@ -2,7 +2,7 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "https://goauthentik.io/blueprints/schema.json",
|
||||
"type": "object",
|
||||
"title": "authentik 2024.10.5 Blueprint schema",
|
||||
"title": "authentik 2024.12.0 Blueprint schema",
|
||||
"required": [
|
||||
"version",
|
||||
"entries"
|
||||
@ -3201,6 +3201,46 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"model",
|
||||
"identifiers"
|
||||
],
|
||||
"properties": {
|
||||
"model": {
|
||||
"const": "authentik_core.applicationentitlement"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"absent",
|
||||
"present",
|
||||
"created",
|
||||
"must_created"
|
||||
],
|
||||
"default": "present"
|
||||
},
|
||||
"conditions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
"$ref": "#/$defs/model_authentik_core.applicationentitlement_permissions"
|
||||
},
|
||||
"attrs": {
|
||||
"$ref": "#/$defs/model_authentik_core.applicationentitlement"
|
||||
},
|
||||
"identifiers": {
|
||||
"$ref": "#/$defs/model_authentik_core.applicationentitlement"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
@ -3844,8 +3884,7 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"model",
|
||||
"identifiers"
|
||||
"model"
|
||||
],
|
||||
"properties": {
|
||||
"model": {
|
||||
@ -3875,9 +3914,6 @@
|
||||
},
|
||||
"attrs": {
|
||||
"$ref": "#/$defs/model_authentik_blueprints.metaapplyblueprint"
|
||||
},
|
||||
"identifiers": {
|
||||
"$ref": "#/$defs/model_authentik_blueprints.metaapplyblueprint"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4640,6 +4676,7 @@
|
||||
"authentik_core.group",
|
||||
"authentik_core.user",
|
||||
"authentik_core.application",
|
||||
"authentik_core.applicationentitlement",
|
||||
"authentik_core.token",
|
||||
"authentik_enterprise.license",
|
||||
"authentik_providers_google_workspace.googleworkspaceprovider",
|
||||
@ -6369,6 +6406,7 @@
|
||||
"authentik_brands.delete_brand",
|
||||
"authentik_brands.view_brand",
|
||||
"authentik_core.add_application",
|
||||
"authentik_core.add_applicationentitlement",
|
||||
"authentik_core.add_authenticatedsession",
|
||||
"authentik_core.add_group",
|
||||
"authentik_core.add_groupsourceconnection",
|
||||
@ -6381,6 +6419,7 @@
|
||||
"authentik_core.add_usersourceconnection",
|
||||
"authentik_core.assign_user_permissions",
|
||||
"authentik_core.change_application",
|
||||
"authentik_core.change_applicationentitlement",
|
||||
"authentik_core.change_authenticatedsession",
|
||||
"authentik_core.change_group",
|
||||
"authentik_core.change_groupsourceconnection",
|
||||
@ -6391,6 +6430,7 @@
|
||||
"authentik_core.change_user",
|
||||
"authentik_core.change_usersourceconnection",
|
||||
"authentik_core.delete_application",
|
||||
"authentik_core.delete_applicationentitlement",
|
||||
"authentik_core.delete_authenticatedsession",
|
||||
"authentik_core.delete_group",
|
||||
"authentik_core.delete_groupsourceconnection",
|
||||
@ -6406,6 +6446,7 @@
|
||||
"authentik_core.reset_user_password",
|
||||
"authentik_core.unassign_user_permissions",
|
||||
"authentik_core.view_application",
|
||||
"authentik_core.view_applicationentitlement",
|
||||
"authentik_core.view_authenticatedsession",
|
||||
"authentik_core.view_group",
|
||||
"authentik_core.view_groupsourceconnection",
|
||||
@ -7170,6 +7211,10 @@
|
||||
"type": "integer",
|
||||
"title": "User"
|
||||
},
|
||||
"source": {
|
||||
"type": "integer",
|
||||
"title": "Source"
|
||||
},
|
||||
"identifier": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
@ -7212,6 +7257,20 @@
|
||||
"model_authentik_sources_kerberos.groupkerberossourceconnection": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"group": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Group"
|
||||
},
|
||||
"source": {
|
||||
"type": "integer",
|
||||
"title": "Source"
|
||||
},
|
||||
"identifier": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Identifier"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
@ -7755,6 +7814,14 @@
|
||||
"model_authentik_sources_oauth.useroauthsourceconnection": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user": {
|
||||
"type": "integer",
|
||||
"title": "User"
|
||||
},
|
||||
"source": {
|
||||
"type": "integer",
|
||||
"title": "Source"
|
||||
},
|
||||
"identifier": {
|
||||
"type": "string",
|
||||
"maxLength": 255,
|
||||
@ -7805,6 +7872,20 @@
|
||||
"model_authentik_sources_oauth.groupoauthsourceconnection": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"group": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Group"
|
||||
},
|
||||
"source": {
|
||||
"type": "integer",
|
||||
"title": "Source"
|
||||
},
|
||||
"identifier": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Identifier"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
@ -8038,6 +8119,14 @@
|
||||
"model_authentik_sources_plex.userplexsourceconnection": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user": {
|
||||
"type": "integer",
|
||||
"title": "User"
|
||||
},
|
||||
"source": {
|
||||
"type": "integer",
|
||||
"title": "Source"
|
||||
},
|
||||
"identifier": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
@ -8085,6 +8174,20 @@
|
||||
"model_authentik_sources_plex.groupplexsourceconnection": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"group": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Group"
|
||||
},
|
||||
"source": {
|
||||
"type": "integer",
|
||||
"title": "Source"
|
||||
},
|
||||
"identifier": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Identifier"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
@ -8395,6 +8498,14 @@
|
||||
"model_authentik_sources_saml.usersamlsourceconnection": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user": {
|
||||
"type": "integer",
|
||||
"title": "User"
|
||||
},
|
||||
"source": {
|
||||
"type": "integer",
|
||||
"title": "Source"
|
||||
},
|
||||
"identifier": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
@ -8437,6 +8548,20 @@
|
||||
"model_authentik_sources_saml.groupsamlsourceconnection": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"group": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Group"
|
||||
},
|
||||
"source": {
|
||||
"type": "integer",
|
||||
"title": "Source"
|
||||
},
|
||||
"identifier": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Identifier"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
@ -12530,6 +12655,7 @@
|
||||
"authentik_brands.delete_brand",
|
||||
"authentik_brands.view_brand",
|
||||
"authentik_core.add_application",
|
||||
"authentik_core.add_applicationentitlement",
|
||||
"authentik_core.add_authenticatedsession",
|
||||
"authentik_core.add_group",
|
||||
"authentik_core.add_groupsourceconnection",
|
||||
@ -12542,6 +12668,7 @@
|
||||
"authentik_core.add_usersourceconnection",
|
||||
"authentik_core.assign_user_permissions",
|
||||
"authentik_core.change_application",
|
||||
"authentik_core.change_applicationentitlement",
|
||||
"authentik_core.change_authenticatedsession",
|
||||
"authentik_core.change_group",
|
||||
"authentik_core.change_groupsourceconnection",
|
||||
@ -12552,6 +12679,7 @@
|
||||
"authentik_core.change_user",
|
||||
"authentik_core.change_usersourceconnection",
|
||||
"authentik_core.delete_application",
|
||||
"authentik_core.delete_applicationentitlement",
|
||||
"authentik_core.delete_authenticatedsession",
|
||||
"authentik_core.delete_group",
|
||||
"authentik_core.delete_groupsourceconnection",
|
||||
@ -12567,6 +12695,7 @@
|
||||
"authentik_core.reset_user_password",
|
||||
"authentik_core.unassign_user_permissions",
|
||||
"authentik_core.view_application",
|
||||
"authentik_core.view_applicationentitlement",
|
||||
"authentik_core.view_authenticatedsession",
|
||||
"authentik_core.view_group",
|
||||
"authentik_core.view_groupsourceconnection",
|
||||
@ -13179,6 +13308,52 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"model_authentik_core.applicationentitlement": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Name"
|
||||
},
|
||||
"app": {
|
||||
"type": "integer",
|
||||
"title": "App"
|
||||
},
|
||||
"attributes": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"title": "Attributes"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"model_authentik_core.applicationentitlement_permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"permission"
|
||||
],
|
||||
"properties": {
|
||||
"permission": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"add_applicationentitlement",
|
||||
"change_applicationentitlement",
|
||||
"delete_applicationentitlement",
|
||||
"view_applicationentitlement"
|
||||
]
|
||||
},
|
||||
"user": {
|
||||
"type": "integer"
|
||||
},
|
||||
"role": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"model_authentik_core.token": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -42,9 +42,21 @@ entries:
|
||||
"given_name": request.user.name,
|
||||
"preferred_username": request.user.username,
|
||||
"nickname": request.user.username,
|
||||
# groups is not part of the official userinfo schema, but is a quasi-standard
|
||||
"groups": [group.name for group in request.user.ak_groups.all()],
|
||||
}
|
||||
- identifiers:
|
||||
managed: goauthentik.io/providers/oauth2/scope-entitlements
|
||||
model: authentik_providers_oauth2.scopemapping
|
||||
attrs:
|
||||
name: "authentik default OAuth Mapping: Application Entitlements"
|
||||
scope_name: entitlements
|
||||
description: "Application entitlements"
|
||||
expression: |
|
||||
entitlements = [entitlement.name for entitlement in request.user.app_entitlements(provider.application)]
|
||||
return {
|
||||
"entitlements": entitlements,
|
||||
"roles": entitlements,
|
||||
}
|
||||
- identifiers:
|
||||
managed: goauthentik.io/providers/oauth2/scope-offline_access
|
||||
model: authentik_providers_oauth2.scopemapping
|
||||
|
@ -31,7 +31,7 @@ services:
|
||||
volumes:
|
||||
- redis:/data
|
||||
server:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.10.5}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.12.0}
|
||||
restart: unless-stopped
|
||||
command: server
|
||||
environment:
|
||||
@ -54,7 +54,7 @@ services:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
worker:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.10.5}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.12.0}
|
||||
restart: unless-stopped
|
||||
command: worker
|
||||
environment:
|
||||
|
2
go.mod
2
go.mod
@ -29,7 +29,7 @@ require (
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/wwt/guac v1.3.2
|
||||
goauthentik.io/api/v3 v3.2024105.3
|
||||
goauthentik.io/api/v3 v3.2024105.5
|
||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||
golang.org/x/oauth2 v0.24.0
|
||||
golang.org/x/sync v0.10.0
|
||||
|
4
go.sum
4
go.sum
@ -299,8 +299,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.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
goauthentik.io/api/v3 v3.2024105.3 h1:Vl1vwPkCtA8hChsxwO3NUI8nupFC7r93jUHvqM+kYVw=
|
||||
goauthentik.io/api/v3 v3.2024105.3/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||
goauthentik.io/api/v3 v3.2024105.5 h1:zBDqIjWN5QNuL6iBLL4o9QwBsSkFQdAnyTjASsyE/fw=
|
||||
goauthentik.io/api/v3 v3.2024105.5/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=
|
||||
|
@ -29,4 +29,4 @@ func UserAgent() string {
|
||||
return fmt.Sprintf("authentik@%s", FullVersion())
|
||||
}
|
||||
|
||||
const VERSION = "2024.10.5"
|
||||
const VERSION = "2024.12.0"
|
||||
|
@ -14,6 +14,7 @@ type Claims struct {
|
||||
Name string `json:"name"`
|
||||
PreferredUsername string `json:"preferred_username"`
|
||||
Groups []string `json:"groups"`
|
||||
Entitlements []string `json:"entitlements"`
|
||||
Sid string `json:"sid"`
|
||||
Proxy *ProxyClaims `json:"ak_proxy"`
|
||||
|
||||
|
@ -41,6 +41,7 @@ func (a *Application) addHeaders(headers http.Header, c *Claims) {
|
||||
// https://goauthentik.io/docs/providers/proxy/proxy
|
||||
headers.Set("X-authentik-username", c.PreferredUsername)
|
||||
headers.Set("X-authentik-groups", strings.Join(c.Groups, "|"))
|
||||
headers.Set("X-authentik-entitlements", strings.Join(c.Entitlements, "|"))
|
||||
headers.Set("X-authentik-email", c.Email)
|
||||
headers.Set("X-authentik-name", c.Name)
|
||||
headers.Set("X-authentik-uid", c.Sub)
|
||||
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-11-26 00:09+0000\n"
|
||||
"POT-Creation-Date: 2024-12-20 00:08+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"
|
||||
@ -101,6 +101,10 @@ msgstr ""
|
||||
msgid "Brands"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/api/application_entitlements.py
|
||||
msgid "User does not have access to application."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/api/devices.py
|
||||
msgid "Extra description not available"
|
||||
msgstr ""
|
||||
@ -225,6 +229,14 @@ msgstr ""
|
||||
msgid "Applications"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Application Entitlement"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Application Entitlements"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Use the source-specific identifier"
|
||||
msgstr ""
|
||||
@ -1873,6 +1885,10 @@ msgstr ""
|
||||
msgid "Custom krb5.conf to use. Uses the system one by default"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/sources/kerberos/models.py
|
||||
msgid "KAdmin server type"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/sources/kerberos/models.py
|
||||
msgid "Sync users from Kerberos into authentik"
|
||||
msgstr ""
|
||||
@ -2812,7 +2828,7 @@ msgstr ""
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
" If you did not request a password change, please ignore this Email. The "
|
||||
" If you did not request a password change, please ignore this email. The "
|
||||
"link above is valid for %(expires)s.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
@ -2833,7 +2849,7 @@ msgstr ""
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
"If you did not request a password change, please ignore this Email. The link "
|
||||
"If you did not request a password change, please ignore this email. The link "
|
||||
"above is valid for %(expires)s.\n"
|
||||
msgstr ""
|
||||
|
||||
@ -3098,6 +3114,22 @@ msgstr ""
|
||||
msgid "Passwords don't match."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/redirect/api.py
|
||||
msgid "Target URL should be present when mode is Static."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/redirect/api.py
|
||||
msgid "Target Flow should be present when mode is Flow."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/redirect/models.py
|
||||
msgid "Redirect Stage"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/redirect/models.py
|
||||
msgid "Redirect Stages"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/user_delete/models.py
|
||||
msgid "User Delete Stage"
|
||||
msgstr ""
|
||||
|
@ -19,7 +19,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-11-26 00:09+0000\n"
|
||||
"POT-Creation-Date: 2024-12-18 13:31+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: Marc Schmitt, 2024\n"
|
||||
"Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n"
|
||||
@ -2084,6 +2084,10 @@ msgid "Custom krb5.conf to use. Uses the system one by default"
|
||||
msgstr ""
|
||||
"krb5.conf personnalisé à utiliser. Utilise celui du système par défault"
|
||||
|
||||
#: authentik/sources/kerberos/models.py
|
||||
msgid "KAdmin server type"
|
||||
msgstr "Type de serveur KAdmin"
|
||||
|
||||
#: authentik/sources/kerberos/models.py
|
||||
msgid "Sync users from Kerberos into authentik"
|
||||
msgstr "Synchroniser les utilisateurs Kerberos dans authentik"
|
||||
@ -3105,7 +3109,7 @@ msgstr ""
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
" If you did not request a password change, please ignore this Email. The link above is valid for %(expires)s.\n"
|
||||
" If you did not request a password change, please ignore this email. The link above is valid for %(expires)s.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
@ -3129,7 +3133,7 @@ msgstr ""
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
"If you did not request a password change, please ignore this Email. The link above is valid for %(expires)s.\n"
|
||||
"If you did not request a password change, please ignore this email. The link above is valid for %(expires)s.\n"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Si vous n'avez pas requis de changement de mot de passe, veuillez ignorer cet e-mail. Le lien ci-dessus est valide pendant %(expires)s.\n"
|
||||
@ -3434,6 +3438,22 @@ msgstr "Étapes invite"
|
||||
msgid "Passwords don't match."
|
||||
msgstr "Les mots de passe ne correspondent pas."
|
||||
|
||||
#: authentik/stages/redirect/api.py
|
||||
msgid "Target URL should be present when mode is Static."
|
||||
msgstr "L'URL destination doit être présente lorsque le mode est Statique."
|
||||
|
||||
#: authentik/stages/redirect/api.py
|
||||
msgid "Target Flow should be present when mode is Flow."
|
||||
msgstr "Le flux destination doit être présent lorsque le mode est Flux."
|
||||
|
||||
#: authentik/stages/redirect/models.py
|
||||
msgid "Redirect Stage"
|
||||
msgstr "Étape de redirection"
|
||||
|
||||
#: authentik/stages/redirect/models.py
|
||||
msgid "Redirect Stages"
|
||||
msgstr "Étapes de redirection"
|
||||
|
||||
#: authentik/stages/user_delete/models.py
|
||||
msgid "User Delete Stage"
|
||||
msgstr "Étape de suppression utilisateur"
|
||||
|
Binary file not shown.
@ -13,15 +13,16 @@
|
||||
# albanobattistella <albanobattistella@gmail.com>, 2024
|
||||
# Nicola Mersi, 2024
|
||||
# tom max, 2024
|
||||
# Marc Schmitt, 2024
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-11-18 00:09+0000\n"
|
||||
"POT-Creation-Date: 2024-11-26 00:09+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: tom max, 2024\n"
|
||||
"Last-Translator: Marc Schmitt, 2024\n"
|
||||
"Language-Team: Italian (https://app.transifex.com/authentik/teams/119923/it/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@ -89,9 +90,9 @@ msgid "authentik Export - {date}"
|
||||
msgstr "Esportazione authentik - {date}"
|
||||
|
||||
#: authentik/blueprints/v1/tasks.py authentik/crypto/tasks.py
|
||||
#, python-format
|
||||
msgid "Successfully imported %(count)d files."
|
||||
msgstr "Importato con successo %(count)d file."
|
||||
#, python-brace-format
|
||||
msgid "Successfully imported {count} files."
|
||||
msgstr "Importato con successo {count} file."
|
||||
|
||||
#: authentik/brands/models.py
|
||||
msgid ""
|
||||
@ -635,7 +636,7 @@ msgstr "Fasi Sorgenti"
|
||||
#: authentik/events/api/tasks.py
|
||||
#, python-brace-format
|
||||
msgid "Successfully started task {name}."
|
||||
msgstr "Attività {nome} avviata correttamente."
|
||||
msgstr "Attività {name} avviata correttamente."
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid "Event"
|
||||
@ -937,14 +938,14 @@ msgid "Starting full provider sync"
|
||||
msgstr "Avvio della sincronizzazione completa del provider"
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
#, python-format
|
||||
msgid "Syncing page %(page)d of users"
|
||||
msgstr "Sincronizzando pagina %(page)d degli utenti"
|
||||
#, python-brace-format
|
||||
msgid "Syncing page {page} of users"
|
||||
msgstr "Sincronizzando pagina {page} degli utenti"
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
#, python-format
|
||||
msgid "Syncing page %(page)d of groups"
|
||||
msgstr "Sincronizzando pagina %(page)d dei gruppi"
|
||||
#, python-brace-format
|
||||
msgid "Syncing page {page} of groups"
|
||||
msgstr "Sincronizzando pagina {page} dei gruppi"
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
#, python-brace-format
|
||||
@ -1117,10 +1118,10 @@ msgid "Event Matcher Policies"
|
||||
msgstr "Criteri Corrispondenza Evento"
|
||||
|
||||
#: authentik/policies/expiry/models.py
|
||||
#, python-format
|
||||
msgid "Password expired %(days)d days ago. Please update your password."
|
||||
#, python-brace-format
|
||||
msgid "Password expired {days} days ago. Please update your password."
|
||||
msgstr ""
|
||||
"Password scaduta %(days)d giorni fa. Si prega di aggiornare la password."
|
||||
"Password scaduta {days} giorni fa. Si prega di aggiornare la password."
|
||||
|
||||
#: authentik/policies/expiry/models.py
|
||||
msgid "Password has expired."
|
||||
@ -1254,9 +1255,9 @@ msgid "Invalid password."
|
||||
msgstr "Password invalida."
|
||||
|
||||
#: authentik/policies/password/models.py
|
||||
#, python-format
|
||||
msgid "Password exists on %(count)d online lists."
|
||||
msgstr "Password esistente in %(count)d lite online."
|
||||
#, python-brace-format
|
||||
msgid "Password exists on {count} online lists."
|
||||
msgstr "Password esistente in {count} lite online."
|
||||
|
||||
#: authentik/policies/password/models.py
|
||||
msgid "Password is too weak."
|
||||
@ -1383,6 +1384,11 @@ msgstr "Providers LDAP"
|
||||
msgid "Search full LDAP directory"
|
||||
msgstr "Ricerca completa nella directory LDAP"
|
||||
|
||||
#: authentik/providers/oauth2/api/providers.py
|
||||
#, python-brace-format
|
||||
msgid "Invalid Regex Pattern: {url}"
|
||||
msgstr "Modello Regex non valido: {url}"
|
||||
|
||||
#: authentik/providers/oauth2/id_token.py
|
||||
msgid "Based on the Hashed User ID"
|
||||
msgstr "Basato sull'ID utente hashato"
|
||||
@ -1428,6 +1434,14 @@ msgid "Each provider has a different issuer, based on the application slug."
|
||||
msgstr ""
|
||||
"Ogni provider ha un issuer differente, basato sullo slug dell'applicazione."
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Strict URL comparison"
|
||||
msgstr "Confronto URL rigoroso"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Regular Expression URL matching"
|
||||
msgstr "Corrispondenza URL espressione regolare"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "code (Authorization Code Flow)"
|
||||
msgstr "code (Flusso di autorizzazione del codice)"
|
||||
@ -1508,10 +1522,6 @@ msgstr "Client Secret"
|
||||
msgid "Redirect URIs"
|
||||
msgstr "URL di reindirizzamento"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Enter each URI on a new line."
|
||||
msgstr "Inserisci ogni URI su una nuova riga."
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Include claims in id_token"
|
||||
msgstr "Includere le richieste in id_token"
|
||||
|
@ -15,7 +15,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-11-26 00:09+0000\n"
|
||||
"POT-Creation-Date: 2024-12-18 13:31+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: deluxghost, 2024\n"
|
||||
"Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n"
|
||||
@ -1898,6 +1898,10 @@ msgstr "Kerberos 领域"
|
||||
msgid "Custom krb5.conf to use. Uses the system one by default"
|
||||
msgstr "要使用的自定义 krb5.conf。默认使用系统自带"
|
||||
|
||||
#: authentik/sources/kerberos/models.py
|
||||
msgid "KAdmin server type"
|
||||
msgstr "KAdmin 服务器类型"
|
||||
|
||||
#: authentik/sources/kerberos/models.py
|
||||
msgid "Sync users from Kerberos into authentik"
|
||||
msgstr "从 Kerberos 同步用户到 authentik"
|
||||
@ -2858,7 +2862,7 @@ msgstr ""
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
" If you did not request a password change, please ignore this Email. The link above is valid for %(expires)s.\n"
|
||||
" If you did not request a password change, please ignore this email. The link above is valid for %(expires)s.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
@ -2882,7 +2886,7 @@ msgstr ""
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
"If you did not request a password change, please ignore this Email. The link above is valid for %(expires)s.\n"
|
||||
"If you did not request a password change, please ignore this email. The link above is valid for %(expires)s.\n"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"如果您没有请求更改密码,请忽略此电子邮件。上面的链接在 %(expires)s 内有效。\n"
|
||||
@ -3151,6 +3155,22 @@ msgstr "输入阶段"
|
||||
msgid "Passwords don't match."
|
||||
msgstr "密码不匹配。"
|
||||
|
||||
#: authentik/stages/redirect/api.py
|
||||
msgid "Target URL should be present when mode is Static."
|
||||
msgstr "当模式为静态时,目标 URL 应存在。"
|
||||
|
||||
#: authentik/stages/redirect/api.py
|
||||
msgid "Target Flow should be present when mode is Flow."
|
||||
msgstr "当模式为流程时,目标流程应存在。"
|
||||
|
||||
#: authentik/stages/redirect/models.py
|
||||
msgid "Redirect Stage"
|
||||
msgstr "重定向阶段"
|
||||
|
||||
#: authentik/stages/redirect/models.py
|
||||
msgid "Redirect Stages"
|
||||
msgstr "重定向阶段"
|
||||
|
||||
#: authentik/stages/user_delete/models.py
|
||||
msgid "User Delete Stage"
|
||||
msgstr "用户删除阶段"
|
||||
|
@ -14,7 +14,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-11-26 00:09+0000\n"
|
||||
"POT-Creation-Date: 2024-12-18 13:31+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: deluxghost, 2024\n"
|
||||
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n"
|
||||
@ -1897,6 +1897,10 @@ msgstr "Kerberos 领域"
|
||||
msgid "Custom krb5.conf to use. Uses the system one by default"
|
||||
msgstr "要使用的自定义 krb5.conf。默认使用系统自带"
|
||||
|
||||
#: authentik/sources/kerberos/models.py
|
||||
msgid "KAdmin server type"
|
||||
msgstr "KAdmin 服务器类型"
|
||||
|
||||
#: authentik/sources/kerberos/models.py
|
||||
msgid "Sync users from Kerberos into authentik"
|
||||
msgstr "从 Kerberos 同步用户到 authentik"
|
||||
@ -2857,7 +2861,7 @@ msgstr ""
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
" If you did not request a password change, please ignore this Email. The link above is valid for %(expires)s.\n"
|
||||
" If you did not request a password change, please ignore this email. The link above is valid for %(expires)s.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
@ -2881,7 +2885,7 @@ msgstr ""
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
"If you did not request a password change, please ignore this Email. The link above is valid for %(expires)s.\n"
|
||||
"If you did not request a password change, please ignore this email. The link above is valid for %(expires)s.\n"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"如果您没有请求更改密码,请忽略此电子邮件。上面的链接在 %(expires)s 内有效。\n"
|
||||
@ -3150,6 +3154,22 @@ msgstr "输入阶段"
|
||||
msgid "Passwords don't match."
|
||||
msgstr "密码不匹配。"
|
||||
|
||||
#: authentik/stages/redirect/api.py
|
||||
msgid "Target URL should be present when mode is Static."
|
||||
msgstr "当模式为静态时,目标 URL 应存在。"
|
||||
|
||||
#: authentik/stages/redirect/api.py
|
||||
msgid "Target Flow should be present when mode is Flow."
|
||||
msgstr "当模式为流程时,目标流程应存在。"
|
||||
|
||||
#: authentik/stages/redirect/models.py
|
||||
msgid "Redirect Stage"
|
||||
msgstr "重定向阶段"
|
||||
|
||||
#: authentik/stages/redirect/models.py
|
||||
msgid "Redirect Stages"
|
||||
msgstr "重定向阶段"
|
||||
|
||||
#: authentik/stages/user_delete/models.py
|
||||
msgid "User Delete Stage"
|
||||
msgstr "用户删除阶段"
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2024.10.5",
|
||||
"version": "2024.12.0",
|
||||
"private": true
|
||||
}
|
||||
|
292
poetry.lock
generated
292
poetry.lock
generated
@ -408,13 +408,13 @@ typeguard = ">=2.13.3,<5.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "aws-cdk-lib"
|
||||
version = "2.172.0"
|
||||
version = "2.173.2"
|
||||
description = "Version 2 of the AWS Cloud Development Kit library"
|
||||
optional = false
|
||||
python-versions = "~=3.8"
|
||||
files = [
|
||||
{file = "aws_cdk_lib-2.172.0-py3-none-any.whl", hash = "sha256:960b64af8eb272d2bc80d42dab4748863c2021c39dbc543bb6e7bec0fdafa099"},
|
||||
{file = "aws_cdk_lib-2.172.0.tar.gz", hash = "sha256:4e8cb368256024e2d35874d7ab2e68812177d7990a27b2ceb50c454e8a018533"},
|
||||
{file = "aws_cdk_lib-2.173.2-py3-none-any.whl", hash = "sha256:1b76846669de83e6572e9c46f5708f6ac045d8e710bafb044230f24e722601ef"},
|
||||
{file = "aws_cdk_lib-2.173.2.tar.gz", hash = "sha256:9eb355c4fd5c1aa56317549600baf88dd4d3b520e2081132119b51349ead8c03"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -1467,13 +1467,13 @@ django = ">=3"
|
||||
|
||||
[[package]]
|
||||
name = "django-pglock"
|
||||
version = "1.7.0"
|
||||
version = "1.7.1"
|
||||
description = "Postgres locking routines and lock table access."
|
||||
optional = false
|
||||
python-versions = "<4,>=3.9.0"
|
||||
files = [
|
||||
{file = "django_pglock-1.7.0-py3-none-any.whl", hash = "sha256:4e28fa19cae96f072f3b74a368519442c5413b1ce72f75f816b77dd567d456df"},
|
||||
{file = "django_pglock-1.7.0.tar.gz", hash = "sha256:180da6d3067b1dcb46b5e37745ee32fe0d8d5976c53bc8912dcf2a46e5000b6a"},
|
||||
{file = "django_pglock-1.7.1-py3-none-any.whl", hash = "sha256:15db418fb56bee37fc8707038495b5085af9b8c203ebfa300202572127bdb3f0"},
|
||||
{file = "django_pglock-1.7.1.tar.gz", hash = "sha256:69050bdb522fd34585d49bb8a4798dbfbab9ec4754dd1927b1b9eef2ec0edadf"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -1922,13 +1922,13 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"]
|
||||
|
||||
[[package]]
|
||||
name = "google-api-python-client"
|
||||
version = "2.154.0"
|
||||
version = "2.156.0"
|
||||
description = "Google API Client Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "google_api_python_client-2.154.0-py2.py3-none-any.whl", hash = "sha256:a521bbbb2ec0ba9d6f307cdd64ed6e21eeac372d1bd7493a4ab5022941f784ad"},
|
||||
{file = "google_api_python_client-2.154.0.tar.gz", hash = "sha256:1b420062e03bfcaa1c79e2e00a612d29a6a934151ceb3d272fe150a656dc8f17"},
|
||||
{file = "google_api_python_client-2.156.0-py2.py3-none-any.whl", hash = "sha256:6352185c505e1f311f11b0b96c1b636dcb0fec82cd04b80ac5a671ac4dcab339"},
|
||||
{file = "google_api_python_client-2.156.0.tar.gz", hash = "sha256:b809c111ded61716a9c1c7936e6899053f13bae3defcdfda904bd2ca68065b9c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -3107,13 +3107,13 @@ dev = ["bumpver", "isort", "mypy", "pylint", "pytest", "yapf"]
|
||||
|
||||
[[package]]
|
||||
name = "msgraph-sdk"
|
||||
version = "1.14.0"
|
||||
version = "1.15.0"
|
||||
description = "The Microsoft Graph Python SDK"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "msgraph_sdk-1.14.0-py3-none-any.whl", hash = "sha256:1a2f327dc8fbe5a5e6d0d84cf71d605e7b118b3066b1e16f011ccd8fd927bb03"},
|
||||
{file = "msgraph_sdk-1.14.0.tar.gz", hash = "sha256:5bbda80941c5d1794682753b8b291bd2ebed719a43d6de949fd0cd613b6dfbbd"},
|
||||
{file = "msgraph_sdk-1.15.0-py3-none-any.whl", hash = "sha256:85332db7ee19eb3d65a2493de83994ce3f5e4d9a084b3643ff6dea797cda81a7"},
|
||||
{file = "msgraph_sdk-1.15.0.tar.gz", hash = "sha256:c920e72cc9de2218f9f9f71682db22ea544d9b440a5f088892bfca686c546b91"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -3463,13 +3463,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "pdoc"
|
||||
version = "15.0.0"
|
||||
version = "15.0.1"
|
||||
description = "API Documentation for Python Projects"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "pdoc-15.0.0-py3-none-any.whl", hash = "sha256:151b0187a25eaf827099e981d6dbe3a4f68aeb18d0d637c24edcab788d5540f1"},
|
||||
{file = "pdoc-15.0.0.tar.gz", hash = "sha256:b761220d3ba129cd87e6da1bb7b62c8e799973ab9c595de7ba1a514850d86da5"},
|
||||
{file = "pdoc-15.0.1-py3-none-any.whl", hash = "sha256:fd437ab8eb55f9b942226af7865a3801e2fb731665199b74fd9a44737dbe20f9"},
|
||||
{file = "pdoc-15.0.1.tar.gz", hash = "sha256:3b08382c9d312243ee6c2a1813d0ff517a6ab84d596fa2c6c6b5255b17c3d666"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -3881,19 +3881,19 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.10.3"
|
||||
version = "2.10.4"
|
||||
description = "Data validation using Python type hints"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d"},
|
||||
{file = "pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9"},
|
||||
{file = "pydantic-2.10.4-py3-none-any.whl", hash = "sha256:597e135ea68be3a37552fb524bc7d0d66dcf93d395acd93a00682f1efcb8ee3d"},
|
||||
{file = "pydantic-2.10.4.tar.gz", hash = "sha256:82f12e9723da6de4fe2ba888b5971157b3be7ad914267dea8f05f82b28254f06"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
annotated-types = ">=0.6.0"
|
||||
email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"email\""}
|
||||
pydantic-core = "2.27.1"
|
||||
pydantic-core = "2.27.2"
|
||||
typing-extensions = ">=4.12.2"
|
||||
|
||||
[package.extras]
|
||||
@ -3902,111 +3902,111 @@ timezone = ["tzdata"]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.27.1"
|
||||
version = "2.27.2"
|
||||
description = "Core functionality for Pydantic validation and serialization"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a"},
|
||||
{file = "pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b"},
|
||||
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278"},
|
||||
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05"},
|
||||
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4"},
|
||||
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f"},
|
||||
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08"},
|
||||
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6"},
|
||||
{file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807"},
|
||||
{file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c"},
|
||||
{file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206"},
|
||||
{file = "pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c"},
|
||||
{file = "pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17"},
|
||||
{file = "pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8"},
|
||||
{file = "pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330"},
|
||||
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52"},
|
||||
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4"},
|
||||
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c"},
|
||||
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de"},
|
||||
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025"},
|
||||
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e"},
|
||||
{file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919"},
|
||||
{file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c"},
|
||||
{file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc"},
|
||||
{file = "pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9"},
|
||||
{file = "pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5"},
|
||||
{file = "pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89"},
|
||||
{file = "pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f"},
|
||||
{file = "pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02"},
|
||||
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c"},
|
||||
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac"},
|
||||
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb"},
|
||||
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529"},
|
||||
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35"},
|
||||
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089"},
|
||||
{file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381"},
|
||||
{file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb"},
|
||||
{file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae"},
|
||||
{file = "pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c"},
|
||||
{file = "pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16"},
|
||||
{file = "pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e"},
|
||||
{file = "pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073"},
|
||||
{file = "pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08"},
|
||||
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf"},
|
||||
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737"},
|
||||
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2"},
|
||||
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107"},
|
||||
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51"},
|
||||
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a"},
|
||||
{file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc"},
|
||||
{file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960"},
|
||||
{file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23"},
|
||||
{file = "pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05"},
|
||||
{file = "pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337"},
|
||||
{file = "pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5"},
|
||||
{file = "pydantic_core-2.27.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:5897bec80a09b4084aee23f9b73a9477a46c3304ad1d2d07acca19723fb1de62"},
|
||||
{file = "pydantic_core-2.27.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d0165ab2914379bd56908c02294ed8405c252250668ebcb438a55494c69f44ab"},
|
||||
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b9af86e1d8e4cfc82c2022bfaa6f459381a50b94a29e95dcdda8442d6d83864"},
|
||||
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f6c8a66741c5f5447e047ab0ba7a1c61d1e95580d64bce852e3df1f895c4067"},
|
||||
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a42d6a8156ff78981f8aa56eb6394114e0dedb217cf8b729f438f643608cbcd"},
|
||||
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64c65f40b4cd8b0e049a8edde07e38b476da7e3aaebe63287c899d2cff253fa5"},
|
||||
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdcf339322a3fae5cbd504edcefddd5a50d9ee00d968696846f089b4432cf78"},
|
||||
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bf99c8404f008750c846cb4ac4667b798a9f7de673ff719d705d9b2d6de49c5f"},
|
||||
{file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8f1edcea27918d748c7e5e4d917297b2a0ab80cad10f86631e488b7cddf76a36"},
|
||||
{file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:159cac0a3d096f79ab6a44d77a961917219707e2a130739c64d4dd46281f5c2a"},
|
||||
{file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:029d9757eb621cc6e1848fa0b0310310de7301057f623985698ed7ebb014391b"},
|
||||
{file = "pydantic_core-2.27.1-cp38-none-win32.whl", hash = "sha256:a28af0695a45f7060e6f9b7092558a928a28553366519f64083c63a44f70e618"},
|
||||
{file = "pydantic_core-2.27.1-cp38-none-win_amd64.whl", hash = "sha256:2d4567c850905d5eaaed2f7a404e61012a51caf288292e016360aa2b96ff38d4"},
|
||||
{file = "pydantic_core-2.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e9386266798d64eeb19dd3677051f5705bf873e98e15897ddb7d76f477131967"},
|
||||
{file = "pydantic_core-2.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4228b5b646caa73f119b1ae756216b59cc6e2267201c27d3912b592c5e323b60"},
|
||||
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3dfe500de26c52abe0477dde16192ac39c98f05bf2d80e76102d394bd13854"},
|
||||
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aee66be87825cdf72ac64cb03ad4c15ffef4143dbf5c113f64a5ff4f81477bf9"},
|
||||
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b748c44bb9f53031c8cbc99a8a061bc181c1000c60a30f55393b6e9c45cc5bd"},
|
||||
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ca038c7f6a0afd0b2448941b6ef9d5e1949e999f9e5517692eb6da58e9d44be"},
|
||||
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e0bd57539da59a3e4671b90a502da9a28c72322a4f17866ba3ac63a82c4498e"},
|
||||
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac6c2c45c847bbf8f91930d88716a0fb924b51e0c6dad329b793d670ec5db792"},
|
||||
{file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b94d4ba43739bbe8b0ce4262bcc3b7b9f31459ad120fb595627eaeb7f9b9ca01"},
|
||||
{file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:00e6424f4b26fe82d44577b4c842d7df97c20be6439e8e685d0d715feceb9fb9"},
|
||||
{file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:38de0a70160dd97540335b7ad3a74571b24f1dc3ed33f815f0880682e6880131"},
|
||||
{file = "pydantic_core-2.27.1-cp39-none-win32.whl", hash = "sha256:7ccebf51efc61634f6c2344da73e366c75e735960b5654b63d7e6f69a5885fa3"},
|
||||
{file = "pydantic_core-2.27.1-cp39-none-win_amd64.whl", hash = "sha256:a57847b090d7892f123726202b7daa20df6694cbd583b67a592e856bff603d6c"},
|
||||
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6"},
|
||||
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676"},
|
||||
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d"},
|
||||
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c"},
|
||||
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27"},
|
||||
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f"},
|
||||
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed"},
|
||||
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f"},
|
||||
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c"},
|
||||
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5fde892e6c697ce3e30c61b239330fc5d569a71fefd4eb6512fc6caec9dd9e2f"},
|
||||
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:816f5aa087094099fff7edabb5e01cc370eb21aa1a1d44fe2d2aefdfb5599b31"},
|
||||
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c10c309e18e443ddb108f0ef64e8729363adbfd92d6d57beec680f6261556f3"},
|
||||
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98476c98b02c8e9b2eec76ac4156fd006628b1b2d0ef27e548ffa978393fd154"},
|
||||
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3027001c28434e7ca5a6e1e527487051136aa81803ac812be51802150d880dd"},
|
||||
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:7699b1df36a48169cdebda7ab5a2bac265204003f153b4bd17276153d997670a"},
|
||||
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1c39b07d90be6b48968ddc8c19e7585052088fd7ec8d568bb31ff64c70ae3c97"},
|
||||
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:46ccfe3032b3915586e469d4972973f893c0a2bb65669194a5bdea9bacc088c2"},
|
||||
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:62ba45e21cf6571d7f716d903b5b7b6d2617e2d5d67c0923dc47b9d41369f840"},
|
||||
{file = "pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235"},
|
||||
{file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"},
|
||||
{file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"},
|
||||
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"},
|
||||
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"},
|
||||
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"},
|
||||
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"},
|
||||
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"},
|
||||
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"},
|
||||
{file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"},
|
||||
{file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"},
|
||||
{file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"},
|
||||
{file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"},
|
||||
{file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"},
|
||||
{file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"},
|
||||
{file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"},
|
||||
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"},
|
||||
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"},
|
||||
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"},
|
||||
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"},
|
||||
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"},
|
||||
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"},
|
||||
{file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"},
|
||||
{file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"},
|
||||
{file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"},
|
||||
{file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"},
|
||||
{file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"},
|
||||
{file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"},
|
||||
{file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"},
|
||||
{file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"},
|
||||
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"},
|
||||
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"},
|
||||
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"},
|
||||
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"},
|
||||
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"},
|
||||
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"},
|
||||
{file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"},
|
||||
{file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"},
|
||||
{file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"},
|
||||
{file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"},
|
||||
{file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"},
|
||||
{file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"},
|
||||
{file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"},
|
||||
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"},
|
||||
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"},
|
||||
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"},
|
||||
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"},
|
||||
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"},
|
||||
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"},
|
||||
{file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"},
|
||||
{file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"},
|
||||
{file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"},
|
||||
{file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"},
|
||||
{file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"},
|
||||
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"},
|
||||
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"},
|
||||
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"},
|
||||
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"},
|
||||
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"},
|
||||
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"},
|
||||
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"},
|
||||
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"},
|
||||
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"},
|
||||
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"},
|
||||
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"},
|
||||
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"},
|
||||
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"},
|
||||
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"},
|
||||
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"},
|
||||
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"},
|
||||
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"},
|
||||
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"},
|
||||
{file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -4630,29 +4630,29 @@ pyasn1 = ">=0.1.3"
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.8.2"
|
||||
version = "0.8.3"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "ruff-0.8.2-py3-none-linux_armv6l.whl", hash = "sha256:c49ab4da37e7c457105aadfd2725e24305ff9bc908487a9bf8d548c6dad8bb3d"},
|
||||
{file = "ruff-0.8.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ec016beb69ac16be416c435828be702ee694c0d722505f9c1f35e1b9c0cc1bf5"},
|
||||
{file = "ruff-0.8.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f05cdf8d050b30e2ba55c9b09330b51f9f97d36d4673213679b965d25a785f3c"},
|
||||
{file = "ruff-0.8.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60f578c11feb1d3d257b2fb043ddb47501ab4816e7e221fbb0077f0d5d4e7b6f"},
|
||||
{file = "ruff-0.8.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cbd5cf9b0ae8f30eebc7b360171bd50f59ab29d39f06a670b3e4501a36ba5897"},
|
||||
{file = "ruff-0.8.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b402ddee3d777683de60ff76da801fa7e5e8a71038f57ee53e903afbcefdaa58"},
|
||||
{file = "ruff-0.8.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:705832cd7d85605cb7858d8a13d75993c8f3ef1397b0831289109e953d833d29"},
|
||||
{file = "ruff-0.8.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:32096b41aaf7a5cc095fa45b4167b890e4c8d3fd217603f3634c92a541de7248"},
|
||||
{file = "ruff-0.8.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e769083da9439508833cfc7c23e351e1809e67f47c50248250ce1ac52c21fb93"},
|
||||
{file = "ruff-0.8.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fe716592ae8a376c2673fdfc1f5c0c193a6d0411f90a496863c99cd9e2ae25d"},
|
||||
{file = "ruff-0.8.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:81c148825277e737493242b44c5388a300584d73d5774defa9245aaef55448b0"},
|
||||
{file = "ruff-0.8.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d261d7850c8367704874847d95febc698a950bf061c9475d4a8b7689adc4f7fa"},
|
||||
{file = "ruff-0.8.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1ca4e3a87496dc07d2427b7dd7ffa88a1e597c28dad65ae6433ecb9f2e4f022f"},
|
||||
{file = "ruff-0.8.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:729850feed82ef2440aa27946ab39c18cb4a8889c1128a6d589ffa028ddcfc22"},
|
||||
{file = "ruff-0.8.2-py3-none-win32.whl", hash = "sha256:ac42caaa0411d6a7d9594363294416e0e48fc1279e1b0e948391695db2b3d5b1"},
|
||||
{file = "ruff-0.8.2-py3-none-win_amd64.whl", hash = "sha256:2aae99ec70abf43372612a838d97bfe77d45146254568d94926e8ed5bbb409ea"},
|
||||
{file = "ruff-0.8.2-py3-none-win_arm64.whl", hash = "sha256:fb88e2a506b70cfbc2de6fae6681c4f944f7dd5f2fe87233a7233d888bad73e8"},
|
||||
{file = "ruff-0.8.2.tar.gz", hash = "sha256:b84f4f414dda8ac7f75075c1fa0b905ac0ff25361f42e6d5da681a465e0f78e5"},
|
||||
{file = "ruff-0.8.3-py3-none-linux_armv6l.whl", hash = "sha256:8d5d273ffffff0acd3db5bf626d4b131aa5a5ada1276126231c4174543ce20d6"},
|
||||
{file = "ruff-0.8.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e4d66a21de39f15c9757d00c50c8cdd20ac84f55684ca56def7891a025d7e939"},
|
||||
{file = "ruff-0.8.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c356e770811858bd20832af696ff6c7e884701115094f427b64b25093d6d932d"},
|
||||
{file = "ruff-0.8.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c0a60a825e3e177116c84009d5ebaa90cf40dfab56e1358d1df4e29a9a14b13"},
|
||||
{file = "ruff-0.8.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fb782f4db39501210ac093c79c3de581d306624575eddd7e4e13747e61ba18"},
|
||||
{file = "ruff-0.8.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f26bc76a133ecb09a38b7868737eded6941b70a6d34ef53a4027e83913b6502"},
|
||||
{file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:01b14b2f72a37390c1b13477c1c02d53184f728be2f3ffc3ace5b44e9e87b90d"},
|
||||
{file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53babd6e63e31f4e96ec95ea0d962298f9f0d9cc5990a1bbb023a6baf2503a82"},
|
||||
{file = "ruff-0.8.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ae441ce4cf925b7f363d33cd6570c51435972d697e3e58928973994e56e1452"},
|
||||
{file = "ruff-0.8.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7c65bc0cadce32255e93c57d57ecc2cca23149edd52714c0c5d6fa11ec328cd"},
|
||||
{file = "ruff-0.8.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5be450bb18f23f0edc5a4e5585c17a56ba88920d598f04a06bd9fd76d324cb20"},
|
||||
{file = "ruff-0.8.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8faeae3827eaa77f5721f09b9472a18c749139c891dbc17f45e72d8f2ca1f8fc"},
|
||||
{file = "ruff-0.8.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:db503486e1cf074b9808403991663e4277f5c664d3fe237ee0d994d1305bb060"},
|
||||
{file = "ruff-0.8.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6567be9fb62fbd7a099209257fef4ad2c3153b60579818b31a23c886ed4147ea"},
|
||||
{file = "ruff-0.8.3-py3-none-win32.whl", hash = "sha256:19048f2f878f3ee4583fc6cb23fb636e48c2635e30fb2022b3a1cd293402f964"},
|
||||
{file = "ruff-0.8.3-py3-none-win_amd64.whl", hash = "sha256:f7df94f57d7418fa7c3ffb650757e0c2b96cf2501a0b192c18e4fb5571dfada9"},
|
||||
{file = "ruff-0.8.3-py3-none-win_arm64.whl", hash = "sha256:fe2756edf68ea79707c8d68b78ca9a58ed9af22e430430491ee03e718b5e4936"},
|
||||
{file = "ruff-0.8.3.tar.gz", hash = "sha256:5e7558304353b84279042fc584a4f4cb8a07ae79b2bf3da1a7551d960b5626d3"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -5087,13 +5087,13 @@ wsproto = ">=0.14"
|
||||
|
||||
[[package]]
|
||||
name = "twilio"
|
||||
version = "9.3.8"
|
||||
version = "9.4.1"
|
||||
description = "Twilio API client and TwiML generator"
|
||||
optional = false
|
||||
python-versions = ">=3.7.0"
|
||||
files = [
|
||||
{file = "twilio-9.3.8-py2.py3-none-any.whl", hash = "sha256:fce1f629295285d583dbe1d615f114a77aab25a654ba569bb18d304d31e9ca3b"},
|
||||
{file = "twilio-9.3.8.tar.gz", hash = "sha256:93a80639db711e58915cfdf772da6274b005ef86f5d2f6092433cb3d53a25303"},
|
||||
{file = "twilio-9.4.1-py2.py3-none-any.whl", hash = "sha256:2447e041cec11167d7765aaa62ab1dae3b82b712245ca9a966096acd8b9f426f"},
|
||||
{file = "twilio-9.4.1.tar.gz", hash = "sha256:e24c640696ccc726bba14160951da3cfc6b4bcb772fdcb3e8c16dc3cc851ef12"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -5258,13 +5258,13 @@ zstd = ["zstandard (>=0.18.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.32.1"
|
||||
version = "0.34.0"
|
||||
description = "The lightning-fast ASGI server."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "uvicorn-0.32.1-py3-none-any.whl", hash = "sha256:82ad92fd58da0d12af7482ecdb5f2470a04c9c9a53ced65b9bbb4a205377602e"},
|
||||
{file = "uvicorn-0.32.1.tar.gz", hash = "sha256:ee9519c246a72b1c084cea8d3b44ed6026e78a4a309cbedae9c37e4cb9fbb175"},
|
||||
{file = "uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4"},
|
||||
{file = "uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "authentik"
|
||||
version = "2024.10.5"
|
||||
version = "2024.12.0"
|
||||
description = ""
|
||||
authors = ["authentik Team <hello@goauthentik.io>"]
|
||||
|
||||
|
644
schema.yml
644
schema.yml
@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: authentik
|
||||
version: 2024.10.5
|
||||
version: 2024.12.0
|
||||
description: Making authentication simple.
|
||||
contact:
|
||||
email: hello@goauthentik.io
|
||||
@ -3097,6 +3097,285 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/core/application_entitlements/:
|
||||
get:
|
||||
operationId: core_application_entitlements_list
|
||||
description: ApplicationEntitlement Viewset
|
||||
parameters:
|
||||
- in: query
|
||||
name: app
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- in: query
|
||||
name: name
|
||||
schema:
|
||||
type: string
|
||||
- name: ordering
|
||||
required: false
|
||||
in: query
|
||||
description: Which field to use when ordering the results.
|
||||
schema:
|
||||
type: string
|
||||
- name: page
|
||||
required: false
|
||||
in: query
|
||||
description: A page number within the paginated result set.
|
||||
schema:
|
||||
type: integer
|
||||
- name: page_size
|
||||
required: false
|
||||
in: query
|
||||
description: Number of results to return per page.
|
||||
schema:
|
||||
type: integer
|
||||
- in: query
|
||||
name: pbm_uuid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- name: search
|
||||
required: false
|
||||
in: query
|
||||
description: A search term.
|
||||
schema:
|
||||
type: string
|
||||
tags:
|
||||
- core
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PaginatedApplicationEntitlementList'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
post:
|
||||
operationId: core_application_entitlements_create
|
||||
description: ApplicationEntitlement Viewset
|
||||
tags:
|
||||
- core
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApplicationEntitlementRequest'
|
||||
required: true
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'201':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApplicationEntitlement'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/core/application_entitlements/{pbm_uuid}/:
|
||||
get:
|
||||
operationId: core_application_entitlements_retrieve
|
||||
description: ApplicationEntitlement Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: pbm_uuid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this Application Entitlement.
|
||||
required: true
|
||||
tags:
|
||||
- core
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApplicationEntitlement'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
put:
|
||||
operationId: core_application_entitlements_update
|
||||
description: ApplicationEntitlement Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: pbm_uuid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this Application Entitlement.
|
||||
required: true
|
||||
tags:
|
||||
- core
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApplicationEntitlementRequest'
|
||||
required: true
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApplicationEntitlement'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
patch:
|
||||
operationId: core_application_entitlements_partial_update
|
||||
description: ApplicationEntitlement Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: pbm_uuid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this Application Entitlement.
|
||||
required: true
|
||||
tags:
|
||||
- core
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PatchedApplicationEntitlementRequest'
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApplicationEntitlement'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
delete:
|
||||
operationId: core_application_entitlements_destroy
|
||||
description: ApplicationEntitlement Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: pbm_uuid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this Application Entitlement.
|
||||
required: true
|
||||
tags:
|
||||
- core
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'204':
|
||||
description: No response body
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/core/application_entitlements/{pbm_uuid}/used_by/:
|
||||
get:
|
||||
operationId: core_application_entitlements_used_by_list
|
||||
description: Get a list of all objects that use this object
|
||||
parameters:
|
||||
- in: path
|
||||
name: pbm_uuid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this Application Entitlement.
|
||||
required: true
|
||||
tags:
|
||||
- core
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/UsedBy'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/core/applications/:
|
||||
get:
|
||||
operationId: core_applications_list
|
||||
@ -23305,6 +23584,7 @@ paths:
|
||||
- authentik_blueprints.blueprintinstance
|
||||
- authentik_brands.brand
|
||||
- authentik_core.application
|
||||
- authentik_core.applicationentitlement
|
||||
- authentik_core.group
|
||||
- authentik_core.token
|
||||
- authentik_core.user
|
||||
@ -23545,6 +23825,7 @@ paths:
|
||||
- authentik_blueprints.blueprintinstance
|
||||
- authentik_brands.brand
|
||||
- authentik_core.application
|
||||
- authentik_core.applicationentitlement
|
||||
- authentik_core.group
|
||||
- authentik_core.token
|
||||
- authentik_core.user
|
||||
@ -25008,6 +25289,12 @@ paths:
|
||||
required: true
|
||||
tags:
|
||||
- sources
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GroupKerberosSourceConnectionRequest'
|
||||
required: true
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
@ -25042,6 +25329,11 @@ paths:
|
||||
required: true
|
||||
tags:
|
||||
- sources
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PatchedGroupKerberosSourceConnectionRequest'
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
@ -25196,6 +25488,12 @@ paths:
|
||||
description: Group-source connection Viewset
|
||||
tags:
|
||||
- sources
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GroupOAuthSourceConnectionRequest'
|
||||
required: true
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
@ -25263,6 +25561,12 @@ paths:
|
||||
required: true
|
||||
tags:
|
||||
- sources
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GroupOAuthSourceConnectionRequest'
|
||||
required: true
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
@ -25296,6 +25600,11 @@ paths:
|
||||
required: true
|
||||
tags:
|
||||
- sources
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PatchedGroupOAuthSourceConnectionRequest'
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
@ -25448,6 +25757,12 @@ paths:
|
||||
description: Group-source connection Viewset
|
||||
tags:
|
||||
- sources
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GroupPlexSourceConnectionRequest'
|
||||
required: true
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
@ -25515,6 +25830,12 @@ paths:
|
||||
required: true
|
||||
tags:
|
||||
- sources
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GroupPlexSourceConnectionRequest'
|
||||
required: true
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
@ -25548,6 +25869,11 @@ paths:
|
||||
required: true
|
||||
tags:
|
||||
- sources
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PatchedGroupPlexSourceConnectionRequest'
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
@ -25741,6 +26067,12 @@ paths:
|
||||
required: true
|
||||
tags:
|
||||
- sources
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GroupSAMLSourceConnectionRequest'
|
||||
required: true
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
@ -25774,6 +26106,11 @@ paths:
|
||||
required: true
|
||||
tags:
|
||||
- sources
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PatchedGroupSAMLSourceConnectionRequest'
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
@ -28734,6 +29071,12 @@ paths:
|
||||
required: true
|
||||
tags:
|
||||
- sources
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserSourceConnectionRequest'
|
||||
required: true
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
@ -28767,6 +29110,11 @@ paths:
|
||||
required: true
|
||||
tags:
|
||||
- sources
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PatchedUserSourceConnectionRequest'
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
@ -38070,6 +38418,38 @@ components:
|
||||
- pk
|
||||
- provider_obj
|
||||
- slug
|
||||
ApplicationEntitlement:
|
||||
type: object
|
||||
description: ApplicationEntitlement Serializer
|
||||
properties:
|
||||
pbm_uuid:
|
||||
type: string
|
||||
format: uuid
|
||||
readOnly: true
|
||||
name:
|
||||
type: string
|
||||
app:
|
||||
type: string
|
||||
format: uuid
|
||||
attributes: {}
|
||||
required:
|
||||
- app
|
||||
- name
|
||||
- pbm_uuid
|
||||
ApplicationEntitlementRequest:
|
||||
type: object
|
||||
description: ApplicationEntitlement Serializer
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
minLength: 1
|
||||
app:
|
||||
type: string
|
||||
format: uuid
|
||||
attributes: {}
|
||||
required:
|
||||
- app
|
||||
- name
|
||||
ApplicationRequest:
|
||||
type: object
|
||||
description: Application Serializer
|
||||
@ -42555,14 +42935,15 @@ components:
|
||||
group:
|
||||
type: string
|
||||
format: uuid
|
||||
readOnly: true
|
||||
source:
|
||||
type: string
|
||||
format: uuid
|
||||
source_obj:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Source'
|
||||
readOnly: true
|
||||
identifier:
|
||||
type: string
|
||||
readOnly: true
|
||||
created:
|
||||
type: string
|
||||
format: date-time
|
||||
@ -42573,6 +42954,24 @@ components:
|
||||
- identifier
|
||||
- pk
|
||||
- source
|
||||
- source_obj
|
||||
GroupKerberosSourceConnectionRequest:
|
||||
type: object
|
||||
description: OAuth Group-Source connection Serializer
|
||||
properties:
|
||||
group:
|
||||
type: string
|
||||
format: uuid
|
||||
source:
|
||||
type: string
|
||||
format: uuid
|
||||
identifier:
|
||||
type: string
|
||||
minLength: 1
|
||||
required:
|
||||
- group
|
||||
- identifier
|
||||
- source
|
||||
GroupMatchingModeEnum:
|
||||
enum:
|
||||
- identifier
|
||||
@ -42667,14 +43066,15 @@ components:
|
||||
group:
|
||||
type: string
|
||||
format: uuid
|
||||
readOnly: true
|
||||
source:
|
||||
type: string
|
||||
format: uuid
|
||||
source_obj:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Source'
|
||||
readOnly: true
|
||||
identifier:
|
||||
type: string
|
||||
readOnly: true
|
||||
created:
|
||||
type: string
|
||||
format: date-time
|
||||
@ -42685,6 +43085,24 @@ components:
|
||||
- identifier
|
||||
- pk
|
||||
- source
|
||||
- source_obj
|
||||
GroupOAuthSourceConnectionRequest:
|
||||
type: object
|
||||
description: OAuth Group-Source connection Serializer
|
||||
properties:
|
||||
group:
|
||||
type: string
|
||||
format: uuid
|
||||
source:
|
||||
type: string
|
||||
format: uuid
|
||||
identifier:
|
||||
type: string
|
||||
minLength: 1
|
||||
required:
|
||||
- group
|
||||
- identifier
|
||||
- source
|
||||
GroupPlexSourceConnection:
|
||||
type: object
|
||||
description: Plex Group-Source connection Serializer
|
||||
@ -42696,14 +43114,15 @@ components:
|
||||
group:
|
||||
type: string
|
||||
format: uuid
|
||||
readOnly: true
|
||||
source:
|
||||
type: string
|
||||
format: uuid
|
||||
source_obj:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Source'
|
||||
readOnly: true
|
||||
identifier:
|
||||
type: string
|
||||
readOnly: true
|
||||
created:
|
||||
type: string
|
||||
format: date-time
|
||||
@ -42714,6 +43133,24 @@ components:
|
||||
- identifier
|
||||
- pk
|
||||
- source
|
||||
- source_obj
|
||||
GroupPlexSourceConnectionRequest:
|
||||
type: object
|
||||
description: Plex Group-Source connection Serializer
|
||||
properties:
|
||||
group:
|
||||
type: string
|
||||
format: uuid
|
||||
source:
|
||||
type: string
|
||||
format: uuid
|
||||
identifier:
|
||||
type: string
|
||||
minLength: 1
|
||||
required:
|
||||
- group
|
||||
- identifier
|
||||
- source
|
||||
GroupRequest:
|
||||
type: object
|
||||
description: Group Serializer
|
||||
@ -42753,14 +43190,15 @@ components:
|
||||
group:
|
||||
type: string
|
||||
format: uuid
|
||||
readOnly: true
|
||||
source:
|
||||
type: string
|
||||
format: uuid
|
||||
source_obj:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Source'
|
||||
readOnly: true
|
||||
identifier:
|
||||
type: string
|
||||
readOnly: true
|
||||
created:
|
||||
type: string
|
||||
format: date-time
|
||||
@ -42771,6 +43209,24 @@ components:
|
||||
- identifier
|
||||
- pk
|
||||
- source
|
||||
- source_obj
|
||||
GroupSAMLSourceConnectionRequest:
|
||||
type: object
|
||||
description: OAuth Group-Source connection Serializer
|
||||
properties:
|
||||
group:
|
||||
type: string
|
||||
format: uuid
|
||||
source:
|
||||
type: string
|
||||
format: uuid
|
||||
identifier:
|
||||
type: string
|
||||
minLength: 1
|
||||
required:
|
||||
- group
|
||||
- identifier
|
||||
- source
|
||||
IdentificationChallenge:
|
||||
type: object
|
||||
description: Identification challenges with all UI elements
|
||||
@ -42801,7 +43257,9 @@ components:
|
||||
flow_designation:
|
||||
$ref: '#/components/schemas/FlowDesignationEnum'
|
||||
captcha_stage:
|
||||
$ref: '#/components/schemas/CaptchaChallenge'
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/CaptchaChallenge'
|
||||
nullable: true
|
||||
enroll_url:
|
||||
type: string
|
||||
recovery_url:
|
||||
@ -44732,6 +45190,7 @@ components:
|
||||
- authentik_core.group
|
||||
- authentik_core.user
|
||||
- authentik_core.application
|
||||
- authentik_core.applicationentitlement
|
||||
- authentik_core.token
|
||||
- authentik_enterprise.license
|
||||
- authentik_providers_google_workspace.googleworkspaceprovider
|
||||
@ -45812,6 +46271,18 @@ components:
|
||||
- radius
|
||||
- rac
|
||||
type: string
|
||||
PaginatedApplicationEntitlementList:
|
||||
type: object
|
||||
properties:
|
||||
pagination:
|
||||
$ref: '#/components/schemas/Pagination'
|
||||
results:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ApplicationEntitlement'
|
||||
required:
|
||||
- pagination
|
||||
- results
|
||||
PaginatedApplicationList:
|
||||
type: object
|
||||
properties:
|
||||
@ -47714,6 +48185,17 @@ components:
|
||||
required:
|
||||
- backends
|
||||
- name
|
||||
PatchedApplicationEntitlementRequest:
|
||||
type: object
|
||||
description: ApplicationEntitlement Serializer
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
minLength: 1
|
||||
app:
|
||||
type: string
|
||||
format: uuid
|
||||
attributes: {}
|
||||
PatchedApplicationRequest:
|
||||
type: object
|
||||
description: Application Serializer
|
||||
@ -48562,6 +49044,45 @@ components:
|
||||
default_group_email_domain:
|
||||
type: string
|
||||
minLength: 1
|
||||
PatchedGroupKerberosSourceConnectionRequest:
|
||||
type: object
|
||||
description: OAuth Group-Source connection Serializer
|
||||
properties:
|
||||
group:
|
||||
type: string
|
||||
format: uuid
|
||||
source:
|
||||
type: string
|
||||
format: uuid
|
||||
identifier:
|
||||
type: string
|
||||
minLength: 1
|
||||
PatchedGroupOAuthSourceConnectionRequest:
|
||||
type: object
|
||||
description: OAuth Group-Source connection Serializer
|
||||
properties:
|
||||
group:
|
||||
type: string
|
||||
format: uuid
|
||||
source:
|
||||
type: string
|
||||
format: uuid
|
||||
identifier:
|
||||
type: string
|
||||
minLength: 1
|
||||
PatchedGroupPlexSourceConnectionRequest:
|
||||
type: object
|
||||
description: Plex Group-Source connection Serializer
|
||||
properties:
|
||||
group:
|
||||
type: string
|
||||
format: uuid
|
||||
source:
|
||||
type: string
|
||||
format: uuid
|
||||
identifier:
|
||||
type: string
|
||||
minLength: 1
|
||||
PatchedGroupRequest:
|
||||
type: object
|
||||
description: Group Serializer
|
||||
@ -48588,6 +49109,19 @@ components:
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
PatchedGroupSAMLSourceConnectionRequest:
|
||||
type: object
|
||||
description: OAuth Group-Source connection Serializer
|
||||
properties:
|
||||
group:
|
||||
type: string
|
||||
format: uuid
|
||||
source:
|
||||
type: string
|
||||
format: uuid
|
||||
identifier:
|
||||
type: string
|
||||
minLength: 1
|
||||
PatchedIdentificationStageRequest:
|
||||
type: object
|
||||
description: IdentificationStage Serializer
|
||||
@ -50510,6 +51044,9 @@ components:
|
||||
properties:
|
||||
user:
|
||||
type: integer
|
||||
source:
|
||||
type: string
|
||||
format: uuid
|
||||
identifier:
|
||||
type: string
|
||||
minLength: 1
|
||||
@ -50562,6 +51099,11 @@ components:
|
||||
type: object
|
||||
description: OAuth Source Serializer
|
||||
properties:
|
||||
user:
|
||||
type: integer
|
||||
source:
|
||||
type: string
|
||||
format: uuid
|
||||
identifier:
|
||||
type: string
|
||||
minLength: 1
|
||||
@ -50574,6 +51116,11 @@ components:
|
||||
type: object
|
||||
description: Plex Source connection Serializer
|
||||
properties:
|
||||
user:
|
||||
type: integer
|
||||
source:
|
||||
type: string
|
||||
format: uuid
|
||||
identifier:
|
||||
type: string
|
||||
minLength: 1
|
||||
@ -50623,9 +51170,23 @@ components:
|
||||
type: object
|
||||
description: SAML Source Serializer
|
||||
properties:
|
||||
user:
|
||||
type: integer
|
||||
source:
|
||||
type: string
|
||||
format: uuid
|
||||
identifier:
|
||||
type: string
|
||||
minLength: 1
|
||||
PatchedUserSourceConnectionRequest:
|
||||
type: object
|
||||
description: User source connection
|
||||
properties:
|
||||
user:
|
||||
type: integer
|
||||
source:
|
||||
type: string
|
||||
format: uuid
|
||||
PatchedUserWriteStageRequest:
|
||||
type: object
|
||||
description: UserWriteStage Serializer
|
||||
@ -55550,6 +56111,9 @@ components:
|
||||
user:
|
||||
type: integer
|
||||
source:
|
||||
type: string
|
||||
format: uuid
|
||||
source_obj:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Source'
|
||||
readOnly: true
|
||||
@ -55564,6 +56128,7 @@ components:
|
||||
- identifier
|
||||
- pk
|
||||
- source
|
||||
- source_obj
|
||||
- user
|
||||
UserKerberosSourceConnectionRequest:
|
||||
type: object
|
||||
@ -55571,11 +56136,15 @@ components:
|
||||
properties:
|
||||
user:
|
||||
type: integer
|
||||
source:
|
||||
type: string
|
||||
format: uuid
|
||||
identifier:
|
||||
type: string
|
||||
minLength: 1
|
||||
required:
|
||||
- identifier
|
||||
- source
|
||||
- user
|
||||
UserLoginChallenge:
|
||||
type: object
|
||||
@ -55798,8 +56367,10 @@ components:
|
||||
title: ID
|
||||
user:
|
||||
type: integer
|
||||
readOnly: true
|
||||
source:
|
||||
type: string
|
||||
format: uuid
|
||||
source_obj:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Source'
|
||||
readOnly: true
|
||||
@ -55815,11 +56386,17 @@ components:
|
||||
- identifier
|
||||
- pk
|
||||
- source
|
||||
- source_obj
|
||||
- user
|
||||
UserOAuthSourceConnectionRequest:
|
||||
type: object
|
||||
description: OAuth Source Serializer
|
||||
properties:
|
||||
user:
|
||||
type: integer
|
||||
source:
|
||||
type: string
|
||||
format: uuid
|
||||
identifier:
|
||||
type: string
|
||||
minLength: 1
|
||||
@ -55830,6 +56407,8 @@ components:
|
||||
nullable: true
|
||||
required:
|
||||
- identifier
|
||||
- source
|
||||
- user
|
||||
UserObjectPermission:
|
||||
type: object
|
||||
description: User-bound object level permission
|
||||
@ -55887,8 +56466,10 @@ components:
|
||||
title: ID
|
||||
user:
|
||||
type: integer
|
||||
readOnly: true
|
||||
source:
|
||||
type: string
|
||||
format: uuid
|
||||
source_obj:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Source'
|
||||
readOnly: true
|
||||
@ -55903,11 +56484,17 @@ components:
|
||||
- identifier
|
||||
- pk
|
||||
- source
|
||||
- source_obj
|
||||
- user
|
||||
UserPlexSourceConnectionRequest:
|
||||
type: object
|
||||
description: Plex Source connection Serializer
|
||||
properties:
|
||||
user:
|
||||
type: integer
|
||||
source:
|
||||
type: string
|
||||
format: uuid
|
||||
identifier:
|
||||
type: string
|
||||
minLength: 1
|
||||
@ -55918,6 +56505,8 @@ components:
|
||||
required:
|
||||
- identifier
|
||||
- plex_token
|
||||
- source
|
||||
- user
|
||||
UserRequest:
|
||||
type: object
|
||||
description: User Serializer
|
||||
@ -55969,8 +56558,10 @@ components:
|
||||
title: ID
|
||||
user:
|
||||
type: integer
|
||||
readOnly: true
|
||||
source:
|
||||
type: string
|
||||
format: uuid
|
||||
source_obj:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Source'
|
||||
readOnly: true
|
||||
@ -55985,16 +56576,24 @@ components:
|
||||
- identifier
|
||||
- pk
|
||||
- source
|
||||
- source_obj
|
||||
- user
|
||||
UserSAMLSourceConnectionRequest:
|
||||
type: object
|
||||
description: SAML Source Serializer
|
||||
properties:
|
||||
user:
|
||||
type: integer
|
||||
source:
|
||||
type: string
|
||||
format: uuid
|
||||
identifier:
|
||||
type: string
|
||||
minLength: 1
|
||||
required:
|
||||
- identifier
|
||||
- source
|
||||
- user
|
||||
UserSelf:
|
||||
type: object
|
||||
description: User Serializer for information a user can retrieve about themselves
|
||||
@ -56130,7 +56729,7 @@ components:
|
||||
- title
|
||||
UserSourceConnection:
|
||||
type: object
|
||||
description: OAuth Source Serializer
|
||||
description: User source connection
|
||||
properties:
|
||||
pk:
|
||||
type: integer
|
||||
@ -56138,8 +56737,10 @@ components:
|
||||
title: ID
|
||||
user:
|
||||
type: integer
|
||||
readOnly: true
|
||||
source:
|
||||
type: string
|
||||
format: uuid
|
||||
source_obj:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Source'
|
||||
readOnly: true
|
||||
@ -56151,6 +56752,19 @@ components:
|
||||
- created
|
||||
- pk
|
||||
- source
|
||||
- source_obj
|
||||
- user
|
||||
UserSourceConnectionRequest:
|
||||
type: object
|
||||
description: User source connection
|
||||
properties:
|
||||
user:
|
||||
type: integer
|
||||
source:
|
||||
type: string
|
||||
format: uuid
|
||||
required:
|
||||
- source
|
||||
- user
|
||||
UserTypeEnum:
|
||||
enum:
|
||||
|
@ -9,7 +9,7 @@ http://localhost {
|
||||
uri /outpost.goauthentik.io/auth/caddy
|
||||
|
||||
# capitalization of the headers is important, otherwise they will be empty
|
||||
copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email X-Authentik-Name X-Authentik-Uid X-Authentik-Jwt X-Authentik-Meta-Jwks X-Authentik-Meta-Outpost X-Authentik-Meta-Provider X-Authentik-Meta-App X-Authentik-Meta-Version
|
||||
copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Entitlements X-Authentik-Email X-Authentik-Name X-Authentik-Uid X-Authentik-Jwt X-Authentik-Meta-Jwks X-Authentik-Meta-Outpost X-Authentik-Meta-Provider X-Authentik-Meta-App X-Authentik-Meta-Version
|
||||
|
||||
# optional, in this config trust all private ranges, should probably be set to the outposts IP
|
||||
trusted_proxies private_ranges
|
||||
|
@ -23,12 +23,14 @@ server {
|
||||
# translate headers from the outposts back to the actual upstream
|
||||
auth_request_set $authentik_username $upstream_http_x_authentik_username;
|
||||
auth_request_set $authentik_groups $upstream_http_x_authentik_groups;
|
||||
auth_request_set $authentik_entitlements $upstream_http_x_authentik_entitlements;
|
||||
auth_request_set $authentik_email $upstream_http_x_authentik_email;
|
||||
auth_request_set $authentik_name $upstream_http_x_authentik_name;
|
||||
auth_request_set $authentik_uid $upstream_http_x_authentik_uid;
|
||||
|
||||
proxy_set_header X-authentik-username $authentik_username;
|
||||
proxy_set_header X-authentik-groups $authentik_groups;
|
||||
proxy_set_header X-authentik-entitlements $authentik_entitlements;
|
||||
proxy_set_header X-authentik-email $authentik_email;
|
||||
proxy_set_header X-authentik-name $authentik_name;
|
||||
proxy_set_header X-authentik-uid $authentik_uid;
|
||||
|
@ -26,6 +26,7 @@ http:
|
||||
authResponseHeaders:
|
||||
- X-authentik-username
|
||||
- X-authentik-groups
|
||||
- X-authentik-entitlements
|
||||
- X-authentik-email
|
||||
- X-authentik-name
|
||||
- X-authentik-uid
|
||||
|
@ -192,5 +192,5 @@ class TestSourceOAuth2(SeleniumTestCase):
|
||||
results = body_json["results"]
|
||||
self.assertEqual(len(results), 1)
|
||||
connection = results[0]
|
||||
self.assertEqual(connection["source"]["slug"], self.slug)
|
||||
self.assertEqual(connection["source_obj"]["slug"], self.slug)
|
||||
self.assertEqual(connection["user"], self.user.pk)
|
||||
|
@ -41,6 +41,7 @@ export default [
|
||||
},
|
||||
files: ["src/**"],
|
||||
rules: {
|
||||
"lit/attribute-names": "off",
|
||||
// "lit/attribute-names": "error",
|
||||
"lit/no-private-properties": "error",
|
||||
// "lit/prefer-nothing": "warn",
|
||||
|
1361
web/package-lock.json
generated
1361
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -11,7 +11,7 @@
|
||||
"@floating-ui/dom": "^1.6.11",
|
||||
"@formatjs/intl-listformat": "^7.5.7",
|
||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||
"@goauthentik/api": "^2024.10.5-1734022840",
|
||||
"@goauthentik/api": "^2024.12.0-1734640050",
|
||||
"@lit-labs/ssr": "^3.2.2",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@lit/localize": "^0.12.2",
|
||||
@ -68,8 +68,8 @@
|
||||
"@types/showdown": "^2.0.6",
|
||||
"@typescript-eslint/eslint-plugin": "^8.8.0",
|
||||
"@typescript-eslint/parser": "^8.8.0",
|
||||
"@wdio/browser-runner": "^9.1.2",
|
||||
"@wdio/cli": "^9.1.2",
|
||||
"@wdio/browser-runner": "9.4",
|
||||
"@wdio/cli": "9.4",
|
||||
"@wdio/spec-reporter": "^9.1.2",
|
||||
"chokidar": "^4.0.1",
|
||||
"chromedriver": "^131.0.1",
|
||||
|
@ -9,6 +9,9 @@ const MAX_DEPTH = 4;
|
||||
const MAX_NESTED_CALLBACKS = 4;
|
||||
const MAX_PARAMS = 5;
|
||||
|
||||
// Waiting for SonarJS to be compatible
|
||||
// const MAX_COGNITIVE_COMPLEXITY = 9;
|
||||
|
||||
const rules = {
|
||||
"accessor-pairs": "error",
|
||||
"array-callback-return": "error",
|
||||
@ -126,6 +129,11 @@ const rules = {
|
||||
|
||||
"no-unused-vars": "off",
|
||||
"no-console": ["error", { allow: ["debug", "warn", "error"] }],
|
||||
// SonarJS is not yet compatible with ESLint 9. Commenting these out
|
||||
// until it is.
|
||||
// "sonarjs/cognitive-complexity": ["off", MAX_COGNITIVE_COMPLEXITY],
|
||||
// "sonarjs/no-duplicate-string": "off",
|
||||
// "sonarjs/no-nested-template-literals": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
@ -162,6 +170,7 @@ export default [
|
||||
wcconf.configs["flat/recommended"],
|
||||
litconf.configs["flat/recommended"],
|
||||
...tseslint.configs.recommended,
|
||||
// sonar.configs.recommended,
|
||||
{
|
||||
languageOptions: {
|
||||
parser: tsparser,
|
||||
|
@ -29,6 +29,7 @@ export default [
|
||||
wcconf.configs["flat/recommended"],
|
||||
litconf.configs["flat/recommended"],
|
||||
...tseslint.configs.recommended,
|
||||
// sonar.configs.recommended,
|
||||
{
|
||||
languageOptions: {
|
||||
parser: tsparser,
|
||||
@ -41,6 +42,11 @@ export default [
|
||||
rules: {
|
||||
"no-unused-vars": "off",
|
||||
"no-console": ["error", { allow: ["debug", "warn", "error"] }],
|
||||
// SonarJS is not yet compatible with ESLint 9. Commenting these out
|
||||
// until it is.
|
||||
// "sonarjs/cognitive-complexity": ["off", 9],
|
||||
// "sonarjs/no-duplicate-string": "off",
|
||||
// "sonarjs/no-nested-template-literals": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
|
@ -25,25 +25,12 @@ import { TemplateResult, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import { Application, CoreApi, PolicyEngineMode, Provider } from "@goauthentik/api";
|
||||
import { Application, CoreApi, Provider } from "@goauthentik/api";
|
||||
|
||||
import { policyOptions } from "./PolicyOptions.js";
|
||||
import "./components/ak-backchannel-input";
|
||||
import "./components/ak-provider-search-input";
|
||||
|
||||
export const policyOptions = [
|
||||
{
|
||||
label: "any",
|
||||
value: PolicyEngineMode.Any,
|
||||
default: true,
|
||||
description: html`${msg("Any policy must match to grant access")}`,
|
||||
},
|
||||
{
|
||||
label: "all",
|
||||
value: PolicyEngineMode.All,
|
||||
description: html`${msg("All policies must match to grant access")}`,
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("ak-application-form")
|
||||
export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Application, string>) {
|
||||
constructor() {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import "@goauthentik/admin/applications/ApplicationAuthorizeChart";
|
||||
import "@goauthentik/admin/applications/ApplicationCheckAccessForm";
|
||||
import "@goauthentik/admin/applications/ApplicationForm";
|
||||
import "@goauthentik/admin/applications/entitlements/ApplicationEntitlementPage";
|
||||
import "@goauthentik/admin/policies/BoundPoliciesList";
|
||||
import "@goauthentik/admin/rbac/ObjectPermissionsPage";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
@ -301,6 +302,28 @@ export class ApplicationViewPage extends AKElement {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
slot="page-app-entitlements"
|
||||
data-tab-title="${msg("Application entitlements")}"
|
||||
>
|
||||
<div slot="header" class="pf-c-banner pf-m-info">
|
||||
${msg("Application entitlements are in preview.")}
|
||||
<a href="mailto:hello+feature/app-ent@goauthentik.io"
|
||||
>${msg("Send us feedback!")}</a
|
||||
>
|
||||
</div>
|
||||
<div class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-c-card">
|
||||
<div class="pf-c-card__title">
|
||||
${msg(
|
||||
"These entitlements can be used to configure user access in this application.",
|
||||
)}
|
||||
</div>
|
||||
<ak-application-entitlements-list .app=${this.application.pk}>
|
||||
</ak-application-entitlements-list>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
slot="page-policy-bindings"
|
||||
data-tab-title="${msg("Policy / Group / User Bindings")}"
|
||||
|
@ -18,6 +18,7 @@ import { styleMap } from "lit/directives/style-map.js";
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFLabel from "@patternfly/patternfly/components/Label/label.css";
|
||||
import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
const closeButtonIcon = html`<svg
|
||||
fill="currentColor"
|
||||
@ -37,6 +38,7 @@ const closeButtonIcon = html`<svg
|
||||
export class AkApplicationWizardHint extends AKElement implements ShowHintControllerHost {
|
||||
static get styles() {
|
||||
return [
|
||||
PFBase,
|
||||
PFButton,
|
||||
PFPage,
|
||||
PFLabel,
|
||||
@ -45,6 +47,9 @@ export class AkApplicationWizardHint extends AKElement implements ShowHintContro
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.ak-hint-text {
|
||||
padding-bottom: var(--pf-global--spacer--md);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
@ -101,16 +106,20 @@ export class AkApplicationWizardHint extends AKElement implements ShowHintContro
|
||||
return html` <section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<ak-hint>
|
||||
<ak-hint-body>
|
||||
<p>
|
||||
<p class="ak-hint-text">
|
||||
You can now configure both an application and its authentication provider at
|
||||
the same time with our new Application Wizard.
|
||||
<!-- <a href="(link to docs)">Learn more about the wizard here.</a> -->
|
||||
</p>
|
||||
|
||||
<ak-application-wizard
|
||||
.open=${getURLParam("createWizard", false)}
|
||||
.showButton=${false}
|
||||
></ak-application-wizard>
|
||||
<ak-application-wizard .open=${getURLParam("createWizard", false)}>
|
||||
<button
|
||||
slot="trigger"
|
||||
class="pf-c-button pf-m-primary"
|
||||
data-ouia-component-id="start-application-wizard"
|
||||
>
|
||||
${msg("Create with wizard")}
|
||||
</button>
|
||||
</ak-application-wizard>
|
||||
</ak-hint-body>
|
||||
${this.showHintController.render()}
|
||||
</ak-hint>
|
||||
|
18
web/src/admin/applications/PolicyOptions.ts
Normal file
18
web/src/admin/applications/PolicyOptions.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { msg } from "@lit/localize";
|
||||
import { html } from "lit";
|
||||
|
||||
import { PolicyEngineMode } from "@goauthentik/api";
|
||||
|
||||
export const policyOptions = [
|
||||
{
|
||||
label: "any",
|
||||
value: PolicyEngineMode.Any,
|
||||
default: true,
|
||||
description: html`${msg("Any policy must match to grant access")}`,
|
||||
},
|
||||
{
|
||||
label: "all",
|
||||
value: PolicyEngineMode.All,
|
||||
description: html`${msg("All policies must match to grant access")}`,
|
||||
},
|
||||
];
|
@ -0,0 +1,89 @@
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/CodeMirror";
|
||||
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
||||
import "@goauthentik/elements/forms/Radio";
|
||||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
import YAML from "yaml";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult } from "lit";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
||||
|
||||
import { ApplicationEntitlement, CoreApi } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-application-entitlement-form")
|
||||
export class ApplicationEntitlementForm extends ModelForm<ApplicationEntitlement, string> {
|
||||
async loadInstance(pk: string): Promise<ApplicationEntitlement> {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreApplicationEntitlementsRetrieve({
|
||||
pbmUuid: pk,
|
||||
});
|
||||
}
|
||||
|
||||
@property()
|
||||
targetPk?: string;
|
||||
|
||||
getSuccessMessage(): string {
|
||||
if (this.instance?.pbmUuid) {
|
||||
return msg("Successfully updated entitlement.");
|
||||
} else {
|
||||
return msg("Successfully created entitlement.");
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [...super.styles, PFContent];
|
||||
}
|
||||
|
||||
send(data: ApplicationEntitlement): Promise<unknown> {
|
||||
if (this.targetPk) {
|
||||
data.app = this.targetPk;
|
||||
}
|
||||
if (this.instance?.pbmUuid) {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreApplicationEntitlementsUpdate({
|
||||
pbmUuid: this.instance.pbmUuid || "",
|
||||
applicationEntitlementRequest: data,
|
||||
});
|
||||
} else {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreApplicationEntitlementsCreate({
|
||||
applicationEntitlementRequest: data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html` <ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
|
||||
<input
|
||||
type="text"
|
||||
value="${first(this.instance?.name, "")}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Attributes")}
|
||||
?required=${false}
|
||||
name="attributes"
|
||||
>
|
||||
<ak-codemirror
|
||||
mode=${CodeMirrorMode.YAML}
|
||||
value="${YAML.stringify(first(this.instance?.attributes, {}))}"
|
||||
>
|
||||
</ak-codemirror>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Set custom attributes using YAML or JSON.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-application-entitlement-form": ApplicationEntitlementForm;
|
||||
}
|
||||
}
|
@ -0,0 +1,152 @@
|
||||
import "@goauthentik/admin/applications/entitlements/ApplicationEntitlementForm";
|
||||
import "@goauthentik/admin/policies/BoundPoliciesList";
|
||||
import { PolicyBindingCheckTarget } from "@goauthentik/admin/policies/utils";
|
||||
import "@goauthentik/admin/rbac/ObjectPermissionModal";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { PFSize } from "@goauthentik/common/enums";
|
||||
import "@goauthentik/components/ak-status-label";
|
||||
import "@goauthentik/elements/Tabs";
|
||||
import "@goauthentik/elements/forms/DeleteBulkForm";
|
||||
import "@goauthentik/elements/forms/ModalForm";
|
||||
import "@goauthentik/elements/forms/ProxyForm";
|
||||
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||
import { 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 { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import {
|
||||
ApplicationEntitlement,
|
||||
CoreApi,
|
||||
RbacPermissionsAssignedByUsersListModelEnum,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-application-entitlements-list")
|
||||
export class ApplicationEntitlementsPage extends Table<ApplicationEntitlement> {
|
||||
@property()
|
||||
app?: string;
|
||||
|
||||
checkbox = true;
|
||||
clearOnRefresh = true;
|
||||
expandable = true;
|
||||
|
||||
order = "order";
|
||||
|
||||
async apiEndpoint(): Promise<PaginatedResponse<ApplicationEntitlement>> {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreApplicationEntitlementsList({
|
||||
...(await this.defaultEndpointConfig()),
|
||||
app: this.app || "",
|
||||
});
|
||||
}
|
||||
|
||||
columns(): TableColumn[] {
|
||||
return [new TableColumn(msg("Name"), "name"), new TableColumn(msg("Actions"))];
|
||||
}
|
||||
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Application entitlement(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.usedBy=${(item: ApplicationEntitlement) => {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreApplicationEntitlementsUsedByList({
|
||||
pbmUuid: item.pbmUuid || "",
|
||||
});
|
||||
}}
|
||||
.delete=${(item: ApplicationEntitlement) => {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreApplicationEntitlementsDestroy({
|
||||
pbmUuid: item.pbmUuid || "",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
|
||||
${msg("Delete")}
|
||||
</button>
|
||||
</ak-forms-delete-bulk>`;
|
||||
}
|
||||
|
||||
row(item: ApplicationEntitlement): TemplateResult[] {
|
||||
return [
|
||||
html`${item.name}`,
|
||||
html`<ak-forms-modal size=${PFSize.Medium}>
|
||||
<span slot="submit"> ${msg("Update")} </span>
|
||||
<span slot="header"> ${msg("Update Entitlement")} </span>
|
||||
<ak-application-entitlement-form
|
||||
slot="form"
|
||||
.instancePk=${item.pbmUuid}
|
||||
targetPk=${ifDefined(this.app)}
|
||||
>
|
||||
</ak-application-entitlement-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-plain">
|
||||
<pf-tooltip position="top" content=${msg("Edit")}>
|
||||
<i class="fas fa-edit"></i>
|
||||
</pf-tooltip>
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
<ak-rbac-object-permission-modal
|
||||
model=${RbacPermissionsAssignedByUsersListModelEnum.CoreApplicationentitlement}
|
||||
objectPk=${item.pbmUuid}
|
||||
>
|
||||
</ak-rbac-object-permission-modal>`,
|
||||
];
|
||||
}
|
||||
|
||||
renderExpanded(item: ApplicationEntitlement): TemplateResult {
|
||||
return html` <td></td>
|
||||
<td role="cell" colspan="4">
|
||||
<div class="pf-c-table__expandable-row-content">
|
||||
<div class="pf-c-content">
|
||||
<p>
|
||||
${msg(
|
||||
"These bindings control which users have access to this entitlement.",
|
||||
)}
|
||||
</p>
|
||||
<ak-bound-policies-list
|
||||
.target=${item.pbmUuid}
|
||||
.allowedTypes=${[
|
||||
PolicyBindingCheckTarget.group,
|
||||
PolicyBindingCheckTarget.user,
|
||||
]}
|
||||
>
|
||||
</ak-bound-policies-list>
|
||||
</div>
|
||||
</div>
|
||||
</td>`;
|
||||
}
|
||||
|
||||
renderEmpty(): TemplateResult {
|
||||
return super.renderEmpty(
|
||||
html`<ak-empty-state
|
||||
header=${msg("No app entitlements created.")}
|
||||
icon="pf-icon-module"
|
||||
>
|
||||
<div slot="body">
|
||||
${msg(
|
||||
"This application does currently not have any application entitlement defined.",
|
||||
)}
|
||||
</div>
|
||||
<div slot="primary"></div>
|
||||
</ak-empty-state>`,
|
||||
);
|
||||
}
|
||||
|
||||
renderToolbar(): TemplateResult {
|
||||
return html`<ak-forms-modal size=${PFSize.Medium}>
|
||||
<span slot="submit"> ${msg("Create")} </span>
|
||||
<span slot="header"> ${msg("Create Entitlement")} </span>
|
||||
<ak-application-entitlement-form slot="form" targetPk=${ifDefined(this.app)}>
|
||||
</ak-application-entitlement-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary">
|
||||
${msg("Create entitlement")}
|
||||
</button>
|
||||
</ak-forms-modal> `;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-application-roles-list": ApplicationEntitlementsPage;
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ import PFFormControl from "@patternfly/patternfly/components/FormControl/form-co
|
||||
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
|
||||
import PFRadio from "@patternfly/patternfly/components/Radio/radio.css";
|
||||
import PFSwitch from "@patternfly/patternfly/components/Switch/switch.css";
|
||||
import PFWizard from "@patternfly/patternfly/components/Wizard/wizard.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
export const styles = [
|
||||
@ -20,6 +21,7 @@ export const styles = [
|
||||
PFInputGroup,
|
||||
PFFormControl,
|
||||
PFSwitch,
|
||||
PFWizard,
|
||||
css`
|
||||
select[multiple] {
|
||||
height: 15em;
|
86
web/src/admin/applications/wizard/ApplicationWizardStep.ts
Normal file
86
web/src/admin/applications/wizard/ApplicationWizardStep.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { styles } from "@goauthentik/admin/applications/wizard/ApplicationWizardFormStepStyles.css.js";
|
||||
import { WizardStep } from "@goauthentik/components/ak-wizard/WizardStep.js";
|
||||
import {
|
||||
NavigationUpdate,
|
||||
WizardNavigationEvent,
|
||||
WizardUpdateEvent,
|
||||
} from "@goauthentik/components/ak-wizard/events";
|
||||
import { KeyUnknown, serializeForm } from "@goauthentik/elements/forms/Form";
|
||||
import { HorizontalFormElement } from "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { property, query } from "lit/decorators.js";
|
||||
|
||||
import { ValidationError } from "@goauthentik/api";
|
||||
|
||||
import {
|
||||
type ApplicationWizardState,
|
||||
type ApplicationWizardStateUpdate,
|
||||
ExtendedValidationError,
|
||||
} from "./types";
|
||||
|
||||
export class ApplicationWizardStep extends WizardStep {
|
||||
static get styles() {
|
||||
return [...WizardStep.styles, ...styles];
|
||||
}
|
||||
|
||||
@property({ type: Object, attribute: false })
|
||||
wizard!: ApplicationWizardState;
|
||||
|
||||
// As recommended in [WizardStep](../../../components/ak-wizard/WizardStep.ts), we override
|
||||
// these fields and provide them to all the child classes.
|
||||
wizardTitle = msg("New application");
|
||||
wizardDescription = msg("Create a new application");
|
||||
canCancel = true;
|
||||
|
||||
// This should be overridden in the children for more precise targeting.
|
||||
@query("form")
|
||||
form!: HTMLFormElement;
|
||||
|
||||
get formValues(): KeyUnknown | undefined {
|
||||
const elements = [
|
||||
...Array.from(
|
||||
this.form.querySelectorAll<HorizontalFormElement>("ak-form-element-horizontal"),
|
||||
),
|
||||
...Array.from(this.form.querySelectorAll<HTMLElement>("[data-ak-control=true]")),
|
||||
];
|
||||
return serializeForm(elements as unknown as NodeListOf<HorizontalFormElement>);
|
||||
}
|
||||
|
||||
protected removeErrors(
|
||||
keyToDelete: keyof ExtendedValidationError,
|
||||
): ValidationError | undefined {
|
||||
if (!this.wizard.errors) {
|
||||
return undefined;
|
||||
}
|
||||
const empty = {};
|
||||
const errors = Object.entries(this.wizard.errors).reduce(
|
||||
(acc, [key, value]) =>
|
||||
key === keyToDelete ||
|
||||
value === undefined ||
|
||||
(Array.isArray(this.wizard?.errors?.[key]) && this.wizard.errors[key].length === 0)
|
||||
? acc
|
||||
: { ...acc, [key]: value },
|
||||
empty,
|
||||
);
|
||||
return errors;
|
||||
}
|
||||
|
||||
// This pattern became visible during development, and the order is important: wizard updating
|
||||
// and validation must complete before navigation is attempted.
|
||||
public handleUpdate(
|
||||
update?: ApplicationWizardStateUpdate,
|
||||
destination?: string,
|
||||
enable?: NavigationUpdate,
|
||||
) {
|
||||
// Inform ApplicationWizard of content state
|
||||
if (update) {
|
||||
this.dispatchEvent(new WizardUpdateEvent(update));
|
||||
}
|
||||
|
||||
// Inform WizardStepManager of steps state
|
||||
if (destination || enable) {
|
||||
this.dispatchEvent(new WizardNavigationEvent(destination, enable));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
import { WizardPanel } from "@goauthentik/components/ak-wizard-main/types";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { KeyUnknown, serializeForm } from "@goauthentik/elements/forms/Form";
|
||||
import { HorizontalFormElement } from "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||
|
||||
import { consume } from "@lit/context";
|
||||
import { query } from "@lit/reactive-element/decorators.js";
|
||||
|
||||
import { styles as AwadStyles } from "./BasePanel.css";
|
||||
|
||||
import { applicationWizardContext } from "./ContextIdentity";
|
||||
import type { ApplicationWizardState, ApplicationWizardStateUpdate } from "./types";
|
||||
|
||||
/**
|
||||
* Application Wizard Base Panel
|
||||
*
|
||||
* All of the displays in our system inherit from this object, which supplies the basic CSS for all
|
||||
* the inputs we display, as well as the values and validity state for the form currently being
|
||||
* displayed.
|
||||
*
|
||||
*/
|
||||
|
||||
export class ApplicationWizardPageBase
|
||||
extends CustomEmitterElement(AKElement)
|
||||
implements WizardPanel
|
||||
{
|
||||
static get styles() {
|
||||
return AwadStyles;
|
||||
}
|
||||
|
||||
@consume({ context: applicationWizardContext })
|
||||
public wizard!: ApplicationWizardState;
|
||||
|
||||
@query("form")
|
||||
form!: HTMLFormElement;
|
||||
|
||||
/**
|
||||
* Provide access to the values on the current form. Child implementations use this to craft the
|
||||
* update that will be sent using `dispatchWizardUpdate` below.
|
||||
*/
|
||||
get formValues(): KeyUnknown | undefined {
|
||||
const elements = [
|
||||
...Array.from(
|
||||
this.form.querySelectorAll<HorizontalFormElement>("ak-form-element-horizontal"),
|
||||
),
|
||||
...Array.from(this.form.querySelectorAll<HTMLElement>("[data-ak-control=true]")),
|
||||
];
|
||||
return serializeForm(elements as unknown as NodeListOf<HorizontalFormElement>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide access to the validity of the current form. Child implementations use this to craft
|
||||
* the update that will be sent using `dispatchWizardUpdate` below.
|
||||
*/
|
||||
get valid() {
|
||||
return this.form.checkValidity();
|
||||
}
|
||||
|
||||
rendered = false;
|
||||
|
||||
/**
|
||||
* Provide a single source of truth for the token used to notify the orchestrator that an event
|
||||
* happens. The token `ak-wizard-update` is used by the Wizard framework's reactive controller
|
||||
* to route "data on the current step has changed" events to the orchestrator.
|
||||
*/
|
||||
dispatchWizardUpdate(update: ApplicationWizardStateUpdate) {
|
||||
this.dispatchCustomEvent("ak-wizard-update", update);
|
||||
}
|
||||
}
|
||||
|
||||
export default ApplicationWizardPageBase;
|
@ -1,7 +1,7 @@
|
||||
import { createContext } from "@lit/context";
|
||||
|
||||
import { ApplicationWizardState } from "./types";
|
||||
import { LocalTypeCreate } from "./steps/ProviderChoices.js";
|
||||
|
||||
export const applicationWizardContext = createContext<ApplicationWizardState>(
|
||||
Symbol("ak-application-wizard-state-context"),
|
||||
export const applicationWizardProvidersContext = createContext<LocalTypeCreate[]>(
|
||||
Symbol("ak-application-wizard-providers-context"),
|
||||
);
|
||||
|
109
web/src/admin/applications/wizard/ak-application-wizard-main.ts
Normal file
109
web/src/admin/applications/wizard/ak-application-wizard-main.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import "@goauthentik/components/ak-wizard/ak-wizard-steps.js";
|
||||
import { WizardUpdateEvent } from "@goauthentik/components/ak-wizard/events";
|
||||
import { AKElement } from "@goauthentik/elements/Base.js";
|
||||
|
||||
import { ContextProvider } from "@lit/context";
|
||||
import { html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
|
||||
import { ProvidersApi, ProxyMode } from "@goauthentik/api";
|
||||
|
||||
import { applicationWizardProvidersContext } from "./ContextIdentity";
|
||||
import { providerTypeRenderers } from "./steps/ProviderChoices.js";
|
||||
import "./steps/ak-application-wizard-application-step.js";
|
||||
import "./steps/ak-application-wizard-bindings-step.js";
|
||||
import "./steps/ak-application-wizard-edit-binding-step.js";
|
||||
import "./steps/ak-application-wizard-provider-choice-step.js";
|
||||
import "./steps/ak-application-wizard-provider-step.js";
|
||||
import "./steps/ak-application-wizard-submit-step.js";
|
||||
import { type ApplicationWizardState, type ApplicationWizardStateUpdate } from "./types";
|
||||
|
||||
const freshWizardState = (): ApplicationWizardState => ({
|
||||
providerModel: "",
|
||||
currentBinding: -1,
|
||||
app: {},
|
||||
provider: {},
|
||||
proxyMode: ProxyMode.Proxy,
|
||||
bindings: [],
|
||||
errors: {},
|
||||
});
|
||||
|
||||
@customElement("ak-application-wizard-main")
|
||||
export class AkApplicationWizardMain extends AKElement {
|
||||
@state()
|
||||
wizard: ApplicationWizardState = freshWizardState();
|
||||
|
||||
wizardProviderProvider = new ContextProvider(this, {
|
||||
context: applicationWizardProvidersContext,
|
||||
initialValue: [],
|
||||
});
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.addEventListener(WizardUpdateEvent.eventName, this.handleUpdate);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList().then((providerTypes) => {
|
||||
const wizardReadyProviders = Object.keys(providerTypeRenderers);
|
||||
this.wizardProviderProvider.setValue(
|
||||
providerTypes
|
||||
.filter((providerType) => wizardReadyProviders.includes(providerType.modelName))
|
||||
.map((providerType) => ({
|
||||
...providerType,
|
||||
renderer: providerTypeRenderers[providerType.modelName].render,
|
||||
}))
|
||||
.sort(
|
||||
(a, b) =>
|
||||
providerTypeRenderers[a.modelName].order -
|
||||
providerTypeRenderers[b.modelName].order,
|
||||
)
|
||||
.reverse(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// This is the actual top of the Wizard; so this is where we accept the update information and
|
||||
// incorporate it into the wizard.
|
||||
handleUpdate(ev: WizardUpdateEvent<ApplicationWizardStateUpdate>) {
|
||||
ev.stopPropagation();
|
||||
const update = ev.content;
|
||||
if (update !== undefined) {
|
||||
this.wizard = {
|
||||
...this.wizard,
|
||||
...update,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<ak-wizard-steps>
|
||||
<ak-application-wizard-application-step
|
||||
slot="application"
|
||||
.wizard=${this.wizard}
|
||||
></ak-application-wizard-application-step>
|
||||
<ak-application-wizard-provider-choice-step
|
||||
slot="provider-choice"
|
||||
.wizard=${this.wizard}
|
||||
></ak-application-wizard-provider-choice-step>
|
||||
<ak-application-wizard-provider-step
|
||||
slot="provider"
|
||||
.wizard=${this.wizard}
|
||||
></ak-application-wizard-provider-step>
|
||||
<ak-application-wizard-bindings-step
|
||||
slot="bindings"
|
||||
.wizard=${this.wizard}
|
||||
></ak-application-wizard-bindings-step>
|
||||
<ak-application-wizard-edit-binding-step
|
||||
slot="edit-binding"
|
||||
.wizard=${this.wizard}
|
||||
></ak-application-wizard-edit-binding-step>
|
||||
<ak-application-wizard-submit-step
|
||||
slot="submit"
|
||||
.wizard=${this.wizard}
|
||||
></ak-application-wizard-submit-step>
|
||||
</ak-wizard-steps>`;
|
||||
}
|
||||
}
|
@ -1,117 +1,32 @@
|
||||
import { AkWizard } from "@goauthentik/components/ak-wizard-main/AkWizard";
|
||||
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||
import { WizardCloseEvent } from "@goauthentik/components/ak-wizard/events.js";
|
||||
import { ModalButton } from "@goauthentik/elements/buttons/ModalButton";
|
||||
import { bound } from "@goauthentik/elements/decorators/bound.js";
|
||||
|
||||
import { ContextProvider } from "@lit/context";
|
||||
import { msg } from "@lit/localize";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
import { applicationWizardContext } from "./ContextIdentity";
|
||||
import { newSteps } from "./steps";
|
||||
import {
|
||||
ApplicationStep,
|
||||
ApplicationWizardState,
|
||||
ApplicationWizardStateUpdate,
|
||||
OneOfProvider,
|
||||
} from "./types";
|
||||
|
||||
const freshWizardState = (): ApplicationWizardState => ({
|
||||
providerModel: "",
|
||||
app: {},
|
||||
provider: {},
|
||||
errors: {},
|
||||
});
|
||||
import "./ak-application-wizard-main.js";
|
||||
|
||||
@customElement("ak-application-wizard")
|
||||
export class ApplicationWizard extends CustomListenerElement(
|
||||
AkWizard<ApplicationWizardStateUpdate, ApplicationStep>,
|
||||
) {
|
||||
export class AkApplicationWizard extends ModalButton {
|
||||
constructor() {
|
||||
super(msg("Create With Wizard"), msg("New application"), msg("Create a new application"));
|
||||
this.steps = newSteps();
|
||||
super();
|
||||
this.addEventListener(WizardCloseEvent.eventName, this.onCloseEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* We're going to be managing the content of the forms by percolating all of the data up to this
|
||||
* class, which will ultimately transmit all of it to the server as a transaction. The
|
||||
* WizardFramework doesn't know anything about the nature of the data itself; it just forwards
|
||||
* valid updates to us. So here we maintain a state object *and* update it so all child
|
||||
* components can access the wizard state.
|
||||
*
|
||||
*/
|
||||
@state()
|
||||
wizardState: ApplicationWizardState = freshWizardState();
|
||||
|
||||
wizardStateProvider = new ContextProvider(this, {
|
||||
context: applicationWizardContext,
|
||||
initialValue: this.wizardState,
|
||||
});
|
||||
|
||||
/**
|
||||
* One of our steps has multiple display variants, one for each type of service provider. We
|
||||
* want to *preserve* a customer's decisions about different providers; never make someone "go
|
||||
* back and type it all back in," even if it's probably rare that someone will chose one
|
||||
* provider, realize it's the wrong one, and go back to chose a different one, *and then go
|
||||
* back*. Nonetheless, strive to *never* lose customer input.
|
||||
*
|
||||
*/
|
||||
providerCache: Map<string, OneOfProvider> = new Map();
|
||||
|
||||
// And this is where all the special cases go...
|
||||
handleUpdate(detail: ApplicationWizardStateUpdate) {
|
||||
if (detail.status === "submitted") {
|
||||
this.step.valid = true;
|
||||
this.requestUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
this.step.valid = this.step.valid || detail.status === "valid";
|
||||
const update = detail.update;
|
||||
|
||||
if (!update) {
|
||||
return;
|
||||
}
|
||||
|
||||
// When the providerModel enum changes, retrieve the customer's prior work for *this* wizard
|
||||
// session (and only this wizard session) or provide an empty model with a default provider
|
||||
// name.
|
||||
if (update.providerModel && update.providerModel !== this.wizardState.providerModel) {
|
||||
const requestedProvider = this.providerCache.get(update.providerModel) ?? {
|
||||
name: `Provider for ${this.wizardState.app.name}`,
|
||||
};
|
||||
if (this.wizardState.providerModel) {
|
||||
this.providerCache.set(this.wizardState.providerModel, this.wizardState.provider);
|
||||
}
|
||||
update.provider = requestedProvider;
|
||||
}
|
||||
|
||||
this.wizardState = update as ApplicationWizardState;
|
||||
this.wizardStateProvider.setValue(this.wizardState);
|
||||
this.requestUpdate();
|
||||
@bound
|
||||
onCloseEvent(ev: WizardCloseEvent) {
|
||||
ev.stopPropagation();
|
||||
this.open = false;
|
||||
}
|
||||
|
||||
close() {
|
||||
this.steps = newSteps();
|
||||
this.currentStep = 0;
|
||||
this.wizardState = freshWizardState();
|
||||
this.providerCache = new Map();
|
||||
this.wizardStateProvider.setValue(this.wizardState);
|
||||
this.frame.value!.open = false;
|
||||
}
|
||||
|
||||
handleNav(stepId: number | undefined) {
|
||||
if (stepId === undefined || this.steps[stepId] === undefined) {
|
||||
throw new Error(`Attempt to navigate to undefined step: ${stepId}`);
|
||||
}
|
||||
if (stepId > this.currentStep && !this.step.valid) {
|
||||
return;
|
||||
}
|
||||
this.currentStep = stepId;
|
||||
this.requestUpdate();
|
||||
renderModalInner() {
|
||||
return html` <ak-application-wizard-main> </ak-application-wizard-main>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-application-wizard": ApplicationWizard;
|
||||
"ak-application-wizard": AkApplicationWizard;
|
||||
}
|
||||
}
|
||||
|
@ -1,103 +0,0 @@
|
||||
import { policyOptions } from "@goauthentik/admin/applications/ApplicationForm";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/components/ak-radio-input";
|
||||
import "@goauthentik/components/ak-slug-input";
|
||||
import "@goauthentik/components/ak-switch-input";
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import BasePanel from "../BasePanel";
|
||||
|
||||
@customElement("ak-application-wizard-application-details")
|
||||
export class ApplicationWizardApplicationDetails extends BasePanel {
|
||||
handleChange(_ev: Event) {
|
||||
const formValues = this.formValues;
|
||||
if (!formValues) {
|
||||
throw new Error("No application values on form?");
|
||||
}
|
||||
this.dispatchWizardUpdate({
|
||||
update: {
|
||||
...this.wizard,
|
||||
app: formValues,
|
||||
},
|
||||
status: this.valid ? "valid" : "invalid",
|
||||
});
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html` <form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
|
||||
<ak-text-input
|
||||
name="name"
|
||||
value=${ifDefined(this.wizard.app?.name)}
|
||||
label=${msg("Name")}
|
||||
required
|
||||
help=${msg("Application's display Name.")}
|
||||
id="ak-application-wizard-details-name"
|
||||
.errorMessages=${this.wizard.errors.app?.name ?? []}
|
||||
></ak-text-input>
|
||||
<ak-slug-input
|
||||
name="slug"
|
||||
value=${ifDefined(this.wizard.app?.slug)}
|
||||
label=${msg("Slug")}
|
||||
source="#ak-application-wizard-details-name"
|
||||
required
|
||||
help=${msg("Internal application name used in URLs.")}
|
||||
.errorMessages=${this.wizard.errors.app?.slug ?? []}
|
||||
></ak-slug-input>
|
||||
<ak-text-input
|
||||
name="group"
|
||||
value=${ifDefined(this.wizard.app?.group)}
|
||||
label=${msg("Group")}
|
||||
.errorMessages=${this.wizard.errors.app?.group ?? []}
|
||||
help=${msg(
|
||||
"Optionally enter a group name. Applications with identical groups are shown grouped together.",
|
||||
)}
|
||||
></ak-text-input>
|
||||
<ak-radio-input
|
||||
label=${msg("Policy engine mode")}
|
||||
required
|
||||
name="policyEngineMode"
|
||||
.options=${policyOptions}
|
||||
.value=${this.wizard.app?.policyEngineMode}
|
||||
.errorMessages=${this.wizard.errors.app?.policyEngineMode ?? []}
|
||||
></ak-radio-input>
|
||||
<ak-form-group aria-label=${msg("UI Settings")}>
|
||||
<span slot="header"> ${msg("UI Settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-text-input
|
||||
name="metaLaunchUrl"
|
||||
label=${msg("Launch URL")}
|
||||
value=${ifDefined(this.wizard.app?.metaLaunchUrl)}
|
||||
help=${msg(
|
||||
"If left empty, authentik will try to extract the launch URL based on the selected provider.",
|
||||
)}
|
||||
.errorMessages=${this.wizard.errors.app?.metaLaunchUrl ?? []}
|
||||
></ak-text-input>
|
||||
<ak-switch-input
|
||||
name="openInNewTab"
|
||||
?checked=${first(this.wizard.app?.openInNewTab, false)}
|
||||
label=${msg("Open in new tab")}
|
||||
help=${msg(
|
||||
"If checked, the launch URL will open in a new browser tab or window from the user's application library.",
|
||||
)}
|
||||
>
|
||||
</ak-switch-input>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
</form>`;
|
||||
}
|
||||
}
|
||||
|
||||
export default ApplicationWizardApplicationDetails;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-application-wizard-application-details": ApplicationWizardApplicationDetails;
|
||||
}
|
||||
}
|
@ -1,176 +0,0 @@
|
||||
import "@goauthentik/admin/common/ak-license-notice";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
|
||||
import type { ProviderModelEnum as ProviderModelEnumType, TypeCreate } from "@goauthentik/api";
|
||||
import { ProviderModelEnum, ProxyMode } from "@goauthentik/api";
|
||||
import type {
|
||||
LDAPProviderRequest,
|
||||
ModelRequest,
|
||||
OAuth2ProviderRequest,
|
||||
ProxyProviderRequest,
|
||||
RACProviderRequest,
|
||||
RadiusProviderRequest,
|
||||
SAMLProviderRequest,
|
||||
SCIMProviderRequest,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import { OneOfProvider } from "../types";
|
||||
|
||||
type ProviderRenderer = () => TemplateResult;
|
||||
|
||||
type ModelConverter = (provider: OneOfProvider) => ModelRequest;
|
||||
|
||||
type ProviderNoteProvider = () => TemplateResult | undefined;
|
||||
type ProviderNote = ProviderNoteProvider | undefined;
|
||||
|
||||
export type LocalTypeCreate = TypeCreate & {
|
||||
formName: string;
|
||||
modelName: ProviderModelEnumType;
|
||||
converter: ModelConverter;
|
||||
note?: ProviderNote;
|
||||
renderer: ProviderRenderer;
|
||||
};
|
||||
|
||||
export const providerModelsList: LocalTypeCreate[] = [
|
||||
{
|
||||
formName: "oauth2provider",
|
||||
name: msg("OAuth2/OIDC (Open Authorization/OpenID Connect)"),
|
||||
description: msg("Modern applications, APIs and Single-page applications."),
|
||||
renderer: () =>
|
||||
html`<ak-application-wizard-authentication-by-oauth></ak-application-wizard-authentication-by-oauth>`,
|
||||
modelName: ProviderModelEnum.Oauth2Oauth2provider,
|
||||
converter: (provider: OneOfProvider) => ({
|
||||
providerModel: ProviderModelEnum.Oauth2Oauth2provider,
|
||||
...(provider as OAuth2ProviderRequest),
|
||||
}),
|
||||
component: "",
|
||||
iconUrl: "/static/authentik/sources/openidconnect.svg",
|
||||
},
|
||||
{
|
||||
formName: "ldapprovider",
|
||||
name: msg("LDAP (Lightweight Directory Access Protocol)"),
|
||||
description: msg(
|
||||
"Provide an LDAP interface for applications and users to authenticate against.",
|
||||
),
|
||||
renderer: () =>
|
||||
html`<ak-application-wizard-authentication-by-ldap></ak-application-wizard-authentication-by-ldap>`,
|
||||
modelName: ProviderModelEnum.LdapLdapprovider,
|
||||
converter: (provider: OneOfProvider) => ({
|
||||
providerModel: ProviderModelEnum.LdapLdapprovider,
|
||||
...(provider as LDAPProviderRequest),
|
||||
}),
|
||||
component: "",
|
||||
iconUrl: "/static/authentik/sources/ldap.png",
|
||||
},
|
||||
{
|
||||
formName: "proxyprovider-proxy",
|
||||
name: msg("Transparent Reverse Proxy"),
|
||||
description: msg("For transparent reverse proxies with required authentication"),
|
||||
renderer: () =>
|
||||
html`<ak-application-wizard-authentication-for-reverse-proxy></ak-application-wizard-authentication-for-reverse-proxy>`,
|
||||
modelName: ProviderModelEnum.ProxyProxyprovider,
|
||||
converter: (provider: OneOfProvider) => ({
|
||||
providerModel: ProviderModelEnum.ProxyProxyprovider,
|
||||
...(provider as ProxyProviderRequest),
|
||||
mode: ProxyMode.Proxy,
|
||||
}),
|
||||
component: "",
|
||||
iconUrl: "/static/authentik/sources/proxy.svg",
|
||||
},
|
||||
{
|
||||
formName: "proxyprovider-forwardsingle",
|
||||
name: msg("Forward Auth (Single Application)"),
|
||||
description: msg("For nginx's auth_request or traefik's forwardAuth"),
|
||||
renderer: () =>
|
||||
html`<ak-application-wizard-authentication-for-single-forward-proxy></ak-application-wizard-authentication-for-single-forward-proxy>`,
|
||||
modelName: ProviderModelEnum.ProxyProxyprovider,
|
||||
converter: (provider: OneOfProvider) => ({
|
||||
providerModel: ProviderModelEnum.ProxyProxyprovider,
|
||||
...(provider as ProxyProviderRequest),
|
||||
mode: ProxyMode.ForwardSingle,
|
||||
}),
|
||||
component: "",
|
||||
iconUrl: "/static/authentik/sources/proxy.svg",
|
||||
},
|
||||
{
|
||||
formName: "proxyprovider-forwarddomain",
|
||||
name: msg("Forward Auth (Domain Level)"),
|
||||
description: msg("For nginx's auth_request or traefik's forwardAuth per root domain"),
|
||||
renderer: () =>
|
||||
html`<ak-application-wizard-authentication-for-forward-proxy-domain></ak-application-wizard-authentication-for-forward-proxy-domain>`,
|
||||
modelName: ProviderModelEnum.ProxyProxyprovider,
|
||||
converter: (provider: OneOfProvider) => ({
|
||||
providerModel: ProviderModelEnum.ProxyProxyprovider,
|
||||
...(provider as ProxyProviderRequest),
|
||||
mode: ProxyMode.ForwardDomain,
|
||||
}),
|
||||
component: "",
|
||||
iconUrl: "/static/authentik/sources/proxy.svg",
|
||||
},
|
||||
{
|
||||
formName: "racprovider",
|
||||
name: msg("Remote Access Provider"),
|
||||
description: msg("Remotely access computers/servers via RDP/SSH/VNC"),
|
||||
renderer: () =>
|
||||
html`<ak-application-wizard-authentication-for-rac></ak-application-wizard-authentication-for-rac>`,
|
||||
modelName: ProviderModelEnum.RacRacprovider,
|
||||
converter: (provider: OneOfProvider) => ({
|
||||
providerModel: ProviderModelEnum.RacRacprovider,
|
||||
...(provider as RACProviderRequest),
|
||||
}),
|
||||
note: () => html`<ak-license-notice></ak-license-notice>`,
|
||||
requiresEnterprise: true,
|
||||
component: "",
|
||||
iconUrl: "/static/authentik/sources/rac.svg",
|
||||
},
|
||||
{
|
||||
formName: "samlprovider",
|
||||
name: msg("SAML (Security Assertion Markup Language)"),
|
||||
description: msg("Configure SAML provider manually"),
|
||||
renderer: () =>
|
||||
html`<ak-application-wizard-authentication-by-saml-configuration></ak-application-wizard-authentication-by-saml-configuration>`,
|
||||
modelName: ProviderModelEnum.SamlSamlprovider,
|
||||
converter: (provider: OneOfProvider) => ({
|
||||
providerModel: ProviderModelEnum.SamlSamlprovider,
|
||||
...(provider as SAMLProviderRequest),
|
||||
}),
|
||||
component: "",
|
||||
iconUrl: "/static/authentik/sources/saml.png",
|
||||
},
|
||||
{
|
||||
formName: "radiusprovider",
|
||||
name: msg("RADIUS (Remote Authentication Dial-In User Service)"),
|
||||
description: msg("Configure RADIUS provider manually"),
|
||||
renderer: () =>
|
||||
html`<ak-application-wizard-authentication-by-radius></ak-application-wizard-authentication-by-radius>`,
|
||||
modelName: ProviderModelEnum.RadiusRadiusprovider,
|
||||
converter: (provider: OneOfProvider) => ({
|
||||
providerModel: ProviderModelEnum.RadiusRadiusprovider,
|
||||
...(provider as RadiusProviderRequest),
|
||||
}),
|
||||
component: "",
|
||||
iconUrl: "/static/authentik/sources/radius.svg",
|
||||
},
|
||||
{
|
||||
formName: "scimprovider",
|
||||
name: msg("SCIM (System for Cross-domain Identity Management)"),
|
||||
description: msg("Configure SCIM provider manually"),
|
||||
renderer: () =>
|
||||
html`<ak-application-wizard-authentication-by-scim></ak-application-wizard-authentication-by-scim>`,
|
||||
modelName: ProviderModelEnum.ScimScimprovider,
|
||||
converter: (provider: OneOfProvider) => ({
|
||||
providerModel: ProviderModelEnum.ScimScimprovider,
|
||||
...(provider as SCIMProviderRequest),
|
||||
}),
|
||||
component: "",
|
||||
iconUrl: "/static/authentik/sources/scim.png",
|
||||
},
|
||||
];
|
||||
|
||||
export const providerRendererList = new Map<string, ProviderRenderer>(
|
||||
providerModelsList.map((tc) => [tc.formName, tc.renderer]),
|
||||
);
|
||||
|
||||
export default providerModelsList;
|
@ -1,62 +0,0 @@
|
||||
import "@goauthentik/components/ak-radio-input";
|
||||
import "@goauthentik/components/ak-switch-input";
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import "@goauthentik/elements/wizard/TypeCreateWizardPage";
|
||||
import { TypeCreateWizardPageLayouts } from "@goauthentik/elements/wizard/TypeCreateWizardPage";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
|
||||
import { html } from "lit";
|
||||
|
||||
import BasePanel from "../BasePanel";
|
||||
import type { LocalTypeCreate } from "./ak-application-wizard-authentication-method-choice.choices";
|
||||
import providerModelsList from "./ak-application-wizard-authentication-method-choice.choices";
|
||||
|
||||
@customElement("ak-application-wizard-authentication-method-choice")
|
||||
export class ApplicationWizardAuthenticationMethodChoice extends WithLicenseSummary(BasePanel) {
|
||||
render() {
|
||||
const selectedTypes = providerModelsList.filter(
|
||||
(t) => t.formName === this.wizard.providerModel,
|
||||
);
|
||||
|
||||
// As a hack, the Application wizard has separate provider paths for our three types of
|
||||
// proxy providers. This patch swaps the form we want to be directed to on page 3 from the
|
||||
// modelName to the formName, so we get the right one. This information isn't modified
|
||||
// or forwarded, so the proxy-plus-subtype is correctly mapped on submission.
|
||||
const typesForWizard = providerModelsList.map((provider) => ({
|
||||
...provider,
|
||||
modelName: provider.formName,
|
||||
}));
|
||||
|
||||
return providerModelsList.length > 0
|
||||
? html`<form class="pf-c-form pf-m-horizontal">
|
||||
<ak-wizard-page-type-create
|
||||
.types=${typesForWizard}
|
||||
layout=${TypeCreateWizardPageLayouts.grid}
|
||||
.selectedType=${selectedTypes.length > 0 ? selectedTypes[0] : undefined}
|
||||
@select=${(ev: CustomEvent<LocalTypeCreate>) => {
|
||||
this.dispatchWizardUpdate({
|
||||
update: {
|
||||
...this.wizard,
|
||||
providerModel: ev.detail.formName,
|
||||
errors: {},
|
||||
},
|
||||
status: this.valid ? "valid" : "invalid",
|
||||
});
|
||||
}}
|
||||
></ak-wizard-page-type-create>
|
||||
</form>`
|
||||
: html`<ak-empty-state loading header=${msg("Loading")}></ak-empty-state>`;
|
||||
}
|
||||
}
|
||||
|
||||
export default ApplicationWizardAuthenticationMethodChoice;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-application-wizard-authentication-method-choice": ApplicationWizardAuthenticationMethodChoice;
|
||||
}
|
||||
}
|
@ -1,237 +0,0 @@
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||
import { parseAPIError } from "@goauthentik/common/errors";
|
||||
import "@goauthentik/components/ak-radio-input";
|
||||
import "@goauthentik/components/ak-switch-input";
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { customElement, state } from "@lit/reactive-element/decorators.js";
|
||||
import { PropertyValues, TemplateResult, css, html, nothing } from "lit";
|
||||
import { classMap } from "lit/directives/class-map.js";
|
||||
|
||||
import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css";
|
||||
import PFProgressStepper from "@patternfly/patternfly/components/ProgressStepper/progress-stepper.css";
|
||||
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
||||
import PFBullseye from "@patternfly/patternfly/layouts/Bullseye/bullseye.css";
|
||||
|
||||
import {
|
||||
type ApplicationRequest,
|
||||
CoreApi,
|
||||
type ModelRequest,
|
||||
type TransactionApplicationRequest,
|
||||
type TransactionApplicationResponse,
|
||||
ValidationError,
|
||||
instanceOfValidationError,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import BasePanel from "../BasePanel";
|
||||
import providerModelsList from "../auth-method-choice/ak-application-wizard-authentication-method-choice.choices";
|
||||
|
||||
function cleanApplication(app: Partial<ApplicationRequest>): ApplicationRequest {
|
||||
return {
|
||||
name: "",
|
||||
slug: "",
|
||||
...app,
|
||||
};
|
||||
}
|
||||
|
||||
type ProviderModelType = Exclude<ModelRequest["providerModel"], "11184809">;
|
||||
|
||||
type State = {
|
||||
state: "idle" | "running" | "error" | "success";
|
||||
label: string | TemplateResult;
|
||||
icon: string[];
|
||||
};
|
||||
|
||||
const idleState: State = {
|
||||
state: "idle",
|
||||
label: "",
|
||||
icon: ["fa-cogs", "pf-m-pending"],
|
||||
};
|
||||
|
||||
const runningState: State = {
|
||||
state: "running",
|
||||
label: msg("Saving Application..."),
|
||||
icon: ["fa-cogs", "pf-m-info"],
|
||||
};
|
||||
const errorState: State = {
|
||||
state: "error",
|
||||
label: msg("authentik was unable to save this application:"),
|
||||
icon: ["fa-times-circle", "pf-m-danger"],
|
||||
};
|
||||
|
||||
const successState: State = {
|
||||
state: "success",
|
||||
label: msg("Your application has been saved"),
|
||||
icon: ["fa-check-circle", "pf-m-success"],
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const isValidationError = (v: any): v is ValidationError => instanceOfValidationError(v);
|
||||
|
||||
@customElement("ak-application-wizard-commit-application")
|
||||
export class ApplicationWizardCommitApplication extends BasePanel {
|
||||
static get styles() {
|
||||
return [
|
||||
...super.styles,
|
||||
PFBullseye,
|
||||
PFEmptyState,
|
||||
PFTitle,
|
||||
PFProgressStepper,
|
||||
css`
|
||||
.pf-c-title {
|
||||
padding-bottom: var(--pf-global--spacer--md);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@state()
|
||||
commitState: State = idleState;
|
||||
|
||||
@state()
|
||||
errors?: ValidationError;
|
||||
|
||||
response?: TransactionApplicationResponse;
|
||||
|
||||
willUpdate(_changedProperties: PropertyValues<this>) {
|
||||
if (this.commitState === idleState) {
|
||||
this.response = undefined;
|
||||
this.commitState = runningState;
|
||||
const providerModel = providerModelsList.find(
|
||||
({ formName }) => formName === this.wizard.providerModel,
|
||||
);
|
||||
if (!providerModel) {
|
||||
throw new Error(
|
||||
`Could not determine provider model from user request: ${JSON.stringify(this.wizard, null, 2)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const request: TransactionApplicationRequest = {
|
||||
providerModel: providerModel.modelName as ProviderModelType,
|
||||
app: cleanApplication(this.wizard.app),
|
||||
provider: providerModel.converter(this.wizard.provider),
|
||||
};
|
||||
|
||||
this.send(request);
|
||||
}
|
||||
}
|
||||
|
||||
async send(
|
||||
data: TransactionApplicationRequest,
|
||||
): Promise<TransactionApplicationResponse | void> {
|
||||
this.errors = undefined;
|
||||
new CoreApi(DEFAULT_CONFIG)
|
||||
.coreTransactionalApplicationsUpdate({
|
||||
transactionApplicationRequest: data,
|
||||
})
|
||||
.then((response: TransactionApplicationResponse) => {
|
||||
this.response = response;
|
||||
this.dispatchCustomEvent(EVENT_REFRESH);
|
||||
this.dispatchWizardUpdate({ status: "submitted" });
|
||||
this.commitState = successState;
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
.catch(async (resolution: any) => {
|
||||
const errors = await parseAPIError(resolution);
|
||||
|
||||
// THIS is a really gross special case; if the user is duplicating the name of an
|
||||
// existing provider, the error appears on the `app` (!) error object. We have to
|
||||
// move that to the `provider.name` error field so it shows up in the right place.
|
||||
if (isValidationError(errors) && Array.isArray(errors?.app?.provider)) {
|
||||
const providerError = errors.app.provider;
|
||||
errors.provider = errors.provider ?? {};
|
||||
errors.provider.name = providerError;
|
||||
delete errors.app.provider;
|
||||
if (Object.keys(errors.app).length === 0) {
|
||||
delete errors.app;
|
||||
}
|
||||
}
|
||||
|
||||
this.errors = errors;
|
||||
this.dispatchWizardUpdate({
|
||||
update: {
|
||||
...this.wizard,
|
||||
errors: this.errors,
|
||||
},
|
||||
status: "failed",
|
||||
});
|
||||
this.commitState = errorState;
|
||||
});
|
||||
}
|
||||
|
||||
renderErrors(errors?: ValidationError) {
|
||||
if (!errors) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const navTo = (step: number) => () =>
|
||||
this.dispatchCustomEvent("ak-wizard-nav", {
|
||||
command: "goto",
|
||||
step,
|
||||
});
|
||||
|
||||
if (errors.app) {
|
||||
return html`<p>${msg("There was an error in the application.")}</p>
|
||||
<p><a @click=${navTo(0)}>${msg("Review the application.")}</a></p>`;
|
||||
}
|
||||
if (errors.provider) {
|
||||
return html`<p>${msg("There was an error in the provider.")}</p>
|
||||
<p><a @click=${navTo(2)}>${msg("Review the provider.")}</a></p>`;
|
||||
}
|
||||
if (errors.detail) {
|
||||
return html`<p>${msg("There was an error")}: ${errors.detail}</p>`;
|
||||
}
|
||||
if ((errors?.nonFieldErrors ?? []).length > 0) {
|
||||
return html`<p>$(msg("There was an error")}:</p>
|
||||
<ul>
|
||||
${(errors.nonFieldErrors ?? []).map((e: string) => html`<li>${e}</li>`)}
|
||||
</ul>`;
|
||||
}
|
||||
return html`<p>
|
||||
${msg(
|
||||
"There was an error creating the application, but no error message was sent. Please review the server logs.",
|
||||
)}
|
||||
</p>`;
|
||||
}
|
||||
|
||||
render() {
|
||||
const icon = classMap(
|
||||
this.commitState.icon.reduce((acc, icon) => ({ ...acc, [icon]: true }), {}),
|
||||
);
|
||||
|
||||
return html`
|
||||
<div>
|
||||
<div class="pf-l-bullseye">
|
||||
<div class="pf-c-empty-state pf-m-lg">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i
|
||||
class="fas fa- ${icon} pf-c-empty-state__icon"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<h1
|
||||
data-commit-state=${this.commitState.state}
|
||||
class="pf-c-title pf-m-lg"
|
||||
>
|
||||
${this.commitState.label}
|
||||
</h1>
|
||||
${this.renderErrors(this.errors)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export default ApplicationWizardCommitApplication;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-application-wizard-commit-application": ApplicationWizardCommitApplication;
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import BasePanel from "../BasePanel";
|
||||
|
||||
export class ApplicationWizardProviderPageBase extends BasePanel {
|
||||
handleChange(_ev: InputEvent) {
|
||||
const formValues = this.formValues;
|
||||
if (!formValues) {
|
||||
throw new Error("No provider values on form?");
|
||||
}
|
||||
this.dispatchWizardUpdate({
|
||||
update: {
|
||||
...this.wizard,
|
||||
provider: formValues,
|
||||
},
|
||||
status: this.valid ? "valid" : "invalid",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default ApplicationWizardProviderPageBase;
|
@ -1,34 +0,0 @@
|
||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
|
||||
|
||||
import BasePanel from "../BasePanel";
|
||||
import { providerRendererList } from "../auth-method-choice/ak-application-wizard-authentication-method-choice.choices";
|
||||
import "./ldap/ak-application-wizard-authentication-by-ldap";
|
||||
import "./oauth/ak-application-wizard-authentication-by-oauth";
|
||||
import "./proxy/ak-application-wizard-authentication-for-forward-domain-proxy";
|
||||
import "./proxy/ak-application-wizard-authentication-for-reverse-proxy";
|
||||
import "./proxy/ak-application-wizard-authentication-for-single-forward-proxy";
|
||||
import "./rac/ak-application-wizard-authentication-for-rac";
|
||||
import "./radius/ak-application-wizard-authentication-by-radius";
|
||||
import "./saml/ak-application-wizard-authentication-by-saml-configuration";
|
||||
import "./scim/ak-application-wizard-authentication-by-scim";
|
||||
|
||||
@customElement("ak-application-wizard-authentication-method")
|
||||
export class ApplicationWizardApplicationDetails extends BasePanel {
|
||||
render() {
|
||||
const handler = providerRendererList.get(this.wizard.providerModel);
|
||||
if (!handler) {
|
||||
throw new Error(
|
||||
"Unrecognized authentication method in ak-application-wizard-authentication-method",
|
||||
);
|
||||
}
|
||||
return handler();
|
||||
}
|
||||
}
|
||||
|
||||
export default ApplicationWizardApplicationDetails;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-application-wizard-authentication-method": ApplicationWizardApplicationDetails;
|
||||
}
|
||||
}
|
@ -1,173 +0,0 @@
|
||||
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
|
||||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/components/ak-number-input";
|
||||
import "@goauthentik/components/ak-radio-input";
|
||||
import "@goauthentik/components/ak-switch-input";
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
|
||||
import { html, nothing } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import { FlowsInstancesListDesignationEnum } from "@goauthentik/api";
|
||||
import type { LDAPProvider } from "@goauthentik/api";
|
||||
|
||||
import BaseProviderPanel from "../BaseProviderPanel";
|
||||
import {
|
||||
bindModeOptions,
|
||||
cryptoCertificateHelp,
|
||||
gidStartNumberHelp,
|
||||
mfaSupportHelp,
|
||||
searchModeOptions,
|
||||
tlsServerNameHelp,
|
||||
uidStartNumberHelp,
|
||||
} from "./LDAPOptionsAndHelp";
|
||||
|
||||
@customElement("ak-application-wizard-authentication-by-ldap")
|
||||
export class ApplicationWizardApplicationDetails extends WithBrandConfig(BaseProviderPanel) {
|
||||
render() {
|
||||
const provider = this.wizard.provider as LDAPProvider | undefined;
|
||||
const errors = this.wizard.errors.provider;
|
||||
|
||||
return html` <ak-wizard-title>${msg("Configure LDAP Provider")}</ak-wizard-title>
|
||||
<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
|
||||
<ak-text-input
|
||||
name="name"
|
||||
value=${ifDefined(provider?.name)}
|
||||
label=${msg("Name")}
|
||||
.errorMessages=${errors?.name ?? []}
|
||||
required
|
||||
help=${msg("Method's display Name.")}
|
||||
></ak-text-input>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Bind flow")}
|
||||
?required=${true}
|
||||
name="authorizationFlow"
|
||||
.errorMessages=${errors?.authorizationFlow ?? []}
|
||||
>
|
||||
<ak-branded-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${provider?.authorizationFlow}
|
||||
.brandFlow=${this.brand.flowAuthentication}
|
||||
required
|
||||
></ak-branded-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used for users to authenticate.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Unbind flow")}
|
||||
name="invalidationFlow"
|
||||
required
|
||||
>
|
||||
<ak-branded-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
|
||||
.currentFlow=${provider?.invalidationFlow}
|
||||
.brandFlow=${this.brand.flowInvalidation}
|
||||
defaultFlowSlug="default-invalidation-flow"
|
||||
required
|
||||
></ak-branded-flow-search>
|
||||
<p class="pf-c-form__helper-text">${msg("Flow used for unbinding users.")}</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-radio-input
|
||||
label=${msg("Bind mode")}
|
||||
name="bindMode"
|
||||
.options=${bindModeOptions}
|
||||
.value=${provider?.bindMode}
|
||||
help=${msg("Configure how the outpost authenticates requests.")}
|
||||
>
|
||||
</ak-radio-input>
|
||||
|
||||
<ak-radio-input
|
||||
label=${msg("Search mode")}
|
||||
name="searchMode"
|
||||
.options=${searchModeOptions}
|
||||
.value=${provider?.searchMode}
|
||||
help=${msg(
|
||||
"Configure how the outpost queries the core authentik server's users.",
|
||||
)}
|
||||
>
|
||||
</ak-radio-input>
|
||||
|
||||
<ak-switch-input
|
||||
name="mfaSupport"
|
||||
label=${msg("Code-based MFA Support")}
|
||||
?checked=${provider?.mfaSupport ?? true}
|
||||
help=${mfaSupportHelp}
|
||||
>
|
||||
</ak-switch-input>
|
||||
|
||||
<ak-form-group .expanded=${true}>
|
||||
<span slot="header"> ${msg("Protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-text-input
|
||||
name="baseDn"
|
||||
label=${msg("Base DN")}
|
||||
required
|
||||
value="${first(provider?.baseDn, "DC=ldap,DC=goauthentik,DC=io")}"
|
||||
.errorMessages=${errors?.baseDn ?? []}
|
||||
help=${msg(
|
||||
"LDAP DN under which bind requests and search requests can be made.",
|
||||
)}
|
||||
>
|
||||
</ak-text-input>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Certificate")}
|
||||
name="certificate"
|
||||
.errorMessages=${errors?.certificate ?? []}
|
||||
>
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${ifDefined(provider?.certificate ?? nothing)}
|
||||
name="certificate"
|
||||
>
|
||||
</ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">${cryptoCertificateHelp}</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-text-input
|
||||
label=${msg("TLS Server name")}
|
||||
name="tlsServerName"
|
||||
value="${first(provider?.tlsServerName, "")}"
|
||||
.errorMessages=${errors?.tlsServerName ?? []}
|
||||
help=${tlsServerNameHelp}
|
||||
></ak-text-input>
|
||||
|
||||
<ak-number-input
|
||||
label=${msg("UID start number")}
|
||||
required
|
||||
name="uidStartNumber"
|
||||
value="${first(provider?.uidStartNumber, 2000)}"
|
||||
.errorMessages=${errors?.uidStartNumber ?? []}
|
||||
help=${uidStartNumberHelp}
|
||||
></ak-number-input>
|
||||
|
||||
<ak-number-input
|
||||
label=${msg("GID start number")}
|
||||
required
|
||||
name="gidStartNumber"
|
||||
value="${first(provider?.gidStartNumber, 4000)}"
|
||||
.errorMessages=${errors?.gidStartNumber ?? []}
|
||||
help=${gidStartNumberHelp}
|
||||
></ak-number-input>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
</form>`;
|
||||
}
|
||||
}
|
||||
|
||||
export default ApplicationWizardApplicationDetails;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-application-wizard-authentication-by-ldap": ApplicationWizardApplicationDetails;
|
||||
}
|
||||
}
|
@ -1,332 +0,0 @@
|
||||
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
|
||||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
|
||||
import {
|
||||
clientTypeOptions,
|
||||
issuerModeOptions,
|
||||
redirectUriHelp,
|
||||
subjectModeOptions,
|
||||
} from "@goauthentik/admin/providers/oauth2/OAuth2ProviderForm";
|
||||
import {
|
||||
propertyMappingsProvider,
|
||||
propertyMappingsSelector,
|
||||
} from "@goauthentik/admin/providers/oauth2/OAuth2ProviderFormHelpers.js";
|
||||
import {
|
||||
IRedirectURIInput,
|
||||
akOAuthRedirectURIInput,
|
||||
} from "@goauthentik/admin/providers/oauth2/OAuth2ProviderRedirectURI";
|
||||
import {
|
||||
oauth2SourcesProvider,
|
||||
oauth2SourcesSelector,
|
||||
} from "@goauthentik/admin/providers/oauth2/OAuth2Sources.js";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/components/ak-number-input";
|
||||
import "@goauthentik/components/ak-radio-input";
|
||||
import "@goauthentik/components/ak-switch-input";
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
import "@goauthentik/components/ak-textarea-input";
|
||||
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { customElement, state } from "@lit/reactive-element/decorators.js";
|
||||
import { html, nothing } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import {
|
||||
ClientTypeEnum,
|
||||
FlowsInstancesListDesignationEnum,
|
||||
MatchingModeEnum,
|
||||
RedirectURI,
|
||||
SourcesApi,
|
||||
} from "@goauthentik/api";
|
||||
import { type OAuth2Provider, type PaginatedOAuthSourceList } from "@goauthentik/api";
|
||||
|
||||
import BaseProviderPanel from "../BaseProviderPanel";
|
||||
|
||||
@customElement("ak-application-wizard-authentication-by-oauth")
|
||||
export class ApplicationWizardAuthenticationByOauth extends BaseProviderPanel {
|
||||
@state()
|
||||
showClientSecret = true;
|
||||
|
||||
@state()
|
||||
oauthSources?: PaginatedOAuthSourceList;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
new SourcesApi(DEFAULT_CONFIG)
|
||||
.sourcesOauthList({
|
||||
ordering: "name",
|
||||
hasJwks: true,
|
||||
})
|
||||
.then((oauthSources: PaginatedOAuthSourceList) => {
|
||||
this.oauthSources = oauthSources;
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const provider = this.wizard.provider as OAuth2Provider | undefined;
|
||||
const errors = this.wizard.errors.provider;
|
||||
|
||||
return html`<ak-wizard-title>${msg("Configure OAuth2/OpenId Provider")}</ak-wizard-title>
|
||||
<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
|
||||
<ak-text-input
|
||||
name="name"
|
||||
label=${msg("Name")}
|
||||
value=${ifDefined(provider?.name)}
|
||||
.errorMessages=${errors?.name ?? []}
|
||||
required
|
||||
></ak-text-input>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
name="authorizationFlow"
|
||||
label=${msg("Authorization flow")}
|
||||
.errorMessages=${errors?.authorizationFlow ?? []}
|
||||
?required=${true}
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authorization}
|
||||
.currentFlow=${provider?.authorizationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when authorizing this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-group .expanded=${true}>
|
||||
<span slot="header"> ${msg("Protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-radio-input
|
||||
name="clientType"
|
||||
label=${msg("Client type")}
|
||||
.value=${provider?.clientType}
|
||||
required
|
||||
@change=${(ev: CustomEvent<{ value: ClientTypeEnum }>) => {
|
||||
this.showClientSecret = ev.detail.value !== ClientTypeEnum.Public;
|
||||
}}
|
||||
.options=${clientTypeOptions}
|
||||
>
|
||||
</ak-radio-input>
|
||||
|
||||
<ak-text-input
|
||||
name="clientId"
|
||||
label=${msg("Client ID")}
|
||||
value=${provider?.clientId ?? randomString(40, ascii_letters + digits)}
|
||||
.errorMessages=${errors?.clientId ?? []}
|
||||
required
|
||||
>
|
||||
</ak-text-input>
|
||||
|
||||
<ak-text-input
|
||||
name="clientSecret"
|
||||
label=${msg("Client Secret")}
|
||||
value=${provider?.clientSecret ??
|
||||
randomString(128, ascii_letters + digits)}
|
||||
.errorMessages=${errors?.clientSecret ?? []}
|
||||
?hidden=${!this.showClientSecret}
|
||||
>
|
||||
</ak-text-input>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Redirect URIs/Origins")}
|
||||
required
|
||||
name="redirectUris"
|
||||
>
|
||||
<ak-array-input
|
||||
.items=${[]}
|
||||
.newItem=${() => ({
|
||||
matchingMode: MatchingModeEnum.Strict,
|
||||
url: "",
|
||||
})}
|
||||
.row=${(f?: RedirectURI) =>
|
||||
akOAuthRedirectURIInput({
|
||||
".redirectURI": f,
|
||||
"style": "width: 100%",
|
||||
"name": "oauth2-redirect-uri",
|
||||
} as unknown as IRedirectURIInput)}
|
||||
>
|
||||
</ak-array-input>
|
||||
${redirectUriHelp}
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Signing Key")}
|
||||
name="signingKey"
|
||||
.errorMessages=${errors?.signingKey ?? []}
|
||||
>
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${ifDefined(provider?.signingKey ?? nothing)}
|
||||
name="certificate"
|
||||
singleton
|
||||
>
|
||||
</ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Key used to sign the tokens.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced flow settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
name="authenticationFlow"
|
||||
label=${msg("Authentication flow")}
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${provider?.authenticationFlow}
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Flow used when a user access this provider and is not authenticated.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Invalidation flow")}
|
||||
name="invalidationFlow"
|
||||
required
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
|
||||
.currentFlow=${provider?.invalidationFlow}
|
||||
defaultFlowSlug="default-provider-invalidation-flow"
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when logging out of this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-text-input
|
||||
name="accessCodeValidity"
|
||||
label=${msg("Access code validity")}
|
||||
required
|
||||
value="${first(provider?.accessCodeValidity, "minutes=1")}"
|
||||
.errorMessages=${errors?.accessCodeValidity ?? []}
|
||||
.bighelp=${html`<p class="pf-c-form__helper-text">
|
||||
${msg("Configure how long access codes are valid for.")}
|
||||
</p>
|
||||
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
|
||||
>
|
||||
</ak-text-input>
|
||||
|
||||
<ak-text-input
|
||||
name="accessTokenValidity"
|
||||
label=${msg("Access Token validity")}
|
||||
value="${first(provider?.accessTokenValidity, "minutes=5")}"
|
||||
required
|
||||
.errorMessages=${errors?.accessTokenValidity ?? []}
|
||||
.bighelp=${html` <p class="pf-c-form__helper-text">
|
||||
${msg("Configure how long access tokens are valid for.")}
|
||||
</p>
|
||||
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
|
||||
>
|
||||
</ak-text-input>
|
||||
|
||||
<ak-text-input
|
||||
name="refreshTokenValidity"
|
||||
label=${msg("Refresh Token validity")}
|
||||
value="${first(provider?.refreshTokenValidity, "days=30")}"
|
||||
.errorMessages=${errors?.refreshTokenValidity ?? []}
|
||||
?required=${true}
|
||||
.bighelp=${html` <p class="pf-c-form__helper-text">
|
||||
${msg("Configure how long refresh tokens are valid for.")}
|
||||
</p>
|
||||
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
|
||||
>
|
||||
</ak-text-input>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Scopes")}
|
||||
name="propertyMappings"
|
||||
.errorMessages=${errors?.propertyMappings ?? []}
|
||||
>
|
||||
<ak-dual-select-dynamic-selected
|
||||
.provider=${propertyMappingsProvider}
|
||||
.selector=${propertyMappingsSelector(provider?.propertyMappings)}
|
||||
available-label=${msg("Available Scopes")}
|
||||
selected-label=${msg("Selected Scopes")}
|
||||
></ak-dual-select-dynamic-selected>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Select which scopes can be used by the client. The client still has to specify the scope to access the data.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-radio-input
|
||||
name="subMode"
|
||||
label=${msg("Subject mode")}
|
||||
required
|
||||
.options=${subjectModeOptions}
|
||||
.value=${provider?.subMode}
|
||||
help=${msg(
|
||||
"Configure what data should be used as unique User Identifier. For most cases, the default should be fine.",
|
||||
)}
|
||||
>
|
||||
</ak-radio-input>
|
||||
<ak-switch-input
|
||||
name="includeClaimsInIdToken"
|
||||
label=${msg("Include claims in id_token")}
|
||||
?checked=${first(provider?.includeClaimsInIdToken, true)}
|
||||
help=${msg(
|
||||
"Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint.",
|
||||
)}
|
||||
></ak-switch-input>
|
||||
<ak-radio-input
|
||||
name="issuerMode"
|
||||
label=${msg("Issuer mode")}
|
||||
required
|
||||
.options=${issuerModeOptions}
|
||||
.value=${provider?.issuerMode}
|
||||
help=${msg(
|
||||
"Configure how the issuer field of the ID Token should be filled.",
|
||||
)}
|
||||
>
|
||||
</ak-radio-input>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
|
||||
<ak-form-group>
|
||||
<span slot="header">${msg("Machine-to-Machine authentication settings")}</span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Trusted OIDC Sources")}
|
||||
name="jwksSources"
|
||||
.errorMessages=${errors?.jwksSources ?? []}
|
||||
>
|
||||
<ak-dual-select-dynamic-selected
|
||||
.provider=${oauth2SourcesProvider}
|
||||
.selector=${oauth2SourcesSelector(provider?.jwtFederationSources)}
|
||||
available-label=${msg("Available Sources")}
|
||||
selected-label=${msg("Selected Sources")}
|
||||
></ak-dual-select-dynamic-selected>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
</form>`;
|
||||
}
|
||||
}
|
||||
|
||||
export default ApplicationWizardAuthenticationByOauth;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-application-wizard-authentication-by-oauth": ApplicationWizardAuthenticationByOauth;
|
||||
}
|
||||
}
|
@ -1,269 +0,0 @@
|
||||
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
|
||||
import {
|
||||
oauth2SourcesProvider,
|
||||
oauth2SourcesSelector,
|
||||
} from "@goauthentik/admin/providers/oauth2/OAuth2Sources.js";
|
||||
import {
|
||||
propertyMappingsProvider,
|
||||
propertyMappingsSelector,
|
||||
} from "@goauthentik/admin/providers/proxy/ProxyProviderFormHelpers.js";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/components/ak-switch-input";
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
import "@goauthentik/components/ak-textarea-input";
|
||||
import "@goauthentik/components/ak-toggle-group";
|
||||
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { state } from "@lit/reactive-element/decorators.js";
|
||||
import { TemplateResult, html, nothing } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import {
|
||||
FlowsInstancesListDesignationEnum,
|
||||
PaginatedOAuthSourceList,
|
||||
PaginatedScopeMappingList,
|
||||
ProxyMode,
|
||||
ProxyProvider,
|
||||
SourcesApi,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import BaseProviderPanel from "../BaseProviderPanel";
|
||||
|
||||
type MaybeTemplateResult = TemplateResult | typeof nothing;
|
||||
|
||||
export class AkTypeProxyApplicationWizardPage extends BaseProviderPanel {
|
||||
constructor() {
|
||||
super();
|
||||
new SourcesApi(DEFAULT_CONFIG)
|
||||
.sourcesOauthList({
|
||||
ordering: "name",
|
||||
hasJwks: true,
|
||||
})
|
||||
.then((oauthSources: PaginatedOAuthSourceList) => {
|
||||
this.oauthSources = oauthSources;
|
||||
});
|
||||
}
|
||||
|
||||
propertyMappings?: PaginatedScopeMappingList;
|
||||
oauthSources?: PaginatedOAuthSourceList;
|
||||
|
||||
@state()
|
||||
showHttpBasic = true;
|
||||
|
||||
@state()
|
||||
mode: ProxyMode = ProxyMode.Proxy;
|
||||
|
||||
get instance(): ProxyProvider | undefined {
|
||||
return this.wizard.provider as ProxyProvider;
|
||||
}
|
||||
|
||||
renderModeDescription(): MaybeTemplateResult {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
renderProxyMode(): TemplateResult {
|
||||
throw new Error("Must be implemented in a child class.");
|
||||
}
|
||||
|
||||
renderHttpBasic() {
|
||||
return html`<ak-text-input
|
||||
name="basicAuthUserAttribute"
|
||||
label=${msg("HTTP-Basic Username Key")}
|
||||
value="${ifDefined(this.instance?.basicAuthUserAttribute)}"
|
||||
help=${msg(
|
||||
"User/Group Attribute used for the user part of the HTTP-Basic Header. If not set, the user's Email address is used.",
|
||||
)}
|
||||
>
|
||||
</ak-text-input>
|
||||
|
||||
<ak-text-input
|
||||
name="basicAuthPasswordAttribute"
|
||||
label=${msg("HTTP-Basic Password Key")}
|
||||
value="${ifDefined(this.instance?.basicAuthPasswordAttribute)}"
|
||||
help=${msg(
|
||||
"User/Group Attribute used for the password part of the HTTP-Basic Header.",
|
||||
)}
|
||||
>
|
||||
</ak-text-input>`;
|
||||
}
|
||||
|
||||
render() {
|
||||
const errors = this.wizard.errors.provider;
|
||||
|
||||
return html` <ak-wizard-title>${msg("Configure Proxy Provider")}</ak-wizard-title>
|
||||
<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
|
||||
${this.renderModeDescription()}
|
||||
<ak-text-input
|
||||
name="name"
|
||||
value=${ifDefined(this.instance?.name)}
|
||||
required
|
||||
.errorMessages=${errors?.name ?? []}
|
||||
label=${msg("Name")}
|
||||
></ak-text-input>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Authorization flow")}
|
||||
required
|
||||
name="authorizationFlow"
|
||||
.errorMessages=${errors?.authorizationFlow ?? []}
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authorization}
|
||||
.currentFlow=${this.instance?.authorizationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when authorizing this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
${this.renderProxyMode()}
|
||||
|
||||
<ak-text-input
|
||||
name="accessTokenValidity"
|
||||
value=${first(this.instance?.accessTokenValidity, "hours=24")}
|
||||
label=${msg("Token validity")}
|
||||
help=${msg("Configure how long tokens are valid for.")}
|
||||
.errorMessages=${errors?.accessTokenValidity ?? []}
|
||||
></ak-text-input>
|
||||
|
||||
<ak-form-group>
|
||||
<span slot="header">${msg("Advanced protocol settings")}</span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Certificate")}
|
||||
name="certificate"
|
||||
.errorMessages=${errors?.certificate ?? []}
|
||||
>
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${ifDefined(this.instance?.certificate ?? undefined)}
|
||||
></ak-crypto-certificate-search>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Additional scopes")}
|
||||
name="propertyMappings"
|
||||
>
|
||||
<ak-dual-select-dynamic-selected
|
||||
.provider=${propertyMappingsProvider}
|
||||
.selector=${propertyMappingsSelector(
|
||||
this.instance?.propertyMappings,
|
||||
)}
|
||||
available-label="${msg("Available Scopes")}"
|
||||
selected-label="${msg("Selected Scopes")}"
|
||||
></ak-dual-select-dynamic-selected>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Additional scope mappings, which are passed to the proxy.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-textarea-input
|
||||
name="skipPathRegex"
|
||||
label=${this.mode === ProxyMode.ForwardDomain
|
||||
? msg("Unauthenticated URLs")
|
||||
: msg("Unauthenticated Paths")}
|
||||
value=${ifDefined(this.instance?.skipPathRegex)}
|
||||
.errorMessages=${errors?.skipPathRegex ?? []}
|
||||
.bighelp=${html` <p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Regular expressions for which authentication is not required. Each new line is interpreted as a new expression.",
|
||||
)}
|
||||
</p>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"When using proxy or forward auth (single application) mode, the requested URL Path is checked against the regular expressions. When using forward auth (domain mode), the full requested URL including scheme and host is matched against the regular expressions.",
|
||||
)}
|
||||
</p>`}
|
||||
>
|
||||
</ak-textarea-input>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced flow settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
name="authenticationFlow"
|
||||
label=${msg("Authentication flow")}
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${this.instance?.authenticationFlow}
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Flow used when a user access this provider and is not authenticated.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Invalidation flow")}
|
||||
name="invalidationFlow"
|
||||
required
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
|
||||
.currentFlow=${this.instance?.invalidationFlow}
|
||||
defaultFlowSlug="default-provider-invalidation-flow"
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when logging out of this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group>
|
||||
<span slot="header">${msg("Authentication settings")}</span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-switch-input
|
||||
name="interceptHeaderAuth"
|
||||
?checked=${first(this.instance?.interceptHeaderAuth, true)}
|
||||
label=${msg("Intercept header authentication")}
|
||||
help=${msg(
|
||||
"When enabled, authentik will intercept the Authorization header to authenticate the request.",
|
||||
)}
|
||||
></ak-switch-input>
|
||||
|
||||
<ak-switch-input
|
||||
name="basicAuthEnabled"
|
||||
?checked=${first(this.instance?.basicAuthEnabled, false)}
|
||||
@change=${(ev: Event) => {
|
||||
const el = ev.target as HTMLInputElement;
|
||||
this.showHttpBasic = el.checked;
|
||||
}}
|
||||
label=${msg("Send HTTP-Basic Authentication")}
|
||||
help=${msg(
|
||||
"Send a custom HTTP-Basic Authentication header based on values from authentik.",
|
||||
)}
|
||||
></ak-switch-input>
|
||||
|
||||
${this.showHttpBasic ? this.renderHttpBasic() : html``}
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Trusted OIDC Sources")}
|
||||
name="jwksSources"
|
||||
.errorMessages=${errors?.jwksSources ?? []}
|
||||
>
|
||||
<ak-dual-select-dynamic-selected
|
||||
.provider=${oauth2SourcesProvider}
|
||||
.selector=${oauth2SourcesSelector(
|
||||
this.instance?.jwtFederationSources,
|
||||
)}
|
||||
available-label=${msg("Available Sources")}
|
||||
selected-label=${msg("Selected Sources")}
|
||||
></ak-dual-select-dynamic-selected>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
</form>`;
|
||||
}
|
||||
}
|
||||
|
||||
export default AkTypeProxyApplicationWizardPage;
|
@ -1,74 +0,0 @@
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { customElement } from "@lit/reactive-element/decorators.js";
|
||||
import { html } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import PFList from "@patternfly/patternfly/components/List/list.css";
|
||||
|
||||
import { ProxyProvider } from "@goauthentik/api";
|
||||
|
||||
import AkTypeProxyApplicationWizardPage from "./AuthenticationByProxyPage";
|
||||
|
||||
@customElement("ak-application-wizard-authentication-for-forward-proxy-domain")
|
||||
export class AkForwardDomainProxyApplicationWizardPage extends AkTypeProxyApplicationWizardPage {
|
||||
static get styles() {
|
||||
return super.styles.concat(PFList);
|
||||
}
|
||||
|
||||
renderModeDescription() {
|
||||
return html`<p>
|
||||
${msg(
|
||||
"Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application.",
|
||||
)}
|
||||
</p>
|
||||
<div>
|
||||
${msg("An example setup can look like this:")}
|
||||
<ul class="pf-c-list">
|
||||
<li>${msg("authentik running on auth.example.com")}</li>
|
||||
<li>${msg("app1 running on app1.example.com")}</li>
|
||||
</ul>
|
||||
${msg(
|
||||
"In this case, you'd set the Authentication URL to auth.example.com and Cookie domain to example.com.",
|
||||
)}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
renderProxyMode() {
|
||||
const provider = this.wizard.provider as ProxyProvider | undefined;
|
||||
const errors = this.wizard.errors.provider;
|
||||
|
||||
return html`
|
||||
<ak-text-input
|
||||
name="externalHost"
|
||||
label=${msg("External host")}
|
||||
value=${ifDefined(provider?.externalHost)}
|
||||
.errorMessages=${errors?.externalHost ?? []}
|
||||
required
|
||||
help=${msg(
|
||||
"The external URL you'll authenticate at. The authentik core server should be reachable under this URL.",
|
||||
)}
|
||||
>
|
||||
</ak-text-input>
|
||||
<ak-text-input
|
||||
name="cookieDomain"
|
||||
label=${msg("Cookie domain")}
|
||||
value="${ifDefined(provider?.cookieDomain)}"
|
||||
.errorMessages=${errors?.cookieDomain ?? []}
|
||||
required
|
||||
help=${msg(
|
||||
"Set this to the domain you wish the authentication to be valid for. Must be a parent domain of the URL above. If you're running applications as app1.domain.tld, app2.domain.tld, set this to 'domain.tld'.",
|
||||
)}
|
||||
></ak-text-input>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export default AkForwardDomainProxyApplicationWizardPage;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-application-wizard-authentication-for-forward-proxy-domain": AkForwardDomainProxyApplicationWizardPage;
|
||||
}
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/components/ak-switch-input";
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { customElement } from "@lit/reactive-element/decorators.js";
|
||||
import { html } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import { ProxyProvider } from "@goauthentik/api";
|
||||
|
||||
import AkTypeProxyApplicationWizardPage from "./AuthenticationByProxyPage";
|
||||
|
||||
@customElement("ak-application-wizard-authentication-for-reverse-proxy")
|
||||
export class AkReverseProxyApplicationWizardPage extends AkTypeProxyApplicationWizardPage {
|
||||
renderModeDescription() {
|
||||
return html`<p class="pf-u-mb-xl">
|
||||
${msg(
|
||||
"This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well.",
|
||||
)}
|
||||
</p>`;
|
||||
}
|
||||
|
||||
renderProxyMode() {
|
||||
const provider = this.wizard.provider as ProxyProvider | undefined;
|
||||
const errors = this.wizard.errors.provider;
|
||||
|
||||
return html` <ak-text-input
|
||||
name="externalHost"
|
||||
value=${ifDefined(provider?.externalHost)}
|
||||
required
|
||||
label=${msg("External host")}
|
||||
.errorMessages=${errors?.externalHost ?? []}
|
||||
help=${msg(
|
||||
"The external URL you'll access the application at. Include any non-standard port.",
|
||||
)}
|
||||
></ak-text-input>
|
||||
<ak-text-input
|
||||
name="internalHost"
|
||||
value=${ifDefined(provider?.internalHost)}
|
||||
.errorMessages=${errors?.internalHost ?? []}
|
||||
required
|
||||
label=${msg("Internal host")}
|
||||
help=${msg("Upstream host that the requests are forwarded to.")}
|
||||
></ak-text-input>
|
||||
<ak-switch-input
|
||||
name="internalHostSslValidation"
|
||||
?checked=${first(provider?.internalHostSslValidation, true)}
|
||||
label=${msg("Internal host SSL Validation")}
|
||||
help=${msg("Validate SSL Certificates of upstream servers.")}
|
||||
>
|
||||
</ak-switch-input>`;
|
||||
}
|
||||
}
|
||||
|
||||
export default AkReverseProxyApplicationWizardPage;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-application-wizard-authentication-for-reverse-proxy": AkReverseProxyApplicationWizardPage;
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { customElement } from "@lit/reactive-element/decorators.js";
|
||||
import { html } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import { ProxyProvider } from "@goauthentik/api";
|
||||
|
||||
import AkTypeProxyApplicationWizardPage from "./AuthenticationByProxyPage";
|
||||
|
||||
@customElement("ak-application-wizard-authentication-for-single-forward-proxy")
|
||||
export class AkForwardSingleProxyApplicationWizardPage extends AkTypeProxyApplicationWizardPage {
|
||||
renderModeDescription() {
|
||||
return html`<p class="pf-u-mb-xl">
|
||||
${msg(
|
||||
html`Use this provider with nginx's <code>auth_request</code> or traefik's
|
||||
<code>forwardAuth</code>. Each application/domain needs its own provider.
|
||||
Additionally, on each domain, <code>/outpost.goauthentik.io</code> must be
|
||||
routed to the outpost (when using a managed outpost, this is done for you).`,
|
||||
)}
|
||||
</p>`;
|
||||
}
|
||||
|
||||
renderProxyMode() {
|
||||
const provider = this.wizard.provider as ProxyProvider | undefined;
|
||||
const errors = this.wizard.errors.provider;
|
||||
|
||||
return html`<ak-text-input
|
||||
name="externalHost"
|
||||
value=${ifDefined(provider?.externalHost)}
|
||||
required
|
||||
label=${msg("External host")}
|
||||
.errorMessages=${errors?.externalHost ?? []}
|
||||
help=${msg(
|
||||
"The external URL you'll access the application at. Include any non-standard port.",
|
||||
)}
|
||||
></ak-text-input>`;
|
||||
}
|
||||
}
|
||||
|
||||
export default AkForwardSingleProxyApplicationWizardPage;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-application-wizard-authentication-for-single-forward-proxy": AkForwardSingleProxyApplicationWizardPage;
|
||||
}
|
||||
}
|
@ -1,108 +0,0 @@
|
||||
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
|
||||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
|
||||
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { customElement } from "@lit/reactive-element/decorators.js";
|
||||
import { html } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import { FlowsInstancesListDesignationEnum, RadiusProvider } from "@goauthentik/api";
|
||||
|
||||
import BaseProviderPanel from "../BaseProviderPanel";
|
||||
|
||||
@customElement("ak-application-wizard-authentication-by-radius")
|
||||
export class ApplicationWizardAuthenticationByRadius extends WithBrandConfig(BaseProviderPanel) {
|
||||
render() {
|
||||
const provider = this.wizard.provider as RadiusProvider | undefined;
|
||||
const errors = this.wizard.errors.provider;
|
||||
|
||||
return html`<ak-wizard-title>${msg("Configure Radius Provider")}</ak-wizard-title>
|
||||
<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
|
||||
<ak-text-input
|
||||
name="name"
|
||||
label=${msg("Name")}
|
||||
value=${ifDefined(provider?.name)}
|
||||
.errorMessages=${errors?.name ?? []}
|
||||
required
|
||||
>
|
||||
</ak-text-input>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Authentication flow")}
|
||||
?required=${true}
|
||||
name="authorizationFlow"
|
||||
.errorMessages=${errors?.authorizationFlow ?? []}
|
||||
>
|
||||
<ak-branded-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${provider?.authorizationFlow}
|
||||
.brandFlow=${this.brand.flowAuthentication}
|
||||
required
|
||||
></ak-branded-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used for users to authenticate.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-group expanded>
|
||||
<span slot="header"> ${msg("Protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-text-input
|
||||
name="sharedSecret"
|
||||
label=${msg("Shared secret")}
|
||||
.errorMessages=${errors?.sharedSecret ?? []}
|
||||
value=${first(
|
||||
provider?.sharedSecret,
|
||||
randomString(128, ascii_letters + digits),
|
||||
)}
|
||||
required
|
||||
></ak-text-input>
|
||||
<ak-text-input
|
||||
name="clientNetworks"
|
||||
label=${msg("Client Networks")}
|
||||
value=${first(provider?.clientNetworks, "0.0.0.0/0, ::/0")}
|
||||
.errorMessages=${errors?.clientNetworks ?? []}
|
||||
required
|
||||
help=${msg(`List of CIDRs (comma-seperated) that clients can connect from. A more specific
|
||||
CIDR will match before a looser one. Clients connecting from a non-specified CIDR
|
||||
will be dropped.`)}
|
||||
></ak-text-input>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced flow settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Invalidation flow")}
|
||||
name="invalidationFlow"
|
||||
required
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
|
||||
.currentFlow=${provider?.invalidationFlow}
|
||||
defaultFlowSlug="default-invalidation-flow"
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when logging out of this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div></ak-form-group
|
||||
>
|
||||
</form>`;
|
||||
}
|
||||
}
|
||||
|
||||
export default ApplicationWizardAuthenticationByRadius;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-application-wizard-authentication-by-radius": ApplicationWizardAuthenticationByRadius;
|
||||
}
|
||||
}
|
@ -1,364 +0,0 @@
|
||||
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
|
||||
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
|
||||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||
import AkCryptoCertificateSearch from "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/components/ak-multi-select";
|
||||
import "@goauthentik/components/ak-number-input";
|
||||
import "@goauthentik/components/ak-radio-input";
|
||||
import "@goauthentik/components/ak-switch-input";
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { customElement, state } from "@lit/reactive-element/decorators.js";
|
||||
import { html, nothing } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import {
|
||||
FlowsInstancesListDesignationEnum,
|
||||
PaginatedSAMLPropertyMappingList,
|
||||
PropertymappingsApi,
|
||||
SAMLProvider,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import BaseProviderPanel from "../BaseProviderPanel";
|
||||
import {
|
||||
digestAlgorithmOptions,
|
||||
signatureAlgorithmOptions,
|
||||
spBindingOptions,
|
||||
} from "./SamlProviderOptions";
|
||||
import "./saml-property-mappings-search";
|
||||
|
||||
@customElement("ak-application-wizard-authentication-by-saml-configuration")
|
||||
export class ApplicationWizardProviderSamlConfiguration extends BaseProviderPanel {
|
||||
@state()
|
||||
propertyMappings?: PaginatedSAMLPropertyMappingList;
|
||||
|
||||
@state()
|
||||
hasSigningKp = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
new PropertymappingsApi(DEFAULT_CONFIG)
|
||||
.propertymappingsProviderSamlList({
|
||||
ordering: "saml_name",
|
||||
})
|
||||
.then((propertyMappings: PaginatedSAMLPropertyMappingList) => {
|
||||
this.propertyMappings = propertyMappings;
|
||||
});
|
||||
}
|
||||
|
||||
propertyMappingConfiguration(provider?: SAMLProvider) {
|
||||
const propertyMappings = this.propertyMappings?.results ?? [];
|
||||
|
||||
const configuredMappings = (providerMappings: string[]) =>
|
||||
propertyMappings.map((pm) => pm.pk).filter((pmpk) => providerMappings.includes(pmpk));
|
||||
|
||||
const managedMappings = () =>
|
||||
propertyMappings
|
||||
.filter((pm) => (pm?.managed ?? "").startsWith("goauthentik.io/providers/saml"))
|
||||
.map((pm) => pm.pk);
|
||||
|
||||
const pmValues = provider?.propertyMappings
|
||||
? configuredMappings(provider?.propertyMappings ?? [])
|
||||
: managedMappings();
|
||||
|
||||
const propertyPairs = propertyMappings.map((pm) => [pm.pk, pm.name]);
|
||||
|
||||
return { pmValues, propertyPairs };
|
||||
}
|
||||
|
||||
render() {
|
||||
const provider = this.wizard.provider as SAMLProvider | undefined;
|
||||
const errors = this.wizard.errors.provider;
|
||||
|
||||
const { pmValues, propertyPairs } = this.propertyMappingConfiguration(provider);
|
||||
|
||||
return html` <ak-wizard-title>${msg("Configure SAML Provider")}</ak-wizard-title>
|
||||
<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
|
||||
<ak-text-input
|
||||
name="name"
|
||||
value=${ifDefined(provider?.name)}
|
||||
required
|
||||
label=${msg("Name")}
|
||||
.errorMessages=${errors?.name ?? []}
|
||||
></ak-text-input>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Authorization flow")}
|
||||
?required=${true}
|
||||
name="authorizationFlow"
|
||||
.errorMessages=${errors?.authorizationFlow ?? []}
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authorization}
|
||||
.currentFlow=${provider?.authorizationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when authorizing this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-group .expanded=${true}>
|
||||
<span slot="header"> ${msg("Protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-text-input
|
||||
name="acsUrl"
|
||||
value=${ifDefined(provider?.acsUrl)}
|
||||
required
|
||||
label=${msg("ACS URL")}
|
||||
.errorMessages=${errors?.acsUrl ?? []}
|
||||
></ak-text-input>
|
||||
|
||||
<ak-text-input
|
||||
name="issuer"
|
||||
value=${provider?.issuer || "authentik"}
|
||||
required
|
||||
label=${msg("Issuer")}
|
||||
help=${msg("Also known as EntityID.")}
|
||||
.errorMessages=${errors?.issuer ?? []}
|
||||
></ak-text-input>
|
||||
|
||||
<ak-radio-input
|
||||
name="spBinding"
|
||||
label=${msg("Service Provider Binding")}
|
||||
required
|
||||
.options=${spBindingOptions}
|
||||
.value=${provider?.spBinding}
|
||||
help=${msg(
|
||||
"Determines how authentik sends the response back to the Service Provider.",
|
||||
)}
|
||||
>
|
||||
</ak-radio-input>
|
||||
|
||||
<ak-text-input
|
||||
name="audience"
|
||||
value=${ifDefined(provider?.audience)}
|
||||
label=${msg("Audience")}
|
||||
.errorMessages=${errors?.audience ?? []}
|
||||
></ak-text-input>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced flow settings")}</span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
name="authenticationFlow"
|
||||
label=${msg("Authentication flow")}
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${provider?.authenticationFlow}
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Flow used when a user access this provider and is not authenticated.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Invalidation flow")}
|
||||
name="invalidationFlow"
|
||||
required
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
|
||||
.currentFlow=${provider?.invalidationFlow}
|
||||
defaultFlowSlug="default-provider-invalidation-flow"
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when logging out of this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Signing Certificate")}
|
||||
name="signingKp"
|
||||
>
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${ifDefined(provider?.signingKp ?? undefined)}
|
||||
@input=${(ev: InputEvent) => {
|
||||
const target = ev.target as AkCryptoCertificateSearch;
|
||||
if (!target) return;
|
||||
this.hasSigningKp = !!target.selectedKeypair;
|
||||
}}
|
||||
></ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Certificate used to sign outgoing Responses going to the Service Provider.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
${this.hasSigningKp
|
||||
? html` <ak-form-element-horizontal name="signAssertion">
|
||||
<label class="pf-c-switch">
|
||||
<input
|
||||
class="pf-c-switch__input"
|
||||
type="checkbox"
|
||||
?checked=${first(provider?.signAssertion, true)}
|
||||
/>
|
||||
<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 class="pf-c-switch__label"
|
||||
>${msg("Sign assertions")}</span
|
||||
>
|
||||
</label>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"When enabled, the assertion element of the SAML response will be signed.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal name="signResponse">
|
||||
<label class="pf-c-switch">
|
||||
<input
|
||||
class="pf-c-switch__input"
|
||||
type="checkbox"
|
||||
?checked=${first(provider?.signResponse, false)}
|
||||
/>
|
||||
<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 class="pf-c-switch__label"
|
||||
>${msg("Sign responses")}</span
|
||||
>
|
||||
</label>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"When enabled, the assertion element of the SAML response will be signed.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>`
|
||||
: nothing}
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Verification Certificate")}
|
||||
name="verificationKp"
|
||||
>
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${ifDefined(provider?.verificationKp ?? undefined)}
|
||||
nokey
|
||||
></ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Encryption Certificate")}
|
||||
name="encryptionKp"
|
||||
>
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${ifDefined(provider?.encryptionKp ?? undefined)}
|
||||
></ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"When selected, encrypted assertions will be decrypted using this keypair.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-multi-select
|
||||
label=${msg("Property Mappings")}
|
||||
name="propertyMappings"
|
||||
.options=${propertyPairs}
|
||||
.values=${pmValues}
|
||||
.richhelp=${html` <p class="pf-c-form__helper-text">
|
||||
${msg("Property mappings used for user mapping.")}
|
||||
</p>`}
|
||||
></ak-multi-select>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("NameID Property Mapping")}
|
||||
name="nameIdMapping"
|
||||
>
|
||||
<ak-saml-property-mapping-search
|
||||
name="nameIdMapping"
|
||||
propertymapping=${ifDefined(provider?.nameIdMapping ?? undefined)}
|
||||
></ak-saml-property-mapping-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Configure how the NameID value will be created. When left empty, the NameIDPolicy of the incoming request will be respected.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-text-input
|
||||
name="assertionValidNotBefore"
|
||||
value=${provider?.assertionValidNotBefore || "minutes=-5"}
|
||||
required
|
||||
label=${msg("Assertion valid not before")}
|
||||
help=${msg(
|
||||
"Configure the maximum allowed time drift for an assertion.",
|
||||
)}
|
||||
.errorMessages=${errors?.assertionValidNotBefore ?? []}
|
||||
></ak-text-input>
|
||||
|
||||
<ak-text-input
|
||||
name="assertionValidNotOnOrAfter"
|
||||
value=${provider?.assertionValidNotOnOrAfter || "minutes=5"}
|
||||
required
|
||||
label=${msg("Assertion valid not on or after")}
|
||||
help=${msg(
|
||||
"Assertion not valid on or after current time + this value.",
|
||||
)}
|
||||
.errorMessages=${errors?.assertionValidNotOnOrAfter ?? []}
|
||||
></ak-text-input>
|
||||
|
||||
<ak-text-input
|
||||
name="sessionValidNotOnOrAfter"
|
||||
value=${provider?.sessionValidNotOnOrAfter || "minutes=86400"}
|
||||
required
|
||||
label=${msg("Session valid not on or after")}
|
||||
help=${msg("Session not valid on or after current time + this value.")}
|
||||
.errorMessages=${errors?.sessionValidNotOnOrAfter ?? []}
|
||||
></ak-text-input>
|
||||
|
||||
<ak-radio-input
|
||||
name="digestAlgorithm"
|
||||
label=${msg("Digest algorithm")}
|
||||
required
|
||||
.options=${digestAlgorithmOptions}
|
||||
.value=${provider?.digestAlgorithm}
|
||||
>
|
||||
</ak-radio-input>
|
||||
|
||||
<ak-radio-input
|
||||
name="signatureAlgorithm"
|
||||
label=${msg("Signature algorithm")}
|
||||
required
|
||||
.options=${signatureAlgorithmOptions}
|
||||
.value=${provider?.signatureAlgorithm}
|
||||
>
|
||||
</ak-radio-input>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
</form>`;
|
||||
}
|
||||
}
|
||||
|
||||
export default ApplicationWizardProviderSamlConfiguration;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-application-wizard-authentication-by-saml-configuration": ApplicationWizardProviderSamlConfiguration;
|
||||
}
|
||||
}
|
@ -1,120 +0,0 @@
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { SearchSelect } from "@goauthentik/elements/forms/SearchSelect";
|
||||
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||
|
||||
import { html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { property, query } from "lit/decorators.js";
|
||||
|
||||
import {
|
||||
PropertymappingsApi,
|
||||
PropertymappingsProviderSamlListRequest,
|
||||
SAMLPropertyMapping,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
async function fetchObjects(query?: string): Promise<SAMLPropertyMapping[]> {
|
||||
const args: PropertymappingsProviderSamlListRequest = {
|
||||
ordering: "saml_name",
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const items = await new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsProviderSamlList(
|
||||
args,
|
||||
);
|
||||
return items.results;
|
||||
}
|
||||
|
||||
function renderElement(item: SAMLPropertyMapping): string {
|
||||
return item.name;
|
||||
}
|
||||
|
||||
function renderValue(item: SAMLPropertyMapping | undefined): string | undefined {
|
||||
return item?.pk;
|
||||
}
|
||||
|
||||
/**
|
||||
* SAML Property Mapping Search
|
||||
*
|
||||
* @element ak-saml-property-mapping-search
|
||||
*
|
||||
* A wrapper around SearchSelect for the SAML Property Search. It's a unique search, but for the
|
||||
* purpose of the form all you need to know is that it is being searched and selected. Let's put the
|
||||
* how somewhere else.
|
||||
*
|
||||
*/
|
||||
|
||||
@customElement("ak-saml-property-mapping-search")
|
||||
export class SAMLPropertyMappingSearch extends CustomListenerElement(AKElement) {
|
||||
/**
|
||||
* The current property mapping known to the caller.
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: String, reflect: true, attribute: "propertymapping" })
|
||||
propertyMapping?: string;
|
||||
|
||||
@query("ak-search-select")
|
||||
search!: SearchSelect<SAMLPropertyMapping>;
|
||||
|
||||
@property({ type: String })
|
||||
name: string | null | undefined;
|
||||
|
||||
selectedPropertyMapping?: SAMLPropertyMapping;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.selected = this.selected.bind(this);
|
||||
this.handleSearchUpdate = this.handleSearchUpdate.bind(this);
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.selectedPropertyMapping ? renderValue(this.selectedPropertyMapping) : undefined;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
const horizontalContainer = this.closest("ak-form-element-horizontal[name]");
|
||||
if (!horizontalContainer) {
|
||||
throw new Error("This search can only be used in a named ak-form-element-horizontal");
|
||||
}
|
||||
const name = horizontalContainer.getAttribute("name");
|
||||
const myName = this.getAttribute("name");
|
||||
if (name !== null && name !== myName) {
|
||||
this.setAttribute("name", name);
|
||||
}
|
||||
}
|
||||
|
||||
handleSearchUpdate(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
this.selectedPropertyMapping = ev.detail.value;
|
||||
this.dispatchEvent(new InputEvent("input", { bubbles: true, composed: true }));
|
||||
}
|
||||
|
||||
selected(item: SAMLPropertyMapping): boolean {
|
||||
return this.propertyMapping === item.pk;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<ak-search-select
|
||||
.fetchObjects=${fetchObjects}
|
||||
.renderElement=${renderElement}
|
||||
.value=${renderValue}
|
||||
.selected=${this.selected}
|
||||
@ak-change=${this.handleSearchUpdate}
|
||||
blankable
|
||||
>
|
||||
</ak-search-select>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export default SAMLPropertyMappingSearch;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-saml-property-mapping-search": SAMLPropertyMappingSearch;
|
||||
}
|
||||
}
|
@ -1,158 +0,0 @@
|
||||
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
|
||||
import "@goauthentik/admin/common/ak-core-group-search";
|
||||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/components/ak-multi-select";
|
||||
import "@goauthentik/components/ak-switch-input";
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { customElement, state } from "@lit/reactive-element/decorators.js";
|
||||
import { html } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import { PaginatedSCIMMappingList, PropertymappingsApi, type SCIMProvider } from "@goauthentik/api";
|
||||
|
||||
import BaseProviderPanel from "../BaseProviderPanel";
|
||||
|
||||
@customElement("ak-application-wizard-authentication-by-scim")
|
||||
export class ApplicationWizardAuthenticationBySCIM extends BaseProviderPanel {
|
||||
@state()
|
||||
propertyMappings?: PaginatedSCIMMappingList;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
new PropertymappingsApi(DEFAULT_CONFIG)
|
||||
.propertymappingsProviderScimList({
|
||||
ordering: "managed",
|
||||
})
|
||||
.then((propertyMappings: PaginatedSCIMMappingList) => {
|
||||
this.propertyMappings = propertyMappings;
|
||||
});
|
||||
}
|
||||
|
||||
propertyMappingConfiguration(provider?: SCIMProvider) {
|
||||
const propertyMappings = this.propertyMappings?.results ?? [];
|
||||
|
||||
const configuredMappings = (providerMappings: string[]) =>
|
||||
propertyMappings.map((pm) => pm.pk).filter((pmpk) => providerMappings.includes(pmpk));
|
||||
|
||||
const managedMappings = (key: string) =>
|
||||
propertyMappings
|
||||
.filter((pm) => pm.managed === `goauthentik.io/providers/scim/${key}`)
|
||||
.map((pm) => pm.pk);
|
||||
|
||||
const pmUserValues = provider?.propertyMappings
|
||||
? configuredMappings(provider?.propertyMappings ?? [])
|
||||
: managedMappings("user");
|
||||
|
||||
const pmGroupValues = provider?.propertyMappingsGroup
|
||||
? configuredMappings(provider?.propertyMappingsGroup ?? [])
|
||||
: managedMappings("group");
|
||||
|
||||
const propertyPairs = propertyMappings.map((pm) => [pm.pk, pm.name]);
|
||||
|
||||
return { pmUserValues, pmGroupValues, propertyPairs };
|
||||
}
|
||||
|
||||
render() {
|
||||
const provider = this.wizard.provider as SCIMProvider | undefined;
|
||||
const errors = this.wizard.errors.provider;
|
||||
|
||||
const { pmUserValues, pmGroupValues, propertyPairs } =
|
||||
this.propertyMappingConfiguration(provider);
|
||||
|
||||
return html`<ak-wizard-title>${msg("Configure SCIM Provider")}</ak-wizard-title>
|
||||
<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
|
||||
<ak-text-input
|
||||
name="name"
|
||||
label=${msg("Name")}
|
||||
value=${ifDefined(provider?.name)}
|
||||
.errorMessages=${errors?.name ?? []}
|
||||
required
|
||||
></ak-text-input>
|
||||
<ak-form-group expanded>
|
||||
<span slot="header"> ${msg("Protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-text-input
|
||||
name="url"
|
||||
label=${msg("URL")}
|
||||
value="${first(provider?.url, "")}"
|
||||
required
|
||||
help=${msg("SCIM base url, usually ends in /v2.")}
|
||||
.errorMessages=${errors?.url ?? []}
|
||||
>
|
||||
</ak-text-input>
|
||||
<ak-text-input
|
||||
name="token"
|
||||
label=${msg("Token")}
|
||||
value="${first(provider?.token, "")}"
|
||||
.errorMessages=${errors?.token ?? []}
|
||||
required
|
||||
help=${msg(
|
||||
"Token to authenticate with. Currently only bearer authentication is supported.",
|
||||
)}
|
||||
>
|
||||
</ak-text-input>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group expanded>
|
||||
<span slot="header">${msg("User filtering")}</span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-switch-input
|
||||
name="excludeUsersServiceAccount"
|
||||
?checked=${first(provider?.excludeUsersServiceAccount, true)}
|
||||
label=${msg("Exclude service accounts")}
|
||||
></ak-switch-input>
|
||||
<ak-form-element-horizontal label=${msg("Group")} name="filterGroup">
|
||||
<ak-core-group-search
|
||||
.group=${provider?.filterGroup}
|
||||
></ak-core-group-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Only sync users within the selected group.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group ?expanded=${true}>
|
||||
<span slot="header"> ${msg("Attribute mapping")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-multi-select
|
||||
label=${msg("User Property Mappings")}
|
||||
name="propertyMappings"
|
||||
.options=${propertyPairs}
|
||||
.values=${pmUserValues}
|
||||
.richhelp=${html`
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Property mappings used for user mapping.")}
|
||||
</p>
|
||||
`}
|
||||
></ak-multi-select>
|
||||
<ak-multi-select
|
||||
label=${msg("Group Property Mappings")}
|
||||
name="propertyMappingsGroup"
|
||||
.options=${propertyPairs}
|
||||
.values=${pmGroupValues}
|
||||
.richhelp=${html`
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Property mappings used for group creation.")}
|
||||
</p>
|
||||
`}
|
||||
></ak-multi-select>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
</form>`;
|
||||
}
|
||||
}
|
||||
|
||||
export default ApplicationWizardAuthenticationBySCIM;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-application-wizard-authentication-by-scim": ApplicationWizardAuthenticationBySCIM;
|
||||
}
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
import {
|
||||
BackStep,
|
||||
CancelWizard,
|
||||
CloseWizard,
|
||||
DisabledNextStep,
|
||||
NextStep,
|
||||
SubmitStep,
|
||||
} from "@goauthentik/components/ak-wizard-main/commonWizardButtons";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html } from "lit";
|
||||
|
||||
import "./application/ak-application-wizard-application-details";
|
||||
import "./auth-method-choice/ak-application-wizard-authentication-method-choice";
|
||||
import "./commit/ak-application-wizard-commit-application";
|
||||
import "./methods/ak-application-wizard-authentication-method";
|
||||
import { ApplicationStep as ApplicationStepType } from "./types";
|
||||
|
||||
/**
|
||||
* In the current implementation, all of the child forms have access to the wizard's
|
||||
* global context, into which all data is written, and which is updated by events
|
||||
* flowing into the top-level orchestrator.
|
||||
*/
|
||||
|
||||
class ApplicationStep implements ApplicationStepType {
|
||||
id = "application";
|
||||
label = msg("Application Details");
|
||||
disabled = false;
|
||||
valid = false;
|
||||
get buttons() {
|
||||
return [this.valid ? NextStep : DisabledNextStep, CancelWizard];
|
||||
}
|
||||
render() {
|
||||
return html`<ak-application-wizard-application-details></ak-application-wizard-application-details>`;
|
||||
}
|
||||
}
|
||||
|
||||
class ProviderMethodStep implements ApplicationStepType {
|
||||
id = "provider-method";
|
||||
label = msg("Provider Type");
|
||||
disabled = false;
|
||||
valid = false;
|
||||
|
||||
get buttons() {
|
||||
return [this.valid ? NextStep : DisabledNextStep, BackStep, CancelWizard];
|
||||
}
|
||||
|
||||
render() {
|
||||
// prettier-ignore
|
||||
return html`<ak-application-wizard-authentication-method-choice
|
||||
></ak-application-wizard-authentication-method-choice> `;
|
||||
}
|
||||
}
|
||||
|
||||
class ProviderStepDetails implements ApplicationStepType {
|
||||
id = "provider-details";
|
||||
label = msg("Provider Configuration");
|
||||
disabled = true;
|
||||
valid = false;
|
||||
get buttons() {
|
||||
return [this.valid ? SubmitStep : DisabledNextStep, BackStep, CancelWizard];
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<ak-application-wizard-authentication-method></ak-application-wizard-authentication-method>`;
|
||||
}
|
||||
}
|
||||
|
||||
class SubmitApplicationStep implements ApplicationStepType {
|
||||
id = "submit";
|
||||
label = msg("Submit Application");
|
||||
disabled = true;
|
||||
valid = false;
|
||||
|
||||
get buttons() {
|
||||
return this.valid ? [CloseWizard] : [BackStep, CancelWizard];
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<ak-application-wizard-commit-application></ak-application-wizard-commit-application>`;
|
||||
}
|
||||
}
|
||||
|
||||
export const newSteps = (): ApplicationStep[] => [
|
||||
new ApplicationStep(),
|
||||
new ProviderMethodStep(),
|
||||
new ProviderStepDetails(),
|
||||
new SubmitApplicationStep(),
|
||||
];
|
52
web/src/admin/applications/wizard/steps/ProviderChoices.ts
Normal file
52
web/src/admin/applications/wizard/steps/ProviderChoices.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import "@goauthentik/admin/common/ak-license-notice";
|
||||
|
||||
import { TemplateResult, html } from "lit";
|
||||
|
||||
import type { TypeCreate } from "@goauthentik/api";
|
||||
|
||||
type ProviderRenderer = () => TemplateResult;
|
||||
|
||||
export type LocalTypeCreate = TypeCreate & {
|
||||
renderer: ProviderRenderer;
|
||||
};
|
||||
|
||||
export const providerTypeRenderers: Record<
|
||||
string,
|
||||
{ render: () => TemplateResult; order: number }
|
||||
> = {
|
||||
oauth2provider: {
|
||||
render: () =>
|
||||
html`<ak-application-wizard-authentication-by-oauth></ak-application-wizard-authentication-by-oauth>`,
|
||||
order: 90,
|
||||
},
|
||||
ldapprovider: {
|
||||
render: () =>
|
||||
html`<ak-application-wizard-authentication-by-ldap></ak-application-wizard-authentication-by-ldap>`,
|
||||
order: 70,
|
||||
},
|
||||
proxyprovider: {
|
||||
render: () =>
|
||||
html`<ak-application-wizard-authentication-for-reverse-proxy></ak-application-wizard-authentication-for-reverse-proxy>`,
|
||||
order: 75,
|
||||
},
|
||||
racprovider: {
|
||||
render: () =>
|
||||
html`<ak-application-wizard-authentication-for-rac></ak-application-wizard-authentication-for-rac>`,
|
||||
order: 80,
|
||||
},
|
||||
samlprovider: {
|
||||
render: () =>
|
||||
html`<ak-application-wizard-authentication-by-saml-configuration></ak-application-wizard-authentication-by-saml-configuration>`,
|
||||
order: 80,
|
||||
},
|
||||
radiusprovider: {
|
||||
render: () =>
|
||||
html`<ak-application-wizard-authentication-by-radius></ak-application-wizard-authentication-by-radius>`,
|
||||
order: 70,
|
||||
},
|
||||
scimprovider: {
|
||||
render: () =>
|
||||
html`<ak-application-wizard-authentication-by-scim></ak-application-wizard-authentication-by-scim>`,
|
||||
order: 60,
|
||||
},
|
||||
};
|
@ -0,0 +1,152 @@
|
||||
import {
|
||||
type DescriptionPair,
|
||||
renderDescriptionList,
|
||||
} from "@goauthentik/components/DescriptionList.js";
|
||||
import { match } from "ts-pattern";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html } from "lit";
|
||||
|
||||
import {
|
||||
ClientTypeEnum,
|
||||
LDAPProvider,
|
||||
MatchingModeEnum,
|
||||
OAuth2Provider,
|
||||
ProviderModelEnum,
|
||||
ProxyMode,
|
||||
ProxyProvider,
|
||||
RACProvider,
|
||||
RadiusProvider,
|
||||
RedirectURI,
|
||||
SAMLProvider,
|
||||
SCIMProvider,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import { OneOfProvider } from "../types.js";
|
||||
|
||||
const renderSummary = (type: string, name: string, fields: DescriptionPair[]) =>
|
||||
renderDescriptionList([[msg("Type"), type], [msg("Name"), name], ...fields], {
|
||||
threecolumn: true,
|
||||
});
|
||||
|
||||
function renderSAMLOverview(rawProvider: OneOfProvider) {
|
||||
const provider = rawProvider as SAMLProvider;
|
||||
|
||||
return renderSummary("SAML", provider.name, [
|
||||
[msg("ACS URL"), provider.acsUrl],
|
||||
[msg("Audience"), provider.audience || "-"],
|
||||
[msg("Issuer"), provider.issuer],
|
||||
]);
|
||||
}
|
||||
|
||||
function renderSCIMOverview(rawProvider: OneOfProvider) {
|
||||
const provider = rawProvider as SCIMProvider;
|
||||
return renderSummary("SCIM", provider.name, [[msg("URL"), provider.url]]);
|
||||
}
|
||||
|
||||
function renderRadiusOverview(rawProvider: OneOfProvider) {
|
||||
const provider = rawProvider as RadiusProvider;
|
||||
return renderSummary("Radius", provider.name, [
|
||||
[msg("Client Networks"), provider.clientNetworks],
|
||||
]);
|
||||
}
|
||||
|
||||
function renderRACOverview(rawProvider: OneOfProvider) {
|
||||
// @ts-expect-error TS6133
|
||||
const _provider = rawProvider as RACProvider;
|
||||
}
|
||||
|
||||
function formatRedirectUris(uris: RedirectURI[] = []) {
|
||||
return uris.length > 0
|
||||
? html`<ul class="pf-c-list pf-m-plain">
|
||||
${uris.map(
|
||||
(uri) =>
|
||||
html`<li>
|
||||
${uri.url}
|
||||
(${uri.matchingMode === MatchingModeEnum.Strict
|
||||
? msg("strict")
|
||||
: msg("regexp")})
|
||||
</li>`,
|
||||
)}
|
||||
</ul>`
|
||||
: "-";
|
||||
}
|
||||
|
||||
const proxyModeToLabel = new Map([
|
||||
[ProxyMode.Proxy, msg("Proxy")],
|
||||
[ProxyMode.ForwardSingle, msg("Forward auth (single application)")],
|
||||
[ProxyMode.ForwardDomain, msg("Forward auth (domain-level)")],
|
||||
[ProxyMode.UnknownDefaultOpenApi, msg("Unknown proxy mode")],
|
||||
]);
|
||||
|
||||
function renderProxyOverview(rawProvider: OneOfProvider) {
|
||||
const provider = rawProvider as ProxyProvider;
|
||||
return renderSummary("Proxy", provider.name, [
|
||||
[msg("Mode"), proxyModeToLabel.get(provider.mode ?? ProxyMode.Proxy)],
|
||||
...match(provider.mode)
|
||||
.with(
|
||||
ProxyMode.Proxy,
|
||||
() =>
|
||||
[
|
||||
[msg("Internal Host"), provider.internalHost],
|
||||
[msg("External Host"), provider.externalHost],
|
||||
] as DescriptionPair[],
|
||||
)
|
||||
.with(
|
||||
ProxyMode.ForwardSingle,
|
||||
() => [[msg("External Host"), provider.externalHost]] as DescriptionPair[],
|
||||
)
|
||||
.with(
|
||||
ProxyMode.ForwardDomain,
|
||||
() =>
|
||||
[
|
||||
[msg("Authentication URL"), provider.externalHost],
|
||||
[msg("Cookie domain"), provider.cookieDomain],
|
||||
] as DescriptionPair[],
|
||||
)
|
||||
.otherwise(() => {
|
||||
throw new Error(
|
||||
`Unrecognized proxy mode: ${provider.mode?.toString() ?? "-- undefined __"}`,
|
||||
);
|
||||
}),
|
||||
[
|
||||
msg("Basic-Auth"),
|
||||
html` <ak-status-label
|
||||
type="info"
|
||||
?good=${provider.basicAuthEnabled}
|
||||
></ak-status-label>`,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
const clientTypeToLabel = new Map<ClientTypeEnum, string>([
|
||||
[ClientTypeEnum.Confidential, msg("Confidential")],
|
||||
[ClientTypeEnum.Public, msg("Public")],
|
||||
[ClientTypeEnum.UnknownDefaultOpenApi, msg("Unknown type")],
|
||||
]);
|
||||
|
||||
function renderOAuth2Overview(rawProvider: OneOfProvider) {
|
||||
const provider = rawProvider as OAuth2Provider;
|
||||
return renderSummary("OAuth2", provider.name, [
|
||||
[msg("Client type"), provider.clientType ? clientTypeToLabel.get(provider.clientType) : ""],
|
||||
[msg("Client ID"), provider.clientId],
|
||||
[msg("Redirect URIs"), formatRedirectUris(provider.redirectUris)],
|
||||
]);
|
||||
}
|
||||
|
||||
function renderLDAPOverview(rawProvider: OneOfProvider) {
|
||||
const provider = rawProvider as LDAPProvider;
|
||||
return renderSummary("Proxy", provider.name, [[msg("Base DN"), provider.baseDn]]);
|
||||
}
|
||||
|
||||
const providerName = (p: ProviderModelEnum): string => p.toString().split(".")[1];
|
||||
|
||||
export const providerRenderers = new Map([
|
||||
[providerName(ProviderModelEnum.SamlSamlprovider), renderSAMLOverview],
|
||||
[providerName(ProviderModelEnum.ScimScimprovider), renderSCIMOverview],
|
||||
[providerName(ProviderModelEnum.RadiusRadiusprovider), renderRadiusOverview],
|
||||
[providerName(ProviderModelEnum.RacRacprovider), renderRACOverview],
|
||||
[providerName(ProviderModelEnum.ProxyProxyprovider), renderProxyOverview],
|
||||
[providerName(ProviderModelEnum.Oauth2Oauth2provider), renderOAuth2Overview],
|
||||
[providerName(ProviderModelEnum.LdapLdapprovider), renderLDAPOverview],
|
||||
]);
|
@ -0,0 +1,192 @@
|
||||
import { policyOptions } from "@goauthentik/admin/applications/PolicyOptions.js";
|
||||
import { ApplicationWizardStep } from "@goauthentik/admin/applications/wizard/ApplicationWizardStep.js";
|
||||
import "@goauthentik/admin/applications/wizard/ak-wizard-title.js";
|
||||
import { isSlug } from "@goauthentik/common/utils.js";
|
||||
import { camelToSnake } from "@goauthentik/common/utils.js";
|
||||
import "@goauthentik/components/ak-radio-input";
|
||||
import "@goauthentik/components/ak-slug-input";
|
||||
import "@goauthentik/components/ak-switch-input";
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
import { type NavigableButton, type WizardButton } from "@goauthentik/components/ak-wizard/types";
|
||||
import { type KeyUnknown } from "@goauthentik/elements/forms/Form";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import { type ApplicationRequest } from "@goauthentik/api";
|
||||
|
||||
import { ApplicationWizardStateUpdate, ValidationRecord } from "../types";
|
||||
|
||||
const autoTrim = (v: unknown) => (typeof v === "string" ? v.trim() : v);
|
||||
|
||||
const trimMany = (o: KeyUnknown, vs: string[]) =>
|
||||
Object.fromEntries(vs.map((v) => [v, autoTrim(o[v])]));
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const isStr = (v: any): v is string => typeof v === "string";
|
||||
|
||||
@customElement("ak-application-wizard-application-step")
|
||||
export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
|
||||
label = msg("Application");
|
||||
|
||||
@state()
|
||||
errors = new Map<string, string>();
|
||||
|
||||
@query("form#applicationform")
|
||||
form!: HTMLFormElement;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
// This is the first step. Ensure it is always enabled.
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
errorMessages(name: string) {
|
||||
return this.errors.has(name)
|
||||
? [this.errors.get(name)]
|
||||
: (this.wizard.errors?.app?.[name] ??
|
||||
this.wizard.errors?.app?.[camelToSnake(name)] ??
|
||||
[]);
|
||||
}
|
||||
|
||||
get buttons(): WizardButton[] {
|
||||
return [{ kind: "next", destination: "provider-choice" }, { kind: "cancel" }];
|
||||
}
|
||||
|
||||
get valid() {
|
||||
this.errors = new Map();
|
||||
const values = trimMany(this.formValues ?? {}, ["metaLaunchUrl", "name", "slug"]);
|
||||
|
||||
if (values["name"] === "") {
|
||||
this.errors.set("name", msg("An application name is required"));
|
||||
}
|
||||
if (
|
||||
!(
|
||||
isStr(values["metaLaunchUrl"]) &&
|
||||
(values["metaLaunchUrl"] === "" || URL.canParse(values["metaLaunchUrl"]))
|
||||
)
|
||||
) {
|
||||
this.errors.set("metaLaunchUrl", msg("Not a valid URL"));
|
||||
}
|
||||
if (!(isStr(values["slug"]) && values["slug"] !== "" && isSlug(values["slug"]))) {
|
||||
this.errors.set("slug", msg("Not a valid slug"));
|
||||
}
|
||||
return this.errors.size === 0;
|
||||
}
|
||||
|
||||
override handleButton(button: NavigableButton) {
|
||||
if (button.kind === "next") {
|
||||
if (!this.valid) {
|
||||
this.handleEnabling({
|
||||
disabled: ["provider-choice", "provider", "bindings", "submit"],
|
||||
});
|
||||
return;
|
||||
}
|
||||
const app: Partial<ApplicationRequest> = this.formValues as Partial<ApplicationRequest>;
|
||||
|
||||
let payload: ApplicationWizardStateUpdate = {
|
||||
app: this.formValues,
|
||||
errors: this.removeErrors("app"),
|
||||
};
|
||||
if (app.name && (this.wizard.provider?.name ?? "").trim() === "") {
|
||||
payload = {
|
||||
...payload,
|
||||
provider: { name: `Provider for ${app.name}` },
|
||||
};
|
||||
}
|
||||
this.handleUpdate(payload, button.destination, {
|
||||
enable: "provider-choice",
|
||||
});
|
||||
return;
|
||||
}
|
||||
super.handleButton(button);
|
||||
}
|
||||
|
||||
renderForm(app: Partial<ApplicationRequest>, errors: ValidationRecord) {
|
||||
return html` <ak-wizard-title>${msg("Configure The Application")}</ak-wizard-title>
|
||||
<form id="applicationform" class="pf-c-form pf-m-horizontal" slot="form">
|
||||
<ak-text-input
|
||||
name="name"
|
||||
value=${ifDefined(app.name)}
|
||||
label=${msg("Name")}
|
||||
required
|
||||
?invalid=${this.errors.has("name")}
|
||||
.errorMessages=${errors.name ?? this.errorMessages("name")}
|
||||
help=${msg("Application's display Name.")}
|
||||
id="ak-application-wizard-details-name"
|
||||
></ak-text-input>
|
||||
<ak-slug-input
|
||||
name="slug"
|
||||
value=${ifDefined(app.slug)}
|
||||
label=${msg("Slug")}
|
||||
source="#ak-application-wizard-details-name"
|
||||
required
|
||||
?invalid=${errors.slug ?? this.errors.has("slug")}
|
||||
.errorMessages=${this.errorMessages("slug")}
|
||||
help=${msg("Internal application name used in URLs.")}
|
||||
></ak-slug-input>
|
||||
<ak-text-input
|
||||
name="group"
|
||||
value=${ifDefined(app.group)}
|
||||
label=${msg("Group")}
|
||||
.errorMessages=${errors.group ?? []}
|
||||
help=${msg(
|
||||
"Optionally enter a group name. Applications with identical groups are shown grouped together.",
|
||||
)}
|
||||
></ak-text-input>
|
||||
<ak-radio-input
|
||||
label=${msg("Policy engine mode")}
|
||||
required
|
||||
name="policyEngineMode"
|
||||
.options=${policyOptions}
|
||||
.value=${app.policyEngineMode}
|
||||
.errorMessages=${errors.policyEngineMode ?? []}
|
||||
></ak-radio-input>
|
||||
<ak-form-group aria-label=${msg("UI Settings")}>
|
||||
<span slot="header"> ${msg("UI Settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-text-input
|
||||
name="metaLaunchUrl"
|
||||
label=${msg("Launch URL")}
|
||||
value=${ifDefined(app.metaLaunchUrl)}
|
||||
?invalid=${this.errors.has("metaLaunchUrl")}
|
||||
.errorMessages=${errors.metaLaunchUrl ??
|
||||
this.errorMessages("metaLaunchUrl")}
|
||||
help=${msg(
|
||||
"If left empty, authentik will try to extract the launch URL based on the selected provider.",
|
||||
)}
|
||||
></ak-text-input>
|
||||
<ak-switch-input
|
||||
name="openInNewTab"
|
||||
?checked=${app.openInNewTab ?? false}
|
||||
label=${msg("Open in new tab")}
|
||||
help=${msg(
|
||||
"If checked, the launch URL will open in a new browser tab or window from the user's application library.",
|
||||
)}
|
||||
>
|
||||
</ak-switch-input>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
</form>`;
|
||||
}
|
||||
|
||||
renderMain() {
|
||||
if (!(this.wizard.app && this.wizard.errors)) {
|
||||
throw new Error("Application Step received uninitialized wizard context.");
|
||||
}
|
||||
return this.renderForm(
|
||||
this.wizard.app as ApplicationRequest,
|
||||
this.wizard.errors?.app ?? {},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-application-wizard-application-step": ApplicationWizardApplicationStep;
|
||||
}
|
||||
}
|
@ -0,0 +1,163 @@
|
||||
import { ApplicationWizardStep } from "@goauthentik/admin/applications/wizard/ApplicationWizardStep.js";
|
||||
import "@goauthentik/admin/applications/wizard/ak-wizard-title.js";
|
||||
import "@goauthentik/components/ak-radio-input";
|
||||
import "@goauthentik/components/ak-slug-input";
|
||||
import "@goauthentik/components/ak-status-label";
|
||||
import "@goauthentik/components/ak-switch-input";
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
import { type WizardButton } from "@goauthentik/components/ak-wizard/types";
|
||||
import "@goauthentik/elements/ak-table/ak-select-table.js";
|
||||
import { SelectTable } from "@goauthentik/elements/ak-table/ak-select-table.js";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import { P, match } from "ts-pattern";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { css, html } from "lit";
|
||||
import { customElement, query } from "lit/decorators.js";
|
||||
|
||||
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
||||
|
||||
import { makeEditButton } from "./bindings/ak-application-wizard-bindings-edit-button.js";
|
||||
import "./bindings/ak-application-wizard-bindings-toolbar.js";
|
||||
|
||||
const COLUMNS = [
|
||||
[msg("Order"), "order"],
|
||||
[msg("Binding")],
|
||||
[msg("Enabled"), "enabled"],
|
||||
[msg("Timeout"), "timeout"],
|
||||
[msg("Actions")],
|
||||
];
|
||||
|
||||
@customElement("ak-application-wizard-bindings-step")
|
||||
export class ApplicationWizardBindingsStep extends ApplicationWizardStep {
|
||||
label = msg("Configure Bindings");
|
||||
|
||||
get buttons(): WizardButton[] {
|
||||
return [
|
||||
{ kind: "next", destination: "submit" },
|
||||
{ kind: "back", destination: "provider" },
|
||||
{ kind: "cancel" },
|
||||
];
|
||||
}
|
||||
|
||||
@query("ak-select-table")
|
||||
selectTable!: SelectTable;
|
||||
|
||||
static get styles() {
|
||||
return super.styles.concat(
|
||||
PFCard,
|
||||
css`
|
||||
.pf-c-card {
|
||||
margin-top: 1em;
|
||||
}
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
get bindingsAsColumns() {
|
||||
return this.wizard.bindings.map((binding, index) => {
|
||||
const { order, enabled, timeout } = binding;
|
||||
const isSet = P.string.minLength(1);
|
||||
const policy = match(binding)
|
||||
.with({ policy: isSet }, (v) => msg(str`Policy ${v.policyObj?.name}`))
|
||||
.with({ group: isSet }, (v) => msg(str`Group ${v.groupObj?.name}`))
|
||||
.with({ user: isSet }, (v) => msg(str`User ${v.userObj?.name}`))
|
||||
.otherwise(() => msg("-"));
|
||||
|
||||
return {
|
||||
key: index,
|
||||
content: [
|
||||
order,
|
||||
policy,
|
||||
html`<ak-status-label type="warning" ?good=${enabled}></ak-status-label>`,
|
||||
timeout,
|
||||
makeEditButton(msg("Edit"), index, (ev: CustomEvent<number>) =>
|
||||
this.onBindingEvent(ev.detail),
|
||||
),
|
||||
],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// TODO Fix those dispatches so that we handle them here, in this component, and *choose* how to
|
||||
// forward them.
|
||||
onBindingEvent(binding?: number) {
|
||||
this.handleUpdate({ currentBinding: binding ?? -1 }, "edit-binding", {
|
||||
enable: "edit-binding",
|
||||
});
|
||||
}
|
||||
|
||||
onDeleteBindings() {
|
||||
const toDelete = this.selectTable
|
||||
.json()
|
||||
.map((i) => (typeof i === "string" ? parseInt(i, 10) : i));
|
||||
const bindings = this.wizard.bindings.filter((binding, index) => !toDelete.includes(index));
|
||||
this.handleUpdate({ bindings }, "bindings");
|
||||
}
|
||||
|
||||
renderEmptyCollection() {
|
||||
return html`<ak-wizard-title
|
||||
>${msg("Configure Policy/User/Group Bindings")}</ak-wizard-title
|
||||
>
|
||||
<h6 class="pf-c-title pf-m-md">
|
||||
${msg("These policies control which users can access this application.")}
|
||||
</h6>
|
||||
<div class="pf-c-card">
|
||||
<ak-application-wizard-bindings-toolbar
|
||||
@clickNew=${() => this.onBindingEvent()}
|
||||
@clickDelete=${() => this.onDeleteBindings()}
|
||||
></ak-application-wizard-bindings-toolbar>
|
||||
<ak-select-table
|
||||
multiple
|
||||
id="bindings"
|
||||
order="order"
|
||||
.columns=${COLUMNS}
|
||||
.content=${[]}
|
||||
></ak-select-table>
|
||||
<ak-empty-state header=${msg("No bound policies.")} icon="pf-icon-module">
|
||||
<div slot="body">${msg("No policies are currently bound to this object.")}</div>
|
||||
<div slot="primary">
|
||||
<button
|
||||
@click=${() => this.onBindingEvent()}
|
||||
class="pf-c-button pf-m-primary"
|
||||
>
|
||||
${msg("Bind policy/group/user")}
|
||||
</button>
|
||||
</div>
|
||||
</ak-empty-state>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
renderCollection() {
|
||||
return html` <ak-wizard-title>${msg("Configure Policy Bindings")}</ak-wizard-title>
|
||||
<h6 class="pf-c-title pf-m-md">
|
||||
${msg("These policies control which users can access this application.")}
|
||||
</h6>
|
||||
<ak-application-wizard-bindings-toolbar
|
||||
@clickNew=${() => this.onBindingEvent()}
|
||||
@clickDelete=${() => this.onDeleteBindings()}
|
||||
?can-delete=${this.wizard.bindings.length > 0}
|
||||
></ak-application-wizard-bindings-toolbar>
|
||||
<ak-select-table
|
||||
multiple
|
||||
id="bindings"
|
||||
order="order"
|
||||
.columns=${COLUMNS}
|
||||
.content=${this.bindingsAsColumns}
|
||||
></ak-select-table>`;
|
||||
}
|
||||
|
||||
renderMain() {
|
||||
if ((this.wizard.bindings ?? []).length === 0) {
|
||||
return this.renderEmptyCollection();
|
||||
}
|
||||
return this.renderCollection();
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-application-wizard-applications-step": ApplicationWizardBindingsStep;
|
||||
}
|
||||
}
|
@ -0,0 +1,235 @@
|
||||
import { ApplicationWizardStep } from "@goauthentik/admin/applications/wizard/ApplicationWizardStep.js";
|
||||
import "@goauthentik/admin/applications/wizard/ak-wizard-title.js";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { groupBy } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/components/ak-radio-input";
|
||||
import "@goauthentik/components/ak-switch-input";
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
import "@goauthentik/components/ak-toggle-group";
|
||||
import { type NavigableButton, type WizardButton } from "@goauthentik/components/ak-wizard/types";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
import { type SearchSelectBase } from "@goauthentik/elements/forms/SearchSelect/SearchSelect.js";
|
||||
import "@goauthentik/elements/forms/SearchSelect/ak-search-select-ez.js";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html, nothing } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators.js";
|
||||
|
||||
import { CoreApi, Group, PoliciesApi, Policy, PolicyBinding, User } from "@goauthentik/api";
|
||||
|
||||
const withQuery = <T>(search: string | undefined, args: T) => (search ? { ...args, search } : args);
|
||||
|
||||
enum target {
|
||||
policy = "policy",
|
||||
group = "group",
|
||||
user = "user",
|
||||
}
|
||||
|
||||
const policyObjectKeys: Record<target, keyof PolicyBinding> = {
|
||||
[target.policy]: "policyObj",
|
||||
[target.group]: "groupObj",
|
||||
[target.user]: "userObj",
|
||||
};
|
||||
|
||||
const PASS_FAIL = [
|
||||
[msg("Pass"), true, false],
|
||||
[msg("Don't Pass"), false, true],
|
||||
].map(([label, value, d]) => ({ label, value, default: d }));
|
||||
|
||||
@customElement("ak-application-wizard-edit-binding-step")
|
||||
export class ApplicationWizardEditBindingStep extends ApplicationWizardStep {
|
||||
label = msg("Edit Binding");
|
||||
|
||||
hide = true;
|
||||
|
||||
@query("form#bindingform")
|
||||
form!: HTMLFormElement;
|
||||
|
||||
@query(".policy-search-select")
|
||||
searchSelect!: SearchSelectBase<Policy> | SearchSelectBase<Group> | SearchSelectBase<User>;
|
||||
|
||||
@state()
|
||||
policyGroupUser: target = target.policy;
|
||||
|
||||
instanceId = -1;
|
||||
|
||||
instance?: PolicyBinding;
|
||||
|
||||
get buttons(): WizardButton[] {
|
||||
return [
|
||||
{ kind: "next", label: msg("Save Binding"), destination: "bindings" },
|
||||
{ kind: "back", destination: "bindings" },
|
||||
{ kind: "cancel" },
|
||||
];
|
||||
}
|
||||
|
||||
override handleButton(button: NavigableButton) {
|
||||
if (button.kind === "next") {
|
||||
if (!this.form.checkValidity()) {
|
||||
return;
|
||||
}
|
||||
const policyObject = this.searchSelect.selectedObject;
|
||||
const policyKey = policyObjectKeys[this.policyGroupUser];
|
||||
const newBinding: PolicyBinding = {
|
||||
...(this.formValues as unknown as PolicyBinding),
|
||||
[policyKey]: policyObject,
|
||||
};
|
||||
|
||||
const bindings = [...(this.wizard.bindings ?? [])];
|
||||
|
||||
if (this.instanceId === -1) {
|
||||
bindings.push(newBinding);
|
||||
} else {
|
||||
bindings[this.instanceId] = newBinding;
|
||||
}
|
||||
|
||||
this.instanceId = -1;
|
||||
this.handleUpdate({ bindings }, "bindings");
|
||||
return;
|
||||
}
|
||||
super.handleButton(button);
|
||||
}
|
||||
|
||||
// The search select configurations for the three different types of fetches that we care about,
|
||||
// policy, user, and group, all using the SearchSelectEZ protocol.
|
||||
searchSelectConfigs(kind: target) {
|
||||
switch (kind) {
|
||||
case target.policy:
|
||||
return {
|
||||
fetchObjects: async (query?: string): Promise<Policy[]> => {
|
||||
const policies = await new PoliciesApi(DEFAULT_CONFIG).policiesAllList(
|
||||
withQuery(query, {
|
||||
ordering: "name",
|
||||
}),
|
||||
);
|
||||
return policies.results;
|
||||
},
|
||||
groupBy: (items: Policy[]) =>
|
||||
groupBy(items, (policy) => policy.verboseNamePlural),
|
||||
renderElement: (policy: Policy): string => policy.name,
|
||||
value: (policy: Policy | undefined): string | undefined => policy?.pk,
|
||||
selected: (policy: Policy): boolean => policy.pk === this.instance?.policy,
|
||||
};
|
||||
case target.group:
|
||||
return {
|
||||
fetchObjects: async (query?: string): Promise<Group[]> => {
|
||||
const groups = await new CoreApi(DEFAULT_CONFIG).coreGroupsList(
|
||||
withQuery(query, {
|
||||
ordering: "name",
|
||||
includeUsers: false,
|
||||
}),
|
||||
);
|
||||
return groups.results;
|
||||
},
|
||||
renderElement: (group: Group): string => group.name,
|
||||
value: (group: Group | undefined): string | undefined => group?.pk,
|
||||
selected: (group: Group): boolean => group.pk === this.instance?.group,
|
||||
};
|
||||
case target.user:
|
||||
return {
|
||||
fetchObjects: async (query?: string): Promise<User[]> => {
|
||||
const users = await new CoreApi(DEFAULT_CONFIG).coreUsersList(
|
||||
withQuery(query, {
|
||||
ordering: "username",
|
||||
}),
|
||||
);
|
||||
return users.results;
|
||||
},
|
||||
renderElement: (user: User): string => user.username,
|
||||
renderDescription: (user: User) => html`${user.name}`,
|
||||
value: (user: User | undefined): number | undefined => user?.pk,
|
||||
selected: (user: User): boolean => user.pk === this.instance?.user,
|
||||
};
|
||||
default:
|
||||
throw new Error(`Unrecognized policy binding target ${kind}`);
|
||||
}
|
||||
}
|
||||
|
||||
renderSearch(title: string, policyKind: target) {
|
||||
if (policyKind !== this.policyGroupUser) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`<ak-form-element-horizontal label=${title} name=${policyKind}>
|
||||
<ak-search-select-ez
|
||||
.config=${this.searchSelectConfigs(policyKind)}
|
||||
class="policy-search-select"
|
||||
blankable
|
||||
></ak-search-select-ez>
|
||||
</ak-form-element-horizontal>`;
|
||||
}
|
||||
|
||||
renderForm(instance?: PolicyBinding) {
|
||||
return html`<ak-wizard-title>${msg("Create a Policy/User/Group Binding")}</ak-wizard-title>
|
||||
<form id="bindingform" class="pf-c-form pf-m-horizontal" slot="form">
|
||||
<div class="pf-c-card pf-m-selectable pf-m-selected">
|
||||
<div class="pf-c-card__body">
|
||||
<ak-toggle-group
|
||||
value=${this.policyGroupUser}
|
||||
@ak-toggle=${(ev: CustomEvent<{ value: target }>) => {
|
||||
this.policyGroupUser = ev.detail.value;
|
||||
}}
|
||||
>
|
||||
<option value=${target.policy}>${msg("Policy")}</option>
|
||||
<option value=${target.group}>${msg("Group")}</option>
|
||||
<option value=${target.user}>${msg("User")}</option>
|
||||
</ak-toggle-group>
|
||||
</div>
|
||||
<div class="pf-c-card__footer">
|
||||
${this.renderSearch(msg("Policy"), target.policy)}
|
||||
${this.renderSearch(msg("Group"), target.group)}
|
||||
${this.renderSearch(msg("User"), target.user)}
|
||||
</div>
|
||||
</div>
|
||||
<ak-switch-input
|
||||
name="enabled"
|
||||
?checked=${instance?.enabled ?? true}
|
||||
label=${msg("Enabled")}
|
||||
></ak-switch-input>
|
||||
<ak-switch-input
|
||||
name="negate"
|
||||
?checked=${instance?.negate ?? false}
|
||||
label=${msg("Negate result")}
|
||||
help=${msg("Negates the outcome of the binding. Messages are unaffected.")}
|
||||
></ak-switch-input>
|
||||
<ak-number-input
|
||||
label=${msg("Order")}
|
||||
name="order"
|
||||
value="${instance?.order ?? 0}"
|
||||
required
|
||||
></ak-number-input>
|
||||
<ak-number-input
|
||||
label=${msg("Timeout")}
|
||||
name="timeout"
|
||||
value="${instance?.timeout ?? 30}"
|
||||
required
|
||||
></ak-number-input>
|
||||
<ak-radio-input
|
||||
name="failureResult"
|
||||
label=${msg("Failure result")}
|
||||
.options=${PASS_FAIL}
|
||||
></ak-radio-input>
|
||||
</form>`;
|
||||
}
|
||||
|
||||
renderMain() {
|
||||
if (!(this.wizard.bindings && this.wizard.errors)) {
|
||||
throw new Error("Application Step received uninitialized wizard context.");
|
||||
}
|
||||
const currentBinding = this.wizard.currentBinding ?? -1;
|
||||
if (this.instanceId !== currentBinding) {
|
||||
this.instanceId = currentBinding;
|
||||
this.instance =
|
||||
this.instanceId === -1 ? undefined : this.wizard.bindings[this.instanceId];
|
||||
}
|
||||
return this.renderForm(this.instance);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-application-wizard-edit-binding-step": ApplicationWizardEditBindingStep;
|
||||
}
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
import { ApplicationWizardStep } from "@goauthentik/admin/applications/wizard/ApplicationWizardStep.js";
|
||||
import "@goauthentik/admin/applications/wizard/ak-wizard-title.js";
|
||||
import type { NavigableButton, WizardButton } from "@goauthentik/components/ak-wizard/types";
|
||||
import "@goauthentik/elements/EmptyState.js";
|
||||
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider.js";
|
||||
import { bound } from "@goauthentik/elements/decorators/bound.js";
|
||||
import "@goauthentik/elements/forms/FormGroup.js";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement.js";
|
||||
import { TypeCreateWizardPageLayouts } from "@goauthentik/elements/wizard/TypeCreateWizardPage.js";
|
||||
import "@goauthentik/elements/wizard/TypeCreateWizardPage.js";
|
||||
|
||||
import { consume } from "@lit/context";
|
||||
import { msg } from "@lit/localize";
|
||||
import { html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
|
||||
import { TypeCreate } from "@goauthentik/api";
|
||||
|
||||
import { applicationWizardProvidersContext } from "../ContextIdentity";
|
||||
import { type LocalTypeCreate } from "./ProviderChoices.js";
|
||||
|
||||
@customElement("ak-application-wizard-provider-choice-step")
|
||||
export class ApplicationWizardProviderChoiceStep extends WithLicenseSummary(ApplicationWizardStep) {
|
||||
label = msg("Choose A Provider");
|
||||
|
||||
@state()
|
||||
failureMessage = "";
|
||||
|
||||
@consume({ context: applicationWizardProvidersContext, subscribe: true })
|
||||
public providerModelsList!: LocalTypeCreate[];
|
||||
|
||||
get buttons(): WizardButton[] {
|
||||
return [
|
||||
{ kind: "next", destination: "provider" },
|
||||
{ kind: "back", destination: "application" },
|
||||
{ kind: "cancel" },
|
||||
];
|
||||
}
|
||||
|
||||
override handleButton(button: NavigableButton) {
|
||||
this.failureMessage = "";
|
||||
if (button.kind === "next") {
|
||||
if (!this.wizard.providerModel) {
|
||||
this.failureMessage = msg("Please choose a provider type before proceeding.");
|
||||
this.handleEnabling({ disabled: ["provider", "bindings", "submit"] });
|
||||
return;
|
||||
}
|
||||
this.handleUpdate(undefined, button.destination, { enable: "provider" });
|
||||
return;
|
||||
}
|
||||
super.handleButton(button);
|
||||
}
|
||||
|
||||
@bound
|
||||
onSelect(ev: CustomEvent<LocalTypeCreate>) {
|
||||
ev.stopPropagation();
|
||||
const detail: TypeCreate = ev.detail;
|
||||
this.handleUpdate({ providerModel: detail.modelName });
|
||||
}
|
||||
|
||||
renderMain() {
|
||||
const selectedTypes = this.providerModelsList.filter(
|
||||
(t) => t.modelName === this.wizard.providerModel,
|
||||
);
|
||||
|
||||
return this.providerModelsList.length > 0
|
||||
? html` <ak-wizard-title>${msg("Choose a Provider Type")}</ak-wizard-title>
|
||||
<form class="pf-c-form pf-m-horizontal">
|
||||
<ak-wizard-page-type-create
|
||||
.types=${this.providerModelsList}
|
||||
name="selectProviderType"
|
||||
layout=${TypeCreateWizardPageLayouts.grid}
|
||||
.selectedType=${selectedTypes.length > 0 ? selectedTypes[0] : undefined}
|
||||
@select=${(ev: CustomEvent<LocalTypeCreate>) => {
|
||||
this.handleUpdate(
|
||||
{
|
||||
...this.wizard,
|
||||
providerModel: ev.detail.modelName,
|
||||
},
|
||||
undefined,
|
||||
{ enable: "provider" },
|
||||
);
|
||||
}}
|
||||
></ak-wizard-page-type-create>
|
||||
</form>`
|
||||
: html`<ak-empty-state loading header=${msg("Loading")}></ak-empty-state>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-application-wizard-provider-choice-step": ApplicationWizardProviderChoiceStep;
|
||||
}
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
import { type NavigableButton, type WizardButton } from "@goauthentik/components/ak-wizard/types";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { PropertyValues, nothing } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators.js";
|
||||
import { html, unsafeStatic } from "lit/static-html.js";
|
||||
|
||||
import { ApplicationWizardStep } from "../ApplicationWizardStep.js";
|
||||
import { OneOfProvider } from "../types.js";
|
||||
import { ApplicationWizardProviderForm } from "./providers/ApplicationWizardProviderForm.js";
|
||||
import "./providers/ak-application-wizard-provider-for-ldap.js";
|
||||
import "./providers/ak-application-wizard-provider-for-oauth.js";
|
||||
import "./providers/ak-application-wizard-provider-for-proxy.js";
|
||||
import "./providers/ak-application-wizard-provider-for-rac.js";
|
||||
import "./providers/ak-application-wizard-provider-for-radius.js";
|
||||
import "./providers/ak-application-wizard-provider-for-saml.js";
|
||||
import "./providers/ak-application-wizard-provider-for-scim.js";
|
||||
|
||||
const providerToTag = new Map([
|
||||
["ldapprovider", "ak-application-wizard-provider-for-ldap"],
|
||||
["oauth2provider", "ak-application-wizard-provider-for-oauth"],
|
||||
["proxyprovider", "ak-application-wizard-provider-for-proxy"],
|
||||
["racprovider", "ak-application-wizard-provider-for-rac"],
|
||||
["radiusprovider", "ak-application-wizard-provider-for-radius"],
|
||||
["samlprovider", "ak-application-wizard-provider-for-saml"],
|
||||
["scimprovider", "ak-application-wizard-provider-for-scim"],
|
||||
]);
|
||||
|
||||
@customElement("ak-application-wizard-provider-step")
|
||||
export class ApplicationWizardProviderStep extends ApplicationWizardStep {
|
||||
@state()
|
||||
label = msg("Configure Provider");
|
||||
|
||||
@query("#providerform")
|
||||
element!: ApplicationWizardProviderForm<OneOfProvider>;
|
||||
|
||||
get valid() {
|
||||
return this.element.valid;
|
||||
}
|
||||
|
||||
get formValues() {
|
||||
return this.element.formValues;
|
||||
}
|
||||
|
||||
override handleButton(button: NavigableButton) {
|
||||
if (button.kind === "next") {
|
||||
if (!this.valid) {
|
||||
this.handleEnabling({
|
||||
disabled: ["bindings", "submit"],
|
||||
});
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
provider: {
|
||||
...this.formValues,
|
||||
mode: this.wizard.proxyMode,
|
||||
},
|
||||
errors: this.removeErrors("provider"),
|
||||
};
|
||||
this.handleUpdate(payload, button.destination, {
|
||||
enable: ["bindings", "submit"],
|
||||
});
|
||||
return;
|
||||
}
|
||||
super.handleButton(button);
|
||||
}
|
||||
|
||||
get buttons(): WizardButton[] {
|
||||
return [
|
||||
{ kind: "next", destination: "bindings" },
|
||||
{ kind: "back", destination: "provider-choice" },
|
||||
{ kind: "cancel" },
|
||||
];
|
||||
}
|
||||
|
||||
renderMain() {
|
||||
if (!this.wizard.providerModel) {
|
||||
throw new Error("Attempted to access provider page without providing a provider type.");
|
||||
}
|
||||
|
||||
// This is, I'm afraid, some rather esoteric bit of Lit-ing, and it makes ESLint
|
||||
// sad. It does allow us to get away with specifying very little about the
|
||||
// provider here.
|
||||
const tag = providerToTag.get(this.wizard.providerModel);
|
||||
return tag
|
||||
? // eslint-disable-next-line lit/binding-positions,lit/no-invalid-html
|
||||
html`<${unsafeStatic(tag)}
|
||||
id="providerform"
|
||||
.wizard=${this.wizard}
|
||||
.errors=${this.wizard.errors?.provider ?? {}}
|
||||
|
||||
></${
|
||||
/* eslint-disable-next-line lit/binding-positions,lit/no-invalid-html */
|
||||
unsafeStatic(tag)
|
||||
}>`
|
||||
: nothing;
|
||||
}
|
||||
|
||||
updated(changed: PropertyValues<this>) {
|
||||
if (changed.has("wizard")) {
|
||||
const label = this.element?.label ?? this.label;
|
||||
if (label !== this.label) {
|
||||
this.label = label;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-application-wizard-provider-step": ApplicationWizardProviderStep;
|
||||
}
|
||||
}
|
@ -0,0 +1,360 @@
|
||||
import "@goauthentik/admin/applications/wizard/ak-wizard-title.js";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||
import { parseAPIError } from "@goauthentik/common/errors";
|
||||
import { WizardNavigationEvent } from "@goauthentik/components/ak-wizard/events.js";
|
||||
import { type WizardButton } from "@goauthentik/components/ak-wizard/types";
|
||||
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||
import { P, match } from "ts-pattern";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, css, html, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { classMap } from "lit/directives/class-map.js";
|
||||
|
||||
// import { map } from "lit/directives/map.js";
|
||||
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
|
||||
import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css";
|
||||
import PFProgressStepper from "@patternfly/patternfly/components/ProgressStepper/progress-stepper.css";
|
||||
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
||||
import PFBullseye from "@patternfly/patternfly/layouts/Bullseye/bullseye.css";
|
||||
|
||||
import {
|
||||
type ApplicationRequest,
|
||||
CoreApi,
|
||||
type ModelRequest,
|
||||
type PolicyBinding,
|
||||
ProviderModelEnum,
|
||||
ProxyMode,
|
||||
type ProxyProviderRequest,
|
||||
type TransactionApplicationRequest,
|
||||
type TransactionApplicationResponse,
|
||||
type TransactionPolicyBindingRequest,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import { ApplicationWizardStep } from "../ApplicationWizardStep.js";
|
||||
import { ExtendedValidationError, OneOfProvider } from "../types.js";
|
||||
import { providerRenderers } from "./SubmitStepOverviewRenderers.js";
|
||||
|
||||
const _submitStates = ["reviewing", "running", "submitted"] as const;
|
||||
type SubmitStates = (typeof _submitStates)[number];
|
||||
|
||||
type StrictProviderModelEnum = Exclude<ProviderModelEnum, "11184809">;
|
||||
|
||||
const providerMap: Map<string, string> = Object.values(ProviderModelEnum)
|
||||
.filter((value) => /^authentik_providers_/.test(value) && /provider$/.test(value))
|
||||
.reduce((acc: Map<string, string>, value) => {
|
||||
acc.set(value.split(".")[1], value);
|
||||
return acc;
|
||||
}, new Map());
|
||||
|
||||
type NonEmptyArray<T> = [T, ...T[]];
|
||||
|
||||
type MaybeTemplateResult = TemplateResult | typeof nothing;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const isNotEmpty = (arr: any): arr is NonEmptyArray<any> => Array.isArray(arr) && arr.length > 0;
|
||||
|
||||
const cleanApplication = (app: Partial<ApplicationRequest>): ApplicationRequest => ({
|
||||
name: "",
|
||||
slug: "",
|
||||
...app,
|
||||
});
|
||||
|
||||
const cleanBinding = (binding: PolicyBinding): TransactionPolicyBindingRequest => ({
|
||||
policy: binding.policy,
|
||||
group: binding.group,
|
||||
user: binding.user,
|
||||
negate: binding.negate,
|
||||
enabled: binding.enabled,
|
||||
order: binding.order,
|
||||
timeout: binding.timeout,
|
||||
failureResult: binding.failureResult,
|
||||
});
|
||||
|
||||
@customElement("ak-application-wizard-submit-step")
|
||||
export class ApplicationWizardSubmitStep extends CustomEmitterElement(ApplicationWizardStep) {
|
||||
static get styles() {
|
||||
return [
|
||||
...ApplicationWizardStep.styles,
|
||||
PFBullseye,
|
||||
PFEmptyState,
|
||||
PFTitle,
|
||||
PFProgressStepper,
|
||||
PFDescriptionList,
|
||||
css`
|
||||
.ak-wizard-main-content .pf-c-title {
|
||||
padding-bottom: var(--pf-global--spacer--md);
|
||||
padding-top: var(--pf-global--spacer--md);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
label = msg("Review and Submit Application");
|
||||
|
||||
@state()
|
||||
state: SubmitStates = "reviewing";
|
||||
|
||||
async send() {
|
||||
const app = this.wizard.app;
|
||||
const provider = this.wizard.provider as ModelRequest;
|
||||
|
||||
if (app === undefined) {
|
||||
throw new Error("Reached the submit state with the app undefined");
|
||||
}
|
||||
|
||||
if (provider === undefined) {
|
||||
throw new Error("Reached the submit state with the provider undefined");
|
||||
}
|
||||
|
||||
// Stringly-based API. Not the best, but it works. Just be aware that it is
|
||||
// stringly-based.
|
||||
|
||||
const providerModel = providerMap.get(this.wizard.providerModel) as StrictProviderModelEnum;
|
||||
provider.providerModel = providerModel;
|
||||
|
||||
// Special case for the Proxy provider.
|
||||
if (this.wizard.providerModel === "proxyprovider") {
|
||||
(provider as ProxyProviderRequest).mode = this.wizard.proxyMode;
|
||||
if ((provider as ProxyProviderRequest).mode !== ProxyMode.ForwardDomain) {
|
||||
(provider as ProxyProviderRequest).cookieDomain = "";
|
||||
}
|
||||
}
|
||||
|
||||
const request: TransactionApplicationRequest = {
|
||||
app: cleanApplication(this.wizard.app),
|
||||
providerModel,
|
||||
provider,
|
||||
policyBindings: (this.wizard.bindings ?? []).map(cleanBinding),
|
||||
};
|
||||
|
||||
this.state = "running";
|
||||
|
||||
return (
|
||||
new CoreApi(DEFAULT_CONFIG)
|
||||
.coreTransactionalApplicationsUpdate({
|
||||
transactionApplicationRequest: request,
|
||||
})
|
||||
.then((_response: TransactionApplicationResponse) => {
|
||||
this.dispatchCustomEvent(EVENT_REFRESH);
|
||||
this.state = "submitted";
|
||||
})
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
.catch(async (resolution: any) => {
|
||||
const errors = (await parseAPIError(
|
||||
await resolution,
|
||||
)) as ExtendedValidationError;
|
||||
|
||||
// THIS is a really gross special case; if the user is duplicating the name of
|
||||
// an existing provider, the error appears on the `app` (!) error object. We
|
||||
// have to move that to the `provider.name` error field so it shows up in the
|
||||
// right place.
|
||||
if (Array.isArray(errors?.app?.provider)) {
|
||||
const providerError = errors.app.provider;
|
||||
errors.provider = errors.provider ?? {};
|
||||
errors.provider.name = providerError;
|
||||
delete errors.app.provider;
|
||||
if (Object.keys(errors.app).length === 0) {
|
||||
delete errors.app;
|
||||
}
|
||||
}
|
||||
this.handleUpdate({ errors });
|
||||
this.state = "reviewing";
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override handleButton(button: WizardButton) {
|
||||
match([button.kind, this.state])
|
||||
.with([P.union("back", "cancel"), P._], () => {
|
||||
super.handleButton(button);
|
||||
})
|
||||
.with(["close", "submitted"], () => {
|
||||
super.handleButton(button);
|
||||
})
|
||||
.with(["next", "reviewing"], () => {
|
||||
this.send();
|
||||
})
|
||||
.with([P._, "running"], () => {
|
||||
throw new Error("No buttons should be showing when running submit phase");
|
||||
})
|
||||
.otherwise(() => {
|
||||
throw new Error(
|
||||
`Submit step received incoherent button/state combination: ${[button.kind, state]}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
get buttons(): WizardButton[] {
|
||||
const forReview: WizardButton[] = [
|
||||
{ kind: "next", label: msg("Submit"), destination: "here" },
|
||||
{ kind: "back", destination: "bindings" },
|
||||
{ kind: "cancel" },
|
||||
];
|
||||
|
||||
const forSubmit: WizardButton[] = [{ kind: "close" }];
|
||||
|
||||
return match(this.state)
|
||||
.with("submitted", () => forSubmit)
|
||||
.with("running", () => [])
|
||||
.with("reviewing", () => forReview)
|
||||
.exhaustive();
|
||||
}
|
||||
|
||||
renderInfo(
|
||||
state: string,
|
||||
label: string,
|
||||
icons: string[],
|
||||
extraInfo: MaybeTemplateResult = nothing,
|
||||
) {
|
||||
const icon = classMap(icons.reduce((acc, icon) => ({ ...acc, [icon]: true }), {}));
|
||||
|
||||
return html`<div data-ouid-component-state=${this.state} class="ak-wizard-main-content">
|
||||
<div class="pf-l-bullseye">
|
||||
<div class="pf-c-empty-state pf-m-lg">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas ${icon} pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 data-ouia-commit-state=${state} class="pf-c-title pf-m-lg">${label}</h1>
|
||||
${extraInfo}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
renderError() {
|
||||
if (Object.keys(this.wizard.errors).length === 0) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const navTo = (step: string) => () => this.dispatchEvent(new WizardNavigationEvent(step));
|
||||
const errors = this.wizard.errors;
|
||||
return html` <hr class="pf-c-divider" />
|
||||
${match(errors as ExtendedValidationError)
|
||||
.with(
|
||||
{ app: P.nonNullable },
|
||||
() =>
|
||||
html`<p>${msg("There was an error in the application.")}</p>
|
||||
<p>
|
||||
<a @click=${navTo("application")}
|
||||
>${msg("Review the application.")}</a
|
||||
>
|
||||
</p>`,
|
||||
)
|
||||
.with(
|
||||
{ provider: P.nonNullable },
|
||||
() =>
|
||||
html`<p>${msg("There was an error in the provider.")}</p>
|
||||
<p>
|
||||
<a @click=${navTo("provider")}>${msg("Review the provider.")}</a>
|
||||
</p>`,
|
||||
)
|
||||
.with(
|
||||
{ detail: P.nonNullable },
|
||||
() =>
|
||||
`<p>${msg("There was an error. Please go back and review the application.")}: ${errors.detail}</p>`,
|
||||
)
|
||||
.with(
|
||||
{
|
||||
nonFieldErrors: P.when(isNotEmpty),
|
||||
},
|
||||
() =>
|
||||
html`<p>${msg("There was an error:")}:</p>
|
||||
<ul>
|
||||
${(errors.nonFieldErrors ?? []).map(
|
||||
(e: string) => html`<li>${e}</li>`,
|
||||
)}
|
||||
</ul>
|
||||
<p>${msg("Please go back and review the application.")}</p>`,
|
||||
)
|
||||
.otherwise(
|
||||
() =>
|
||||
html`<p>
|
||||
${msg(
|
||||
"There was an error creating the application, but no error message was sent. Please review the server logs.",
|
||||
)}
|
||||
</p>`,
|
||||
)}`;
|
||||
}
|
||||
|
||||
renderReview(app: Partial<ApplicationRequest>, provider: OneOfProvider) {
|
||||
const renderer = providerRenderers.get(this.wizard.providerModel);
|
||||
if (!renderer) {
|
||||
throw new Error(
|
||||
`Provider ${this.wizard.providerModel ?? "-- undefined --"} has no summary renderer.`,
|
||||
);
|
||||
}
|
||||
return html`
|
||||
<div class="ak-wizard-main-content">
|
||||
<ak-wizard-title>${msg("Review the Application and Provider")}</ak-wizard-title>
|
||||
<h2 class="pf-c-title pf-m-xl">${msg("Application")}</h2>
|
||||
<dl class="pf-c-description-list">
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">${msg("Name")}</dt>
|
||||
<dt class="pf-c-description-list__description">${app.name}</dt>
|
||||
</div>
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">${msg("Group")}</dt>
|
||||
<dt class="pf-c-description-list__description">${app.group || msg("-")}</dt>
|
||||
</div>
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">${msg("Policy engine mode")}</dt>
|
||||
<dt class="pf-c-description-list__description">
|
||||
${app.policyEngineMode?.toUpperCase()}
|
||||
</dt>
|
||||
</div>
|
||||
${(app.metaLaunchUrl ?? "").trim() !== ""
|
||||
? html` <div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">${msg("Launch URL")}</dt>
|
||||
<dt class="pf-c-description-list__description">
|
||||
${app.metaLaunchUrl}
|
||||
</dt>
|
||||
</div>`
|
||||
: nothing}
|
||||
</dl>
|
||||
${renderer
|
||||
? html` <h2 class="pf-c-title pf-m-xl pf-u-pt-xl">${msg("Provider")}</h2>
|
||||
${renderer(provider)}`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
renderMain() {
|
||||
const app = this.wizard.app;
|
||||
const provider = this.wizard.provider;
|
||||
if (!(this.wizard && app && provider)) {
|
||||
throw new Error("Submit step received uninitialized wizard context");
|
||||
}
|
||||
// An empty object is truthy, an empty array is falsey. *WAT Javascript*.
|
||||
const keys = Object.keys(this.wizard.errors);
|
||||
return match([this.state, keys])
|
||||
.with(["submitted", P._], () =>
|
||||
this.renderInfo("success", msg("Your application has been saved"), [
|
||||
"fa-check-circle",
|
||||
"pf-m-success",
|
||||
]),
|
||||
)
|
||||
.with(["running", P._], () =>
|
||||
this.renderInfo("running", msg("Saving application..."), ["fa-cogs", "pf-m-info"]),
|
||||
)
|
||||
.with(["reviewing", []], () => this.renderReview(app, provider))
|
||||
.with(["reviewing", [P.any, ...P.array()]], () =>
|
||||
this.renderInfo(
|
||||
"error",
|
||||
msg("authentik was unable to complete this process."),
|
||||
["fa-times-circle", "pf-m-danger"],
|
||||
this.renderError(),
|
||||
),
|
||||
)
|
||||
.exhaustive();
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-application-wizard-submit-step": ApplicationWizardSubmitStep;
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
import { AKElement } from "@goauthentik/elements/Base.js";
|
||||
import { bound } from "@goauthentik/elements/decorators/bound.js";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
|
||||
@customElement("ak-application-wizard-binding-step-edit-button")
|
||||
export class ApplicationWizardBindingStepEditButton extends AKElement {
|
||||
static get styles() {
|
||||
return [PFButton];
|
||||
}
|
||||
|
||||
@property({ type: Number })
|
||||
value = -1;
|
||||
|
||||
@bound
|
||||
onClick(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this.dispatchEvent(
|
||||
new CustomEvent<number>("click-edit", {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: this.value,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<button class="pf-c-button pf-c-secondary" @click=${this.onClick}>
|
||||
${msg("Edit")}
|
||||
</button>`;
|
||||
}
|
||||
}
|
||||
|
||||
export function makeEditButton(
|
||||
label: string,
|
||||
value: number,
|
||||
handler: (_: CustomEvent<number>) => void,
|
||||
) {
|
||||
return html`<ak-application-wizard-binding-step-edit-button
|
||||
class="pf-c-button pf-m-secondary"
|
||||
.value=${value}
|
||||
@click-edit=${handler}
|
||||
>
|
||||
${label}
|
||||
</ak-application-wizard-binding-step-edit-button>`;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user