Compare commits
20 Commits
linter-fix
...
sfe-packag
Author | SHA1 | Date | |
---|---|---|---|
a9373d60d0 | |||
82fadf587b | |||
220378b3f2 | |||
363d655378 | |||
e93b2a1a75 | |||
76665cf65e | |||
3ad7f4dc24 | |||
c5045e8792 | |||
a8c9b3a8ba | |||
148506639a | |||
53814d9919 | |||
08b04c32f5 | |||
1c1d97339d | |||
cafa9c1737 | |||
5f64347ba1 | |||
45ef54480a | |||
a3dc8af4c6 | |||
36933a0aca | |||
8f689890df | |||
ec49b2e0e0 |
5
.github/workflows/api-ts-publish.yml
vendored
5
.github/workflows/api-ts-publish.yml
vendored
@ -36,11 +36,6 @@ jobs:
|
||||
run: |
|
||||
export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'`
|
||||
npm i @goauthentik/api@$VERSION
|
||||
- name: Upgrade /web/packages/sfe
|
||||
working-directory: web/packages/sfe
|
||||
run: |
|
||||
export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'`
|
||||
npm i @goauthentik/api@$VERSION
|
||||
- uses: peter-evans/create-pull-request@v7
|
||||
id: cpr
|
||||
with:
|
||||
|
@ -30,7 +30,6 @@ WORKDIR /work/web
|
||||
|
||||
RUN --mount=type=bind,target=/work/web/package.json,src=./web/package.json \
|
||||
--mount=type=bind,target=/work/web/package-lock.json,src=./web/package-lock.json \
|
||||
--mount=type=bind,target=/work/web/packages/sfe/package.json,src=./web/packages/sfe/package.json \
|
||||
--mount=type=bind,target=/work/web/scripts,src=./web/scripts \
|
||||
--mount=type=cache,id=npm-web,sharing=shared,target=/root/.npm \
|
||||
npm ci --include=dev
|
||||
@ -94,7 +93,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
|
||||
/bin/sh -c "/usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
|
||||
|
||||
# Stage 5: Download uv
|
||||
FROM ghcr.io/astral-sh/uv:0.6.11 AS uv
|
||||
FROM ghcr.io/astral-sh/uv:0.6.12 AS uv
|
||||
# Stage 6: Base python image
|
||||
FROM ghcr.io/goauthentik/fips-python:3.12.9-slim-bookworm-fips AS python-base
|
||||
|
||||
|
@ -179,10 +179,13 @@ class UserSourceConnectionSerializer(SourceSerializer):
|
||||
"user",
|
||||
"source",
|
||||
"source_obj",
|
||||
"identifier",
|
||||
"created",
|
||||
"last_updated",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"created": {"read_only": True},
|
||||
"last_updated": {"read_only": True},
|
||||
}
|
||||
|
||||
|
||||
@ -199,7 +202,7 @@ class UserSourceConnectionViewSet(
|
||||
queryset = UserSourceConnection.objects.all()
|
||||
serializer_class = UserSourceConnectionSerializer
|
||||
filterset_fields = ["user", "source__slug"]
|
||||
search_fields = ["source__slug"]
|
||||
search_fields = ["user__username", "source__slug", "identifier"]
|
||||
ordering = ["source__slug", "pk"]
|
||||
owner_field = "user"
|
||||
|
||||
@ -218,9 +221,11 @@ class GroupSourceConnectionSerializer(SourceSerializer):
|
||||
"source_obj",
|
||||
"identifier",
|
||||
"created",
|
||||
"last_updated",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"created": {"read_only": True},
|
||||
"last_updated": {"read_only": True},
|
||||
}
|
||||
|
||||
|
||||
@ -237,6 +242,5 @@ class GroupSourceConnectionViewSet(
|
||||
queryset = GroupSourceConnection.objects.all()
|
||||
serializer_class = GroupSourceConnectionSerializer
|
||||
filterset_fields = ["group", "source__slug"]
|
||||
search_fields = ["source__slug"]
|
||||
search_fields = ["group__name", "source__slug", "identifier"]
|
||||
ordering = ["source__slug", "pk"]
|
||||
owner_field = "user"
|
||||
|
@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.0.13 on 2025-04-07 14:04
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0043_alter_group_options"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="usersourceconnection",
|
||||
name="new_identifier",
|
||||
field=models.TextField(default=""),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
@ -0,0 +1,30 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0044_usersourceconnection_new_identifier"),
|
||||
("authentik_sources_kerberos", "0003_migrate_userkerberossourceconnection_identifier"),
|
||||
("authentik_sources_oauth", "0009_migrate_useroauthsourceconnection_identifier"),
|
||||
("authentik_sources_plex", "0005_migrate_userplexsourceconnection_identifier"),
|
||||
("authentik_sources_saml", "0019_migrate_usersamlsourceconnection_identifier"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name="usersourceconnection",
|
||||
old_name="new_identifier",
|
||||
new_name="identifier",
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="usersourceconnection",
|
||||
index=models.Index(fields=["identifier"], name="authentik_c_identif_59226f_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="usersourceconnection",
|
||||
index=models.Index(
|
||||
fields=["source", "identifier"], name="authentik_c_source__649e04_idx"
|
||||
),
|
||||
),
|
||||
]
|
@ -824,6 +824,7 @@ class UserSourceConnection(SerializerModel, CreatedUpdatedModel):
|
||||
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
source = models.ForeignKey(Source, on_delete=models.CASCADE)
|
||||
identifier = models.TextField()
|
||||
|
||||
objects = InheritanceManager()
|
||||
|
||||
@ -837,6 +838,10 @@ class UserSourceConnection(SerializerModel, CreatedUpdatedModel):
|
||||
|
||||
class Meta:
|
||||
unique_together = (("user", "source"),)
|
||||
indexes = (
|
||||
models.Index(fields=("identifier",)),
|
||||
models.Index(fields=("source", "identifier")),
|
||||
)
|
||||
|
||||
|
||||
class GroupSourceConnection(SerializerModel, CreatedUpdatedModel):
|
||||
|
@ -13,7 +13,11 @@ from authentik.core.api.devices import AdminDeviceViewSet, DeviceViewSet
|
||||
from authentik.core.api.groups import GroupViewSet
|
||||
from authentik.core.api.property_mappings import PropertyMappingViewSet
|
||||
from authentik.core.api.providers import ProviderViewSet
|
||||
from authentik.core.api.sources import SourceViewSet, UserSourceConnectionViewSet
|
||||
from authentik.core.api.sources import (
|
||||
GroupSourceConnectionViewSet,
|
||||
SourceViewSet,
|
||||
UserSourceConnectionViewSet,
|
||||
)
|
||||
from authentik.core.api.tokens import TokenViewSet
|
||||
from authentik.core.api.transactional_applications import TransactionalApplicationView
|
||||
from authentik.core.api.users import UserViewSet
|
||||
@ -81,6 +85,7 @@ api_urlpatterns = [
|
||||
("core/tokens", TokenViewSet),
|
||||
("sources/all", SourceViewSet),
|
||||
("sources/user_connections/all", UserSourceConnectionViewSet),
|
||||
("sources/group_connections/all", GroupSourceConnectionViewSet),
|
||||
("providers/all", ProviderViewSet),
|
||||
("propertymappings/all", PropertyMappingViewSet),
|
||||
("authenticators/all", DeviceViewSet, "device"),
|
||||
|
@ -49,6 +49,6 @@
|
||||
</main>
|
||||
<span class="mt-3 mb-0 text-muted text-center">{% trans 'Powered by authentik' %}</span>
|
||||
</div>
|
||||
<script src="{% static 'dist/sfe/index.js' %}"></script>
|
||||
<script src="{% static 'dist/sfe/main.js' %}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,13 +1,11 @@
|
||||
"""Kerberos Source Serializer"""
|
||||
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.sources import (
|
||||
GroupSourceConnectionSerializer,
|
||||
GroupSourceConnectionViewSet,
|
||||
UserSourceConnectionSerializer,
|
||||
UserSourceConnectionViewSet,
|
||||
)
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.sources.kerberos.models import (
|
||||
GroupKerberosSourceConnection,
|
||||
UserKerberosSourceConnection,
|
||||
@ -15,33 +13,20 @@ from authentik.sources.kerberos.models import (
|
||||
|
||||
|
||||
class UserKerberosSourceConnectionSerializer(UserSourceConnectionSerializer):
|
||||
"""Kerberos Source Serializer"""
|
||||
|
||||
class Meta:
|
||||
class Meta(UserSourceConnectionSerializer.Meta):
|
||||
model = UserKerberosSourceConnection
|
||||
fields = UserSourceConnectionSerializer.Meta.fields + ["identifier"]
|
||||
|
||||
|
||||
class UserKerberosSourceConnectionViewSet(UsedByMixin, ModelViewSet):
|
||||
"""Source Viewset"""
|
||||
|
||||
class UserKerberosSourceConnectionViewSet(UserSourceConnectionViewSet, ModelViewSet):
|
||||
queryset = UserKerberosSourceConnection.objects.all()
|
||||
serializer_class = UserKerberosSourceConnectionSerializer
|
||||
filterset_fields = ["source__slug"]
|
||||
search_fields = ["source__slug"]
|
||||
ordering = ["source__slug"]
|
||||
owner_field = "user"
|
||||
|
||||
|
||||
class GroupKerberosSourceConnectionSerializer(GroupSourceConnectionSerializer):
|
||||
"""OAuth Group-Source connection Serializer"""
|
||||
|
||||
class Meta(GroupSourceConnectionSerializer.Meta):
|
||||
model = GroupKerberosSourceConnection
|
||||
|
||||
|
||||
class GroupKerberosSourceConnectionViewSet(GroupSourceConnectionViewSet):
|
||||
"""Group-source connection Viewset"""
|
||||
|
||||
class GroupKerberosSourceConnectionViewSet(GroupSourceConnectionViewSet, ModelViewSet):
|
||||
queryset = GroupKerberosSourceConnection.objects.all()
|
||||
serializer_class = GroupKerberosSourceConnectionSerializer
|
||||
|
@ -0,0 +1,28 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def migrate_identifier(apps, schema_editor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
UserKerberosSourceConnection = apps.get_model(
|
||||
"authentik_sources_kerberos", "UserKerberosSourceConnection"
|
||||
)
|
||||
|
||||
for connection in UserKerberosSourceConnection.objects.using(db_alias).all():
|
||||
connection.new_identifier = connection.identifier
|
||||
connection.save(using=db_alias)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_sources_kerberos", "0002_kerberossource_kadmin_type"),
|
||||
("authentik_core", "0044_usersourceconnection_new_identifier"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(code=migrate_identifier, reverse_code=migrations.RunPython.noop),
|
||||
migrations.RemoveField(
|
||||
model_name="userkerberossourceconnection",
|
||||
name="identifier",
|
||||
),
|
||||
]
|
@ -372,8 +372,6 @@ class KerberosSourcePropertyMapping(PropertyMapping):
|
||||
class UserKerberosSourceConnection(UserSourceConnection):
|
||||
"""Connection to configured Kerberos Sources."""
|
||||
|
||||
identifier = models.TextField()
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
from authentik.sources.kerberos.api.source_connection import (
|
||||
|
@ -1,5 +1,3 @@
|
||||
"""OAuth Source Serializer"""
|
||||
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.sources import (
|
||||
@ -12,11 +10,9 @@ from authentik.sources.oauth.models import GroupOAuthSourceConnection, UserOAuth
|
||||
|
||||
|
||||
class UserOAuthSourceConnectionSerializer(UserSourceConnectionSerializer):
|
||||
"""OAuth Source Serializer"""
|
||||
|
||||
class Meta(UserSourceConnectionSerializer.Meta):
|
||||
model = UserOAuthSourceConnection
|
||||
fields = UserSourceConnectionSerializer.Meta.fields + ["identifier", "access_token"]
|
||||
fields = UserSourceConnectionSerializer.Meta.fields + ["access_token"]
|
||||
extra_kwargs = {
|
||||
**UserSourceConnectionSerializer.Meta.extra_kwargs,
|
||||
"access_token": {"write_only": True},
|
||||
@ -24,21 +20,15 @@ class UserOAuthSourceConnectionSerializer(UserSourceConnectionSerializer):
|
||||
|
||||
|
||||
class UserOAuthSourceConnectionViewSet(UserSourceConnectionViewSet, ModelViewSet):
|
||||
"""Source Viewset"""
|
||||
|
||||
queryset = UserOAuthSourceConnection.objects.all()
|
||||
serializer_class = UserOAuthSourceConnectionSerializer
|
||||
|
||||
|
||||
class GroupOAuthSourceConnectionSerializer(GroupSourceConnectionSerializer):
|
||||
"""OAuth Group-Source connection Serializer"""
|
||||
|
||||
class Meta(GroupSourceConnectionSerializer.Meta):
|
||||
model = GroupOAuthSourceConnection
|
||||
|
||||
|
||||
class GroupOAuthSourceConnectionViewSet(GroupSourceConnectionViewSet, ModelViewSet):
|
||||
"""Group-source connection Viewset"""
|
||||
|
||||
queryset = GroupOAuthSourceConnection.objects.all()
|
||||
serializer_class = GroupOAuthSourceConnectionSerializer
|
||||
|
@ -0,0 +1,28 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def migrate_identifier(apps, schema_editor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
UserOAuthSourceConnection = apps.get_model(
|
||||
"authentik_sources_oauth", "UserOAuthSourceConnection"
|
||||
)
|
||||
|
||||
for connection in UserOAuthSourceConnection.objects.using(db_alias).all():
|
||||
connection.new_identifier = connection.identifier
|
||||
connection.save(using=db_alias)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_sources_oauth", "0008_groupoauthsourceconnection_and_more"),
|
||||
("authentik_core", "0044_usersourceconnection_new_identifier"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(code=migrate_identifier, reverse_code=migrations.RunPython.noop),
|
||||
migrations.RemoveField(
|
||||
model_name="useroauthsourceconnection",
|
||||
name="identifier",
|
||||
),
|
||||
]
|
@ -286,7 +286,6 @@ class OAuthSourcePropertyMapping(PropertyMapping):
|
||||
class UserOAuthSourceConnection(UserSourceConnection):
|
||||
"""Authorized remote OAuth provider."""
|
||||
|
||||
identifier = models.CharField(max_length=255)
|
||||
access_token = models.TextField(blank=True, null=True, default=None)
|
||||
|
||||
@property
|
||||
|
@ -1,5 +1,3 @@
|
||||
"""Plex Source connection Serializer"""
|
||||
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.sources import (
|
||||
@ -12,14 +10,9 @@ from authentik.sources.plex.models import GroupPlexSourceConnection, UserPlexSou
|
||||
|
||||
|
||||
class UserPlexSourceConnectionSerializer(UserSourceConnectionSerializer):
|
||||
"""Plex Source connection Serializer"""
|
||||
|
||||
class Meta(UserSourceConnectionSerializer.Meta):
|
||||
model = UserPlexSourceConnection
|
||||
fields = UserSourceConnectionSerializer.Meta.fields + [
|
||||
"identifier",
|
||||
"plex_token",
|
||||
]
|
||||
fields = UserSourceConnectionSerializer.Meta.fields + ["plex_token"]
|
||||
extra_kwargs = {
|
||||
**UserSourceConnectionSerializer.Meta.extra_kwargs,
|
||||
"plex_token": {"write_only": True},
|
||||
@ -27,21 +20,15 @@ class UserPlexSourceConnectionSerializer(UserSourceConnectionSerializer):
|
||||
|
||||
|
||||
class UserPlexSourceConnectionViewSet(UserSourceConnectionViewSet, ModelViewSet):
|
||||
"""Plex Source connection Serializer"""
|
||||
|
||||
queryset = UserPlexSourceConnection.objects.all()
|
||||
serializer_class = UserPlexSourceConnectionSerializer
|
||||
|
||||
|
||||
class GroupPlexSourceConnectionSerializer(GroupSourceConnectionSerializer):
|
||||
"""Plex Group-Source connection Serializer"""
|
||||
|
||||
class Meta(GroupSourceConnectionSerializer.Meta):
|
||||
model = GroupPlexSourceConnection
|
||||
|
||||
|
||||
class GroupPlexSourceConnectionViewSet(GroupSourceConnectionViewSet, ModelViewSet):
|
||||
"""Group-source connection Viewset"""
|
||||
|
||||
queryset = GroupPlexSourceConnection.objects.all()
|
||||
serializer_class = GroupPlexSourceConnectionSerializer
|
||||
|
@ -0,0 +1,29 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def migrate_identifier(apps, schema_editor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
UserPlexSourceConnection = apps.get_model("authentik_sources_plex", "UserPlexSourceConnection")
|
||||
|
||||
for connection in UserPlexSourceConnection.objects.using(db_alias).all():
|
||||
connection.new_identifier = connection.identifier
|
||||
connection.save(using=db_alias)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
(
|
||||
"authentik_sources_plex",
|
||||
"0004_groupplexsourceconnection_plexsourcepropertymapping_and_more",
|
||||
),
|
||||
("authentik_core", "0044_usersourceconnection_new_identifier"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(code=migrate_identifier, reverse_code=migrations.RunPython.noop),
|
||||
migrations.RemoveField(
|
||||
model_name="userplexsourceconnection",
|
||||
name="identifier",
|
||||
),
|
||||
]
|
@ -141,7 +141,6 @@ class UserPlexSourceConnection(UserSourceConnection):
|
||||
"""Connect user and plex source"""
|
||||
|
||||
plex_token = models.TextField()
|
||||
identifier = models.TextField()
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
|
@ -1,5 +1,3 @@
|
||||
"""SAML Source Serializer"""
|
||||
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.sources import (
|
||||
@ -12,29 +10,20 @@ from authentik.sources.saml.models import GroupSAMLSourceConnection, UserSAMLSou
|
||||
|
||||
|
||||
class UserSAMLSourceConnectionSerializer(UserSourceConnectionSerializer):
|
||||
"""SAML Source Serializer"""
|
||||
|
||||
class Meta(UserSourceConnectionSerializer.Meta):
|
||||
model = UserSAMLSourceConnection
|
||||
fields = UserSourceConnectionSerializer.Meta.fields + ["identifier"]
|
||||
|
||||
|
||||
class UserSAMLSourceConnectionViewSet(UserSourceConnectionViewSet, ModelViewSet):
|
||||
"""Source Viewset"""
|
||||
|
||||
queryset = UserSAMLSourceConnection.objects.all()
|
||||
serializer_class = UserSAMLSourceConnectionSerializer
|
||||
|
||||
|
||||
class GroupSAMLSourceConnectionSerializer(GroupSourceConnectionSerializer):
|
||||
"""OAuth Group-Source connection Serializer"""
|
||||
|
||||
class Meta(GroupSourceConnectionSerializer.Meta):
|
||||
model = GroupSAMLSourceConnection
|
||||
|
||||
|
||||
class GroupSAMLSourceConnectionViewSet(GroupSourceConnectionViewSet):
|
||||
"""Group-source connection Viewset"""
|
||||
|
||||
class GroupSAMLSourceConnectionViewSet(GroupSourceConnectionViewSet, ModelViewSet):
|
||||
queryset = GroupSAMLSourceConnection.objects.all()
|
||||
serializer_class = GroupSAMLSourceConnectionSerializer
|
||||
|
@ -0,0 +1,26 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def migrate_identifier(apps, schema_editor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
UserSAMLSourceConnection = apps.get_model("authentik_sources_saml", "UserSAMLSourceConnection")
|
||||
|
||||
for connection in UserSAMLSourceConnection.objects.using(db_alias).all():
|
||||
connection.new_identifier = connection.identifier
|
||||
connection.save(using=db_alias)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_sources_saml", "0018_alter_samlsource_slo_url_alter_samlsource_sso_url"),
|
||||
("authentik_core", "0044_usersourceconnection_new_identifier"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(code=migrate_identifier, reverse_code=migrations.RunPython.noop),
|
||||
migrations.RemoveField(
|
||||
model_name="usersamlsourceconnection",
|
||||
name="identifier",
|
||||
),
|
||||
]
|
@ -318,8 +318,6 @@ class SAMLSourcePropertyMapping(PropertyMapping):
|
||||
class UserSAMLSourceConnection(UserSourceConnection):
|
||||
"""Connection to configured SAML Sources."""
|
||||
|
||||
identifier = models.TextField()
|
||||
|
||||
@property
|
||||
def serializer(self) -> Serializer:
|
||||
from authentik.sources.saml.api.source_connection import UserSAMLSourceConnectionSerializer
|
||||
|
@ -104,6 +104,13 @@ def send_mail(
|
||||
# can't be converted to json)
|
||||
message_object.attach(logo_data())
|
||||
|
||||
if (
|
||||
message_object.to
|
||||
and isinstance(message_object.to[0], str)
|
||||
and "=?utf-8?" in message_object.to[0]
|
||||
):
|
||||
message_object.to = [message_object.to[0].split("<")[-1].replace(">", "")]
|
||||
|
||||
LOGGER.debug("Sending mail", to=message_object.to)
|
||||
backend.send_messages([message_object])
|
||||
Event.new(
|
||||
|
@ -97,6 +97,37 @@ class TestEmailStageSending(FlowTestCase):
|
||||
self.assertEqual(mail.outbox[0].subject, "authentik")
|
||||
self.assertEqual(mail.outbox[0].to, [f"Test User Many Words <{long_user.email}>"])
|
||||
|
||||
def test_utf8_name(self):
|
||||
"""Test with pending user"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
utf8_user = create_test_user()
|
||||
utf8_user.name = "Cirilo ЉМНЊ el cirilico И̂ӢЙӤ "
|
||||
utf8_user.email = "cyrillic@authentik.local"
|
||||
utf8_user.save()
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = utf8_user
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
Event.objects.filter(action=EventAction.EMAIL_SENT).delete()
|
||||
|
||||
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||
with patch(
|
||||
"authentik.stages.email.models.EmailStage.backend_class",
|
||||
PropertyMock(return_value=EmailBackend),
|
||||
):
|
||||
response = self.client.post(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertStageResponse(
|
||||
response,
|
||||
self.flow,
|
||||
response_errors={
|
||||
"non_field_errors": [{"string": "email-sent", "code": "email-sent"}]
|
||||
},
|
||||
)
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertEqual(mail.outbox[0].subject, "authentik")
|
||||
self.assertEqual(mail.outbox[0].to, [f"{utf8_user.email}"])
|
||||
|
||||
def test_pending_fake_user(self):
|
||||
"""Test with pending (fake) user"""
|
||||
self.flow.designation = FlowDesignation.RECOVERY
|
||||
|
@ -8231,7 +8231,6 @@
|
||||
},
|
||||
"identifier": {
|
||||
"type": "string",
|
||||
"maxLength": 255,
|
||||
"minLength": 1,
|
||||
"title": "Identifier"
|
||||
},
|
||||
|
8
lifecycle/aws/package-lock.json
generated
8
lifecycle/aws/package-lock.json
generated
@ -9,7 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"aws-cdk": "^2.1006.0",
|
||||
"aws-cdk": "^2.1007.0",
|
||||
"cross-env": "^7.0.3"
|
||||
},
|
||||
"engines": {
|
||||
@ -17,9 +17,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk": {
|
||||
"version": "2.1006.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1006.0.tgz",
|
||||
"integrity": "sha512-6qYnCt4mBN+3i/5F+FC2yMETkDHY/IL7gt3EuqKVPcaAO4jU7oXfVSlR60CYRkZWL4fnAurUV14RkJuJyVG/IA==",
|
||||
"version": "2.1007.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1007.0.tgz",
|
||||
"integrity": "sha512-/UOYOTGWUm+pP9qxg03tID5tL6euC+pb+xo0RBue+xhnUWwj/Bbsw6DbqbpOPMrNzTUxmM723/uMEQmM6S26dw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
|
@ -10,7 +10,7 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"aws-cdk": "^2.1006.0",
|
||||
"aws-cdk": "^2.1007.0",
|
||||
"cross-env": "^7.0.3"
|
||||
}
|
||||
}
|
||||
|
Binary file not shown.
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2025.2.1",
|
||||
"version": "2025.2.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2025.2.1"
|
||||
"version": "2025.2.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ dependencies = [
|
||||
"pydantic-scim",
|
||||
"pyjwt",
|
||||
"pyrad",
|
||||
"python-kadmin-rs ==0.5.3",
|
||||
"python-kadmin-rs ==0.6.0",
|
||||
"pyyaml",
|
||||
"requests-oauthlib",
|
||||
"scim2-filter-parser",
|
||||
|
545
schema.yml
545
schema.yml
@ -25938,6 +25938,243 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/sources/group_connections/all/:
|
||||
get:
|
||||
operationId: sources_group_connections_all_list
|
||||
description: Group-source connection Viewset
|
||||
parameters:
|
||||
- in: query
|
||||
name: group
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- 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
|
||||
- name: search
|
||||
required: false
|
||||
in: query
|
||||
description: A search term.
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: source__slug
|
||||
schema:
|
||||
type: string
|
||||
tags:
|
||||
- sources
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PaginatedGroupSourceConnectionList'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/sources/group_connections/all/{id}/:
|
||||
get:
|
||||
operationId: sources_group_connections_all_retrieve
|
||||
description: Group-source connection Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: integer
|
||||
description: A unique integer value identifying this group source connection.
|
||||
required: true
|
||||
tags:
|
||||
- sources
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GroupSourceConnection'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
put:
|
||||
operationId: sources_group_connections_all_update
|
||||
description: Group-source connection Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: integer
|
||||
description: A unique integer value identifying this group source connection.
|
||||
required: true
|
||||
tags:
|
||||
- sources
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GroupSourceConnectionRequest'
|
||||
required: true
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GroupSourceConnection'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
patch:
|
||||
operationId: sources_group_connections_all_partial_update
|
||||
description: Group-source connection Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: integer
|
||||
description: A unique integer value identifying this group source connection.
|
||||
required: true
|
||||
tags:
|
||||
- sources
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PatchedGroupSourceConnectionRequest'
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GroupSourceConnection'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
delete:
|
||||
operationId: sources_group_connections_all_destroy
|
||||
description: Group-source connection Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: integer
|
||||
description: A unique integer value identifying this group source connection.
|
||||
required: true
|
||||
tags:
|
||||
- sources
|
||||
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: ''
|
||||
/sources/group_connections/all/{id}/used_by/:
|
||||
get:
|
||||
operationId: sources_group_connections_all_used_by_list
|
||||
description: Get a list of all objects that use this object
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: integer
|
||||
description: A unique integer value identifying this group source connection.
|
||||
required: true
|
||||
tags:
|
||||
- sources
|
||||
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: ''
|
||||
/sources/group_connections/kerberos/:
|
||||
get:
|
||||
operationId: sources_group_connections_kerberos_list
|
||||
@ -25999,6 +26236,38 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
post:
|
||||
operationId: sources_group_connections_kerberos_create
|
||||
description: Group-source connection Viewset
|
||||
tags:
|
||||
- sources
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GroupKerberosSourceConnectionRequest'
|
||||
required: true
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'201':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GroupKerberosSourceConnection'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/sources/group_connections/kerberos/{id}/:
|
||||
get:
|
||||
operationId: sources_group_connections_kerberos_retrieve
|
||||
@ -26779,6 +27048,38 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
post:
|
||||
operationId: sources_group_connections_saml_create
|
||||
description: Group-source connection Viewset
|
||||
tags:
|
||||
- sources
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GroupSAMLSourceConnectionRequest'
|
||||
required: true
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'201':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GroupSAMLSourceConnection'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/sources/group_connections/saml/{id}/:
|
||||
get:
|
||||
operationId: sources_group_connections_saml_retrieve
|
||||
@ -29992,7 +30293,7 @@ paths:
|
||||
/sources/user_connections/kerberos/:
|
||||
get:
|
||||
operationId: sources_user_connections_kerberos_list
|
||||
description: Source Viewset
|
||||
description: User-source connection Viewset
|
||||
parameters:
|
||||
- name: ordering
|
||||
required: false
|
||||
@ -30022,6 +30323,10 @@ paths:
|
||||
name: source__slug
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: user
|
||||
schema:
|
||||
type: integer
|
||||
tags:
|
||||
- sources
|
||||
security:
|
||||
@ -30047,7 +30352,7 @@ paths:
|
||||
description: ''
|
||||
post:
|
||||
operationId: sources_user_connections_kerberos_create
|
||||
description: Source Viewset
|
||||
description: User-source connection Viewset
|
||||
tags:
|
||||
- sources
|
||||
requestBody:
|
||||
@ -30080,7 +30385,7 @@ paths:
|
||||
/sources/user_connections/kerberos/{id}/:
|
||||
get:
|
||||
operationId: sources_user_connections_kerberos_retrieve
|
||||
description: Source Viewset
|
||||
description: User-source connection Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
@ -30114,7 +30419,7 @@ paths:
|
||||
description: ''
|
||||
put:
|
||||
operationId: sources_user_connections_kerberos_update
|
||||
description: Source Viewset
|
||||
description: User-source connection Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
@ -30154,7 +30459,7 @@ paths:
|
||||
description: ''
|
||||
patch:
|
||||
operationId: sources_user_connections_kerberos_partial_update
|
||||
description: Source Viewset
|
||||
description: User-source connection Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
@ -30193,7 +30498,7 @@ paths:
|
||||
description: ''
|
||||
delete:
|
||||
operationId: sources_user_connections_kerberos_destroy
|
||||
description: Source Viewset
|
||||
description: User-source connection Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
@ -30261,7 +30566,7 @@ paths:
|
||||
/sources/user_connections/oauth/:
|
||||
get:
|
||||
operationId: sources_user_connections_oauth_list
|
||||
description: Source Viewset
|
||||
description: User-source connection Viewset
|
||||
parameters:
|
||||
- name: ordering
|
||||
required: false
|
||||
@ -30320,7 +30625,7 @@ paths:
|
||||
description: ''
|
||||
post:
|
||||
operationId: sources_user_connections_oauth_create
|
||||
description: Source Viewset
|
||||
description: User-source connection Viewset
|
||||
tags:
|
||||
- sources
|
||||
requestBody:
|
||||
@ -30353,7 +30658,7 @@ paths:
|
||||
/sources/user_connections/oauth/{id}/:
|
||||
get:
|
||||
operationId: sources_user_connections_oauth_retrieve
|
||||
description: Source Viewset
|
||||
description: User-source connection Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
@ -30386,7 +30691,7 @@ paths:
|
||||
description: ''
|
||||
put:
|
||||
operationId: sources_user_connections_oauth_update
|
||||
description: Source Viewset
|
||||
description: User-source connection Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
@ -30425,7 +30730,7 @@ paths:
|
||||
description: ''
|
||||
patch:
|
||||
operationId: sources_user_connections_oauth_partial_update
|
||||
description: Source Viewset
|
||||
description: User-source connection Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
@ -30463,7 +30768,7 @@ paths:
|
||||
description: ''
|
||||
delete:
|
||||
operationId: sources_user_connections_oauth_destroy
|
||||
description: Source Viewset
|
||||
description: User-source connection Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
@ -30529,7 +30834,7 @@ paths:
|
||||
/sources/user_connections/plex/:
|
||||
get:
|
||||
operationId: sources_user_connections_plex_list
|
||||
description: Plex Source connection Serializer
|
||||
description: User-source connection Viewset
|
||||
parameters:
|
||||
- name: ordering
|
||||
required: false
|
||||
@ -30588,7 +30893,7 @@ paths:
|
||||
description: ''
|
||||
post:
|
||||
operationId: sources_user_connections_plex_create
|
||||
description: Plex Source connection Serializer
|
||||
description: User-source connection Viewset
|
||||
tags:
|
||||
- sources
|
||||
requestBody:
|
||||
@ -30621,7 +30926,7 @@ paths:
|
||||
/sources/user_connections/plex/{id}/:
|
||||
get:
|
||||
operationId: sources_user_connections_plex_retrieve
|
||||
description: Plex Source connection Serializer
|
||||
description: User-source connection Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
@ -30654,7 +30959,7 @@ paths:
|
||||
description: ''
|
||||
put:
|
||||
operationId: sources_user_connections_plex_update
|
||||
description: Plex Source connection Serializer
|
||||
description: User-source connection Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
@ -30693,7 +30998,7 @@ paths:
|
||||
description: ''
|
||||
patch:
|
||||
operationId: sources_user_connections_plex_partial_update
|
||||
description: Plex Source connection Serializer
|
||||
description: User-source connection Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
@ -30731,7 +31036,7 @@ paths:
|
||||
description: ''
|
||||
delete:
|
||||
operationId: sources_user_connections_plex_destroy
|
||||
description: Plex Source connection Serializer
|
||||
description: User-source connection Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
@ -30797,7 +31102,7 @@ paths:
|
||||
/sources/user_connections/saml/:
|
||||
get:
|
||||
operationId: sources_user_connections_saml_list
|
||||
description: Source Viewset
|
||||
description: User-source connection Viewset
|
||||
parameters:
|
||||
- name: ordering
|
||||
required: false
|
||||
@ -30856,7 +31161,7 @@ paths:
|
||||
description: ''
|
||||
post:
|
||||
operationId: sources_user_connections_saml_create
|
||||
description: Source Viewset
|
||||
description: User-source connection Viewset
|
||||
tags:
|
||||
- sources
|
||||
requestBody:
|
||||
@ -30889,7 +31194,7 @@ paths:
|
||||
/sources/user_connections/saml/{id}/:
|
||||
get:
|
||||
operationId: sources_user_connections_saml_retrieve
|
||||
description: Source Viewset
|
||||
description: User-source connection Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
@ -30922,7 +31227,7 @@ paths:
|
||||
description: ''
|
||||
put:
|
||||
operationId: sources_user_connections_saml_update
|
||||
description: Source Viewset
|
||||
description: User-source connection Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
@ -30961,7 +31266,7 @@ paths:
|
||||
description: ''
|
||||
patch:
|
||||
operationId: sources_user_connections_saml_partial_update
|
||||
description: Source Viewset
|
||||
description: User-source connection Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
@ -30999,7 +31304,7 @@ paths:
|
||||
description: ''
|
||||
delete:
|
||||
operationId: sources_user_connections_saml_destroy
|
||||
description: Source Viewset
|
||||
description: User-source connection Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
@ -44453,7 +44758,7 @@ components:
|
||||
- users_obj
|
||||
GroupKerberosSourceConnection:
|
||||
type: object
|
||||
description: OAuth Group-Source connection Serializer
|
||||
description: Group Source Connection
|
||||
properties:
|
||||
pk:
|
||||
type: integer
|
||||
@ -44475,16 +44780,21 @@ components:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
last_updated:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
required:
|
||||
- created
|
||||
- group
|
||||
- identifier
|
||||
- last_updated
|
||||
- pk
|
||||
- source
|
||||
- source_obj
|
||||
GroupKerberosSourceConnectionRequest:
|
||||
type: object
|
||||
description: OAuth Group-Source connection Serializer
|
||||
description: Group Source Connection
|
||||
properties:
|
||||
group:
|
||||
type: string
|
||||
@ -44584,7 +44894,7 @@ components:
|
||||
- username
|
||||
GroupOAuthSourceConnection:
|
||||
type: object
|
||||
description: OAuth Group-Source connection Serializer
|
||||
description: Group Source Connection
|
||||
properties:
|
||||
pk:
|
||||
type: integer
|
||||
@ -44606,16 +44916,21 @@ components:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
last_updated:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
required:
|
||||
- created
|
||||
- group
|
||||
- identifier
|
||||
- last_updated
|
||||
- pk
|
||||
- source
|
||||
- source_obj
|
||||
GroupOAuthSourceConnectionRequest:
|
||||
type: object
|
||||
description: OAuth Group-Source connection Serializer
|
||||
description: Group Source Connection
|
||||
properties:
|
||||
group:
|
||||
type: string
|
||||
@ -44632,7 +44947,7 @@ components:
|
||||
- source
|
||||
GroupPlexSourceConnection:
|
||||
type: object
|
||||
description: Plex Group-Source connection Serializer
|
||||
description: Group Source Connection
|
||||
properties:
|
||||
pk:
|
||||
type: integer
|
||||
@ -44654,16 +44969,21 @@ components:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
last_updated:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
required:
|
||||
- created
|
||||
- group
|
||||
- identifier
|
||||
- last_updated
|
||||
- pk
|
||||
- source
|
||||
- source_obj
|
||||
GroupPlexSourceConnectionRequest:
|
||||
type: object
|
||||
description: Plex Group-Source connection Serializer
|
||||
description: Group Source Connection
|
||||
properties:
|
||||
group:
|
||||
type: string
|
||||
@ -44708,7 +45028,7 @@ components:
|
||||
- name
|
||||
GroupSAMLSourceConnection:
|
||||
type: object
|
||||
description: OAuth Group-Source connection Serializer
|
||||
description: Group Source Connection
|
||||
properties:
|
||||
pk:
|
||||
type: integer
|
||||
@ -44730,16 +45050,74 @@ components:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
last_updated:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
required:
|
||||
- created
|
||||
- group
|
||||
- identifier
|
||||
- last_updated
|
||||
- pk
|
||||
- source
|
||||
- source_obj
|
||||
GroupSAMLSourceConnectionRequest:
|
||||
type: object
|
||||
description: OAuth Group-Source connection Serializer
|
||||
description: Group Source Connection
|
||||
properties:
|
||||
group:
|
||||
type: string
|
||||
format: uuid
|
||||
source:
|
||||
type: string
|
||||
format: uuid
|
||||
identifier:
|
||||
type: string
|
||||
minLength: 1
|
||||
required:
|
||||
- group
|
||||
- identifier
|
||||
- source
|
||||
GroupSourceConnection:
|
||||
type: object
|
||||
description: Group Source Connection
|
||||
properties:
|
||||
pk:
|
||||
type: integer
|
||||
readOnly: true
|
||||
title: ID
|
||||
group:
|
||||
type: string
|
||||
format: uuid
|
||||
source:
|
||||
type: string
|
||||
format: uuid
|
||||
source_obj:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Source'
|
||||
readOnly: true
|
||||
identifier:
|
||||
type: string
|
||||
created:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
last_updated:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
required:
|
||||
- created
|
||||
- group
|
||||
- identifier
|
||||
- last_updated
|
||||
- pk
|
||||
- source
|
||||
- source_obj
|
||||
GroupSourceConnectionRequest:
|
||||
type: object
|
||||
description: Group Source Connection
|
||||
properties:
|
||||
group:
|
||||
type: string
|
||||
@ -48365,6 +48743,18 @@ components:
|
||||
required:
|
||||
- pagination
|
||||
- results
|
||||
PaginatedGroupSourceConnectionList:
|
||||
type: object
|
||||
properties:
|
||||
pagination:
|
||||
$ref: '#/components/schemas/Pagination'
|
||||
results:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/GroupSourceConnection'
|
||||
required:
|
||||
- pagination
|
||||
- results
|
||||
PaginatedIdentificationStageList:
|
||||
type: object
|
||||
properties:
|
||||
@ -50745,7 +51135,7 @@ components:
|
||||
the remote system.
|
||||
PatchedGroupKerberosSourceConnectionRequest:
|
||||
type: object
|
||||
description: OAuth Group-Source connection Serializer
|
||||
description: Group Source Connection
|
||||
properties:
|
||||
group:
|
||||
type: string
|
||||
@ -50758,7 +51148,7 @@ components:
|
||||
minLength: 1
|
||||
PatchedGroupOAuthSourceConnectionRequest:
|
||||
type: object
|
||||
description: OAuth Group-Source connection Serializer
|
||||
description: Group Source Connection
|
||||
properties:
|
||||
group:
|
||||
type: string
|
||||
@ -50771,7 +51161,7 @@ components:
|
||||
minLength: 1
|
||||
PatchedGroupPlexSourceConnectionRequest:
|
||||
type: object
|
||||
description: Plex Group-Source connection Serializer
|
||||
description: Group Source Connection
|
||||
properties:
|
||||
group:
|
||||
type: string
|
||||
@ -50810,7 +51200,20 @@ components:
|
||||
format: uuid
|
||||
PatchedGroupSAMLSourceConnectionRequest:
|
||||
type: object
|
||||
description: OAuth Group-Source connection Serializer
|
||||
description: Group Source Connection
|
||||
properties:
|
||||
group:
|
||||
type: string
|
||||
format: uuid
|
||||
source:
|
||||
type: string
|
||||
format: uuid
|
||||
identifier:
|
||||
type: string
|
||||
minLength: 1
|
||||
PatchedGroupSourceConnectionRequest:
|
||||
type: object
|
||||
description: Group Source Connection
|
||||
properties:
|
||||
group:
|
||||
type: string
|
||||
@ -52783,7 +53186,7 @@ components:
|
||||
$ref: '#/components/schemas/FlowSetRequest'
|
||||
PatchedUserKerberosSourceConnectionRequest:
|
||||
type: object
|
||||
description: Kerberos Source Serializer
|
||||
description: User source connection
|
||||
properties:
|
||||
user:
|
||||
type: integer
|
||||
@ -52840,7 +53243,7 @@ components:
|
||||
$ref: '#/components/schemas/FlowSetRequest'
|
||||
PatchedUserOAuthSourceConnectionRequest:
|
||||
type: object
|
||||
description: OAuth Source Serializer
|
||||
description: User source connection
|
||||
properties:
|
||||
user:
|
||||
type: integer
|
||||
@ -52850,14 +53253,13 @@ components:
|
||||
identifier:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 255
|
||||
access_token:
|
||||
type: string
|
||||
writeOnly: true
|
||||
nullable: true
|
||||
PatchedUserPlexSourceConnectionRequest:
|
||||
type: object
|
||||
description: Plex Source connection Serializer
|
||||
description: User source connection
|
||||
properties:
|
||||
user:
|
||||
type: integer
|
||||
@ -52911,7 +53313,7 @@ components:
|
||||
$ref: '#/components/schemas/UserTypeEnum'
|
||||
PatchedUserSAMLSourceConnectionRequest:
|
||||
type: object
|
||||
description: SAML Source Serializer
|
||||
description: User source connection
|
||||
properties:
|
||||
user:
|
||||
type: integer
|
||||
@ -52930,6 +53332,9 @@ components:
|
||||
source:
|
||||
type: string
|
||||
format: uuid
|
||||
identifier:
|
||||
type: string
|
||||
minLength: 1
|
||||
PatchedUserWriteStageRequest:
|
||||
type: object
|
||||
description: UserWriteStage Serializer
|
||||
@ -58017,7 +58422,7 @@ components:
|
||||
- name
|
||||
UserKerberosSourceConnection:
|
||||
type: object
|
||||
description: Kerberos Source Serializer
|
||||
description: User source connection
|
||||
properties:
|
||||
pk:
|
||||
type: integer
|
||||
@ -58032,22 +58437,27 @@ components:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Source'
|
||||
readOnly: true
|
||||
identifier:
|
||||
type: string
|
||||
created:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
identifier:
|
||||
last_updated:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
required:
|
||||
- created
|
||||
- identifier
|
||||
- last_updated
|
||||
- pk
|
||||
- source
|
||||
- source_obj
|
||||
- user
|
||||
UserKerberosSourceConnectionRequest:
|
||||
type: object
|
||||
description: Kerberos Source Serializer
|
||||
description: User source connection
|
||||
properties:
|
||||
user:
|
||||
type: integer
|
||||
@ -58274,7 +58684,7 @@ components:
|
||||
- logins_failed
|
||||
UserOAuthSourceConnection:
|
||||
type: object
|
||||
description: OAuth Source Serializer
|
||||
description: User source connection
|
||||
properties:
|
||||
pk:
|
||||
type: integer
|
||||
@ -58289,23 +58699,27 @@ components:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Source'
|
||||
readOnly: true
|
||||
identifier:
|
||||
type: string
|
||||
created:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
identifier:
|
||||
last_updated:
|
||||
type: string
|
||||
maxLength: 255
|
||||
format: date-time
|
||||
readOnly: true
|
||||
required:
|
||||
- created
|
||||
- identifier
|
||||
- last_updated
|
||||
- pk
|
||||
- source
|
||||
- source_obj
|
||||
- user
|
||||
UserOAuthSourceConnectionRequest:
|
||||
type: object
|
||||
description: OAuth Source Serializer
|
||||
description: User source connection
|
||||
properties:
|
||||
user:
|
||||
type: integer
|
||||
@ -58315,7 +58729,6 @@ components:
|
||||
identifier:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 255
|
||||
access_token:
|
||||
type: string
|
||||
writeOnly: true
|
||||
@ -58373,7 +58786,7 @@ components:
|
||||
- paths
|
||||
UserPlexSourceConnection:
|
||||
type: object
|
||||
description: Plex Source connection Serializer
|
||||
description: User source connection
|
||||
properties:
|
||||
pk:
|
||||
type: integer
|
||||
@ -58388,22 +58801,27 @@ components:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Source'
|
||||
readOnly: true
|
||||
identifier:
|
||||
type: string
|
||||
created:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
identifier:
|
||||
last_updated:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
required:
|
||||
- created
|
||||
- identifier
|
||||
- last_updated
|
||||
- pk
|
||||
- source
|
||||
- source_obj
|
||||
- user
|
||||
UserPlexSourceConnectionRequest:
|
||||
type: object
|
||||
description: Plex Source connection Serializer
|
||||
description: User source connection
|
||||
properties:
|
||||
user:
|
||||
type: integer
|
||||
@ -58465,7 +58883,7 @@ components:
|
||||
- username
|
||||
UserSAMLSourceConnection:
|
||||
type: object
|
||||
description: SAML Source Serializer
|
||||
description: User source connection
|
||||
properties:
|
||||
pk:
|
||||
type: integer
|
||||
@ -58480,22 +58898,27 @@ components:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Source'
|
||||
readOnly: true
|
||||
identifier:
|
||||
type: string
|
||||
created:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
identifier:
|
||||
last_updated:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
required:
|
||||
- created
|
||||
- identifier
|
||||
- last_updated
|
||||
- pk
|
||||
- source
|
||||
- source_obj
|
||||
- user
|
||||
UserSAMLSourceConnectionRequest:
|
||||
type: object
|
||||
description: SAML Source Serializer
|
||||
description: User source connection
|
||||
properties:
|
||||
user:
|
||||
type: integer
|
||||
@ -58659,12 +59082,20 @@ components:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Source'
|
||||
readOnly: true
|
||||
identifier:
|
||||
type: string
|
||||
created:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
last_updated:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
required:
|
||||
- created
|
||||
- identifier
|
||||
- last_updated
|
||||
- pk
|
||||
- source
|
||||
- source_obj
|
||||
@ -58678,7 +59109,11 @@ components:
|
||||
source:
|
||||
type: string
|
||||
format: uuid
|
||||
identifier:
|
||||
type: string
|
||||
minLength: 1
|
||||
required:
|
||||
- identifier
|
||||
- source
|
||||
- user
|
||||
UserTypeEnum:
|
||||
|
18
uv.lock
generated
18
uv.lock
generated
@ -310,7 +310,7 @@ requires-dist = [
|
||||
{ name = "pydantic-scim" },
|
||||
{ name = "pyjwt" },
|
||||
{ name = "pyrad" },
|
||||
{ name = "python-kadmin-rs", specifier = "==0.5.3" },
|
||||
{ name = "python-kadmin-rs", specifier = "==0.6.0" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "requests-oauthlib" },
|
||||
{ name = "scim2-filter-parser" },
|
||||
@ -2599,16 +2599,16 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "python-kadmin-rs"
|
||||
version = "0.5.3"
|
||||
version = "0.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e7/95/07b708623f13874ad86dc603f2fe36e980a5f5890edea87286d13f2b0b81/python_kadmin_rs-0.5.3.tar.gz", hash = "sha256:4f46fd854af622896136c3ac4fc5e6a37d37bfffb5b2023e438001ffa62ab7e3", size = 89865 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b9/ac/df3a093b1e186cd68a6f38778fac025450e5c5e9859c4790e00c2ed0ff62/python_kadmin_rs-0.6.0.tar.gz", hash = "sha256:dadd3d4ef542b829c1dcde97360a6b6a10700a4b5686f12f24b10f6cf5ca6e6c", size = 89318 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/96/46/1bbfd7d6819851c300b991d7340452fba8edc3d2fe68b33271279eb74887/python_kadmin_rs-0.5.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:54b5e1c2e22da0d16c1418eb2b46da8baa11699a5db8db2afc52dbfd02d14958", size = 1416637 },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/34/fd7f5c324aaf1b9ad3dd5050ac2059230618c29adc452d676d2af4d5ae79/python_kadmin_rs-0.5.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:d1dc7ad1f07bbfd09baeb1fb0dfc45c87776ed717052081e63d3bdba340a250e", size = 1503018 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/29/3931502534e07806cf7c70631374452cfcbafa44e75c5403416372b701c7/python_kadmin_rs-0.5.3-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:86404a1060ece916088ae4a0d188e9309fd46e0b3003779ee7a8dc7493176779", size = 3268475 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/5d/f18ca5df97a4241711555987eb308c6e6c5505883514ac7f18d7aebd52f2/python_kadmin_rs-0.5.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7aa62a618af2b2112f708fd44f9cc3cf25e28f1562ea66a2036fb3cd1a47e649", size = 3371699 },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/d3/42c4d57414cfdf4e4ff528dd8e72428908ee67aeeae6a63fe2f5dbcd04bc/python_kadmin_rs-0.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80813af82dfbcc6a90505183c822eab11de77b6703e5691e37ed77d292224dd9", size = 1584049 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/65/705f179cf4bf4d16fc1daeac0810def57da2f4514a5b79ca60f24d7efb90/python_kadmin_rs-0.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6799a0faddb4ccf200acfa87da38e5fa2af54970d066b2c876e752bbf794b204", size = 1590360 },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/6d/59fefe1c4c11177c4feb8ad65dd6a265e9cc5fc83682a928acdccb170000/python_kadmin_rs-0.6.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:0069fbd656096b98853f8cdc6d5e24f754829fa9cb4a716dac33777f0305d37a", size = 1418187 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/12/c00a71c0fc17f5d208b4bb5e570002d74f0bc414e35194537d46ea32080f/python_kadmin_rs-0.6.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:cfcfe9982e969705dee62f2b97c8d7c249b55b2a97e2bc981408061ea7182b96", size = 1501759 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/b5/06cf809cfaaeded84e6634bf07116264ab4f8fd5eccca7523114e197f424/python_kadmin_rs-0.6.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:920df382e7a554d2f6fd160436a64adf1251f3262ec16bccd6d3b9f7e039d5fa", size = 3262691 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/72/99884dbc1856440a548ea8bf2ff1232c7f2823b6cb1a62bbb4d902a34609/python_kadmin_rs-0.6.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:94509b7470b18105c27fcaf5e6af894644614a687af74a43499735c405217e01", size = 3382996 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/4f/5d7e5be27cd466affc00fcab71fb94ea0420aee95306188988faf270b129/python_kadmin_rs-0.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6f89e7fbcb7220a42c143a1b008685f98ca0a72ecc55c30f85b72c9d1ba9c3b9", size = 1572007 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/1e/fdd7d6cd2ebc4cc654112329311380d1c03c681511973e32ae6ab90f261c/python_kadmin_rs-0.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:775ce07ffd47a50ba27c8d74c20baacb56acfc7a8c56a8b02f2207ed9829156e", size = 1618897 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
206
web/authentication/index.js
Normal file
206
web/authentication/index.js
Normal file
@ -0,0 +1,206 @@
|
||||
/**
|
||||
* @file WebAuthn utilities.
|
||||
*/
|
||||
import { fromByteArray } from "base64-js";
|
||||
|
||||
//@ts-check
|
||||
|
||||
//#region Type Definitions
|
||||
|
||||
/**
|
||||
* @typedef {object} Assertion
|
||||
* @property {string} id
|
||||
* @property {string} rawId
|
||||
* @property {string} type
|
||||
* @property {string} registrationClientExtensions
|
||||
* @property {object} response
|
||||
* @property {string} response.clientDataJSON
|
||||
* @property {string} response.attestationObject
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} AuthAssertion
|
||||
* @property {string} id
|
||||
* @property {string} rawId
|
||||
* @property {string} type
|
||||
* @property {string} assertionClientExtensions
|
||||
* @property {object} response
|
||||
* @property {string} response.clientDataJSON
|
||||
* @property {string} response.authenticatorData
|
||||
* @property {string} response.signature
|
||||
* @property {string | null} response.userHandle
|
||||
*/
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Encoding/Decoding
|
||||
|
||||
/**
|
||||
* Encodes a byte array into a URL-safe base64 string.
|
||||
*
|
||||
* @param {Uint8Array} buffer
|
||||
* @returns {string}
|
||||
*/
|
||||
export function encodeBase64(buffer) {
|
||||
return fromByteArray(buffer).replace(/\+/g, "-").replace(/\//g, "_").replace(/[=]/g, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a byte array into a base64 string without URL-safe encoding, i.e., with padding.
|
||||
* @param {Uint8Array} buffer
|
||||
* @returns {string}
|
||||
*/
|
||||
export function encodeBase64Raw(buffer) {
|
||||
return fromByteArray(buffer).replace(/\+/g, "-").replace(/\//g, "_");
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a base64 string into a byte array.
|
||||
*
|
||||
* @param {string} input
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
export function decodeBase64(input) {
|
||||
return Uint8Array.from(atob(input.replace(/_/g, "/").replace(/-/g, "+")), (c) =>
|
||||
c.charCodeAt(0),
|
||||
);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Utility Functions
|
||||
|
||||
/**
|
||||
* Checks if the browser supports WebAuthn.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isWebAuthnSupported() {
|
||||
if ("credentials" in navigator) return true;
|
||||
|
||||
if (window.location.protocol === "http:" && window.location.hostname !== "localhost") {
|
||||
console.warn("WebAuthn requires this page to be accessed via HTTPS.");
|
||||
return false;
|
||||
}
|
||||
|
||||
console.warn("WebAuthn not supported by browser.");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the browser supports WebAuthn and that we're in a secure context.
|
||||
*
|
||||
* @throws {Error} If WebAuthn is not supported.
|
||||
*/
|
||||
export function assertWebAuthnSupport() {
|
||||
// Is the navigator exposing the credentials API?
|
||||
if ("credentials" in navigator) return;
|
||||
|
||||
if (window.location.protocol === "http:" && window.location.hostname !== "localhost") {
|
||||
throw new Error("WebAuthn requires this page to be accessed via HTTPS.");
|
||||
}
|
||||
throw new Error("WebAuthn not supported by browser.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms items in the credentialCreateOptions generated on the server
|
||||
* into byte arrays expected by the navigator.credentials.create() call
|
||||
* @param {PublicKeyCredentialCreationOptions} credentialCreateOptions
|
||||
* @param {string} userID
|
||||
* @returns {PublicKeyCredentialCreationOptions}
|
||||
*/
|
||||
export function transformCredentialCreateOptions(credentialCreateOptions, userID) {
|
||||
const user = credentialCreateOptions.user;
|
||||
// Because json can't contain raw bytes, the server base64-encodes the User ID
|
||||
// So to get the base64 encoded byte array, we first need to convert it to a regular
|
||||
// string, then a byte array, re-encode it and wrap that in an array.
|
||||
const stringId = decodeURIComponent(window.atob(userID));
|
||||
|
||||
user.id = decodeBase64(encodeBase64(decodeBase64(stringId)));
|
||||
const challenge = decodeBase64(credentialCreateOptions.challenge.toString());
|
||||
|
||||
return {
|
||||
...credentialCreateOptions,
|
||||
challenge,
|
||||
user,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the binary data in the credential into base64 strings
|
||||
* for posting to the server.
|
||||
*
|
||||
* @param {PublicKeyCredential} newAssertion
|
||||
* @returns {Assertion}
|
||||
*/
|
||||
export function transformNewAssertionForServer(newAssertion) {
|
||||
const response = /** @type {AuthenticatorAttestationResponse} */ (newAssertion.response);
|
||||
|
||||
const attObj = new Uint8Array(response.attestationObject);
|
||||
const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON);
|
||||
const rawId = new Uint8Array(newAssertion.rawId);
|
||||
|
||||
const registrationClientExtensions = newAssertion.getClientExtensionResults();
|
||||
|
||||
return {
|
||||
id: newAssertion.id,
|
||||
rawId: encodeBase64(rawId),
|
||||
type: newAssertion.type,
|
||||
registrationClientExtensions: JSON.stringify(registrationClientExtensions),
|
||||
response: {
|
||||
clientDataJSON: encodeBase64(clientDataJSON),
|
||||
attestationObject: encodeBase64(attObj),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the items in the credentialRequestOptions generated on the server
|
||||
*
|
||||
* @param {PublicKeyCredentialRequestOptions} credentialRequestOptions
|
||||
* @returns {PublicKeyCredentialRequestOptions}
|
||||
*/
|
||||
export function transformCredentialRequestOptions(credentialRequestOptions) {
|
||||
const challenge = decodeBase64(credentialRequestOptions.challenge.toString());
|
||||
|
||||
const allowCredentials = (credentialRequestOptions.allowCredentials || []).map(
|
||||
(credentialDescriptor) => {
|
||||
const id = decodeBase64(credentialDescriptor.id.toString());
|
||||
return Object.assign({}, credentialDescriptor, { id });
|
||||
},
|
||||
);
|
||||
|
||||
return Object.assign({}, credentialRequestOptions, {
|
||||
challenge,
|
||||
allowCredentials,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the binary data in the assertion into strings for posting to the server.
|
||||
* @param {PublicKeyCredential} newAssertion
|
||||
* @returns {AuthAssertion}
|
||||
*/
|
||||
export function transformAssertionForServer(newAssertion) {
|
||||
const response = /** @type {AuthenticatorAssertionResponse} */ (newAssertion.response);
|
||||
|
||||
const authData = new Uint8Array(response.authenticatorData);
|
||||
const clientDataJSON = new Uint8Array(response.clientDataJSON);
|
||||
const rawId = new Uint8Array(newAssertion.rawId);
|
||||
const sig = new Uint8Array(response.signature);
|
||||
const assertionClientExtensions = newAssertion.getClientExtensionResults();
|
||||
|
||||
return {
|
||||
id: newAssertion.id,
|
||||
rawId: encodeBase64(rawId),
|
||||
type: newAssertion.type,
|
||||
assertionClientExtensions: JSON.stringify(assertionClientExtensions),
|
||||
|
||||
response: {
|
||||
clientDataJSON: encodeBase64Raw(clientDataJSON),
|
||||
signature: encodeBase64Raw(sig),
|
||||
authenticatorData: encodeBase64Raw(authData),
|
||||
userHandle: null,
|
||||
},
|
||||
};
|
||||
}
|
@ -48,6 +48,9 @@ export default [
|
||||
"lit/no-template-bind": "error",
|
||||
"no-unused-vars": "off",
|
||||
"no-console": ["error", { allow: ["debug", "warn", "error"] }],
|
||||
// TODO: TypeScript already handles this.
|
||||
// Remove after project-wide ESLint config is properly set up.
|
||||
"no-undef": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
@ -71,8 +74,18 @@ export default [
|
||||
...globals.node,
|
||||
},
|
||||
},
|
||||
files: ["scripts/**/*.mjs", "*.ts", "*.mjs"],
|
||||
files: [
|
||||
// TODO:Remove after project-wide ESLint config is properly set up.
|
||||
"scripts/**/*.mjs",
|
||||
"authentication/**/*.js",
|
||||
"sfe/**/*.js",
|
||||
"*.ts",
|
||||
"*.mjs",
|
||||
],
|
||||
rules: {
|
||||
"no-undef": "off",
|
||||
// TODO: TypeScript already handles this.
|
||||
// Remove after project-wide ESLint config is properly set up.
|
||||
"no-unused-vars": "off",
|
||||
// We WANT our scripts to output to the console!
|
||||
"no-console": "off",
|
||||
|
1914
web/package-lock.json
generated
1914
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -57,9 +57,14 @@
|
||||
"ts-pattern": "^5.4.0",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"webcomponent-qr-code": "^1.2.0",
|
||||
"yaml": "^2.5.1"
|
||||
"yaml": "^2.5.1",
|
||||
"bootstrap": "^4.6.1",
|
||||
"formdata-polyfill": "^4.0.10",
|
||||
"jquery": "^3.7.1",
|
||||
"weakmap-polyfill": "^2.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jquery": "^3.5.31",
|
||||
"@eslint/js": "^9.11.1",
|
||||
"@hcaptcha/types": "^1.0.4",
|
||||
"@lit/localize-tools": "^0.8.0",
|
||||
@ -90,6 +95,8 @@
|
||||
"@wdio/spec-reporter": "^9.1.2",
|
||||
"chromedriver": "^131.0.1",
|
||||
"esbuild": "^0.25.0",
|
||||
"esbuild-plugin-copy": "^2.1.1",
|
||||
"esbuild-plugin-es5": "^2.1.1",
|
||||
"esbuild-plugin-polyfill-node": "^0.3.0",
|
||||
"esbuild-plugins-node-modules-polyfill": "^1.7.0",
|
||||
"eslint": "^9.11.1",
|
||||
@ -161,6 +168,12 @@
|
||||
"watch": "run-s build-locales esbuild:watch"
|
||||
},
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
"./paths": "./paths.js",
|
||||
"./authentication": "./authentication/index.js",
|
||||
"./scripts/*": "./scripts/*.mjs"
|
||||
},
|
||||
"wireit": {
|
||||
"build": {
|
||||
"#comment": [
|
||||
@ -193,8 +206,7 @@
|
||||
"./dist/patternfly.min.css"
|
||||
],
|
||||
"dependencies": [
|
||||
"build-locales",
|
||||
"./packages/sfe:build"
|
||||
"build-locales"
|
||||
],
|
||||
"env": {
|
||||
"NODE_RUNNER": {
|
||||
@ -204,12 +216,7 @@
|
||||
}
|
||||
},
|
||||
"build:sfe": {
|
||||
"dependencies": [
|
||||
"./packages/sfe:build"
|
||||
],
|
||||
"files": [
|
||||
"./packages/sfe/**/*.ts"
|
||||
]
|
||||
"command": "node scripts/build-sfe.mjs"
|
||||
},
|
||||
"build-proxy": {
|
||||
"command": "node scripts/build-web.mjs --proxy",
|
||||
@ -242,11 +249,6 @@
|
||||
"lint:package"
|
||||
]
|
||||
},
|
||||
"format:packages": {
|
||||
"dependencies": [
|
||||
"./packages/sfe:prettier"
|
||||
]
|
||||
},
|
||||
"lint": {
|
||||
"command": "eslint --max-warnings 0 --fix",
|
||||
"env": {
|
||||
@ -274,11 +276,6 @@
|
||||
"shell": true,
|
||||
"command": "sh ./scripts/lint-lockfile.sh package-lock.json"
|
||||
},
|
||||
"lint:lockfiles": {
|
||||
"dependencies": [
|
||||
"./packages/sfe:lint:lockfile"
|
||||
]
|
||||
},
|
||||
"lint:package": {
|
||||
"command": "syncpack format -i ' '"
|
||||
},
|
||||
@ -314,9 +311,7 @@
|
||||
"lint:spelling",
|
||||
"lint:package",
|
||||
"lint:lockfile",
|
||||
"lint:lockfiles",
|
||||
"lint:precommit",
|
||||
"format:packages"
|
||||
"lint:precommit"
|
||||
]
|
||||
},
|
||||
"prettier": {
|
||||
|
@ -1,23 +0,0 @@
|
||||
{
|
||||
"arrowParens": "always",
|
||||
"bracketSpacing": true,
|
||||
"embeddedLanguageFormatting": "auto",
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"insertPragma": false,
|
||||
"jsxSingleQuote": false,
|
||||
"printWidth": 100,
|
||||
"proseWrap": "preserve",
|
||||
"quoteProps": "consistent",
|
||||
"requirePragma": false,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "all",
|
||||
"useTabs": false,
|
||||
"vueIndentScriptAndStyle": false,
|
||||
"plugins": ["@trivago/prettier-plugin-sort-imports"],
|
||||
"importOrder": ["^(@?)lit(.*)$", "\\.css$", "^@goauthentik/api$", "^[./]"],
|
||||
"importOrderSeparation": true,
|
||||
"importOrderSortSpecifiers": true,
|
||||
"importOrderParserPlugins": ["typescript", "classProperties", "decorators-legacy"]
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2024 Authentik Security, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||
including without limitation the rights to use, copy, modify, merge, publish, distribute,
|
||||
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
|
||||
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
|
||||
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
@ -1,68 +0,0 @@
|
||||
{
|
||||
"name": "@goauthentik/web-sfe",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@goauthentik/api": "^2024.6.0-1719577139",
|
||||
"base64-js": "^1.5.1",
|
||||
"bootstrap": "^4.6.1",
|
||||
"formdata-polyfill": "^4.0.10",
|
||||
"jquery": "^3.7.1",
|
||||
"weakmap-polyfill": "^2.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^28.0.0",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-swc": "^0.4.0",
|
||||
"@swc/cli": "^0.4.0",
|
||||
"@swc/core": "^1.7.28",
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"@types/jquery": "^3.5.31",
|
||||
"lockfile-lint": "^4.14.0",
|
||||
"prettier": "^3.3.2",
|
||||
"rollup": "^4.23.0",
|
||||
"rollup-plugin-copy": "^3.5.0",
|
||||
"wireit": "^0.14.9"
|
||||
},
|
||||
"license": "MIT",
|
||||
"optionalDependencies": {
|
||||
"@swc/core": "^1.7.28",
|
||||
"@swc/core-darwin-arm64": "^1.6.13",
|
||||
"@swc/core-darwin-x64": "^1.6.13",
|
||||
"@swc/core-linux-arm-gnueabihf": "^1.6.13",
|
||||
"@swc/core-linux-arm64-gnu": "^1.6.13",
|
||||
"@swc/core-linux-arm64-musl": "^1.6.13",
|
||||
"@swc/core-linux-x64-gnu": "^1.6.13",
|
||||
"@swc/core-linux-x64-musl": "^1.6.13",
|
||||
"@swc/core-win32-arm64-msvc": "^1.6.13",
|
||||
"@swc/core-win32-ia32-msvc": "^1.6.13",
|
||||
"@swc/core-win32-x64-msvc": "^1.6.13"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "wireit",
|
||||
"lint:lockfile": "wireit",
|
||||
"prettier": "prettier --write ./src ./tsconfig.json ./rollup.config.js ./package.json",
|
||||
"watch": "rollup -w -c rollup.config.js --bundleConfigAsCjs"
|
||||
},
|
||||
"wireit": {
|
||||
"build:sfe": {
|
||||
"command": "rollup -c rollup.config.js --bundleConfigAsCjs",
|
||||
"files": [
|
||||
"../../node_modules/bootstrap/dist/css/bootstrap.min.css",
|
||||
"src/index.ts"
|
||||
],
|
||||
"output": [
|
||||
"./dist/sfe/*"
|
||||
]
|
||||
},
|
||||
"build": {
|
||||
"command": "mkdir -p ../../dist/sfe && cp -r dist/sfe/* ../../dist/sfe",
|
||||
"dependencies": [
|
||||
"build:sfe"
|
||||
]
|
||||
},
|
||||
"lint:lockfile": {
|
||||
"command": "lockfile-lint --path package.json --type npm --allowed-hosts npm --validate-https"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
import commonjs from "@rollup/plugin-commonjs";
|
||||
import resolve from "@rollup/plugin-node-resolve";
|
||||
import swc from "@rollup/plugin-swc";
|
||||
import copy from "rollup-plugin-copy";
|
||||
|
||||
export default {
|
||||
input: "src/index.ts",
|
||||
output: {
|
||||
dir: "./dist/sfe",
|
||||
format: "cjs",
|
||||
},
|
||||
context: "window",
|
||||
plugins: [
|
||||
copy({
|
||||
targets: [
|
||||
{
|
||||
src: "../../node_modules/bootstrap/dist/css/bootstrap.min.css",
|
||||
dest: "./dist/sfe",
|
||||
},
|
||||
],
|
||||
}),
|
||||
resolve({ browser: true }),
|
||||
commonjs(),
|
||||
swc({
|
||||
swc: {
|
||||
jsc: {
|
||||
loose: false,
|
||||
externalHelpers: false,
|
||||
// Requires v1.2.50 or upper and requires target to be es2016 or upper.
|
||||
keepClassNames: false,
|
||||
},
|
||||
minify: false,
|
||||
env: {
|
||||
targets: {
|
||||
edge: "17",
|
||||
ie: "11",
|
||||
},
|
||||
mode: "entry",
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
@ -1,527 +0,0 @@
|
||||
import { fromByteArray } from "base64-js";
|
||||
import "formdata-polyfill";
|
||||
import $ from "jquery";
|
||||
import "weakmap-polyfill";
|
||||
|
||||
import {
|
||||
type AuthenticatorValidationChallenge,
|
||||
type AutosubmitChallenge,
|
||||
type ChallengeTypes,
|
||||
ChallengeTypesFromJSON,
|
||||
type ContextualFlowInfo,
|
||||
type DeviceChallenge,
|
||||
type ErrorDetail,
|
||||
type IdentificationChallenge,
|
||||
type PasswordChallenge,
|
||||
type RedirectChallenge,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
interface GlobalAuthentik {
|
||||
brand: {
|
||||
branding_logo: string;
|
||||
};
|
||||
api: {
|
||||
base: string;
|
||||
};
|
||||
}
|
||||
|
||||
function ak(): GlobalAuthentik {
|
||||
return (
|
||||
window as unknown as {
|
||||
authentik: GlobalAuthentik;
|
||||
}
|
||||
).authentik;
|
||||
}
|
||||
|
||||
class SimpleFlowExecutor {
|
||||
challenge?: ChallengeTypes;
|
||||
flowSlug: string;
|
||||
container: HTMLDivElement;
|
||||
|
||||
constructor(container: HTMLDivElement) {
|
||||
this.flowSlug = window.location.pathname.split("/")[3];
|
||||
this.container = container;
|
||||
}
|
||||
|
||||
get apiURL() {
|
||||
return `${ak().api.base}api/v3/flows/executor/${this.flowSlug}/?query=${encodeURIComponent(window.location.search.substring(1))}`;
|
||||
}
|
||||
|
||||
start() {
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url: this.apiURL,
|
||||
success: (data) => {
|
||||
this.challenge = ChallengeTypesFromJSON(data);
|
||||
this.renderChallenge();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
submit(data: { [key: string]: unknown } | FormData) {
|
||||
$("button[type=submit]").addClass("disabled")
|
||||
.html(`<span class="spinner-border spinner-border-sm" aria-hidden="true"></span>
|
||||
<span role="status">Loading...</span>`);
|
||||
let finalData: { [key: string]: unknown } = {};
|
||||
if (data instanceof FormData) {
|
||||
finalData = {};
|
||||
data.forEach((value, key) => {
|
||||
finalData[key] = value;
|
||||
});
|
||||
} else {
|
||||
finalData = data;
|
||||
}
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: this.apiURL,
|
||||
data: JSON.stringify(finalData),
|
||||
success: (data) => {
|
||||
this.challenge = ChallengeTypesFromJSON(data);
|
||||
this.renderChallenge();
|
||||
},
|
||||
contentType: "application/json",
|
||||
dataType: "json",
|
||||
});
|
||||
}
|
||||
|
||||
renderChallenge() {
|
||||
switch (this.challenge?.component) {
|
||||
case "ak-stage-identification":
|
||||
new IdentificationStage(this, this.challenge).render();
|
||||
return;
|
||||
case "ak-stage-password":
|
||||
new PasswordStage(this, this.challenge).render();
|
||||
return;
|
||||
case "xak-flow-redirect":
|
||||
new RedirectStage(this, this.challenge).render();
|
||||
return;
|
||||
case "ak-stage-autosubmit":
|
||||
new AutosubmitStage(this, this.challenge).render();
|
||||
return;
|
||||
case "ak-stage-authenticator-validate":
|
||||
new AuthenticatorValidateStage(this, this.challenge).render();
|
||||
return;
|
||||
default:
|
||||
this.container.innerText = "Unsupported stage: " + this.challenge?.component;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface FlowInfoChallenge {
|
||||
flowInfo?: ContextualFlowInfo;
|
||||
responseErrors?: {
|
||||
[key: string]: Array<ErrorDetail>;
|
||||
};
|
||||
}
|
||||
|
||||
class Stage<T extends FlowInfoChallenge> {
|
||||
constructor(
|
||||
public executor: SimpleFlowExecutor,
|
||||
public challenge: T,
|
||||
) {}
|
||||
|
||||
error(fieldName: string) {
|
||||
if (!this.challenge.responseErrors) {
|
||||
return [];
|
||||
}
|
||||
return this.challenge.responseErrors[fieldName] || [];
|
||||
}
|
||||
|
||||
renderInputError(fieldName: string) {
|
||||
return `${this.error(fieldName)
|
||||
.map((error) => {
|
||||
return `<div class="invalid-feedback">
|
||||
${error.string}
|
||||
</div>`;
|
||||
})
|
||||
.join("")}`;
|
||||
}
|
||||
|
||||
renderNonFieldErrors() {
|
||||
return `${this.error("non_field_errors")
|
||||
.map((error) => {
|
||||
return `<div class="alert alert-danger" role="alert">
|
||||
${error.string}
|
||||
</div>`;
|
||||
})
|
||||
.join("")}`;
|
||||
}
|
||||
|
||||
html(html: string) {
|
||||
this.executor.container.innerHTML = html;
|
||||
}
|
||||
|
||||
render() {
|
||||
throw new Error("Abstract method");
|
||||
}
|
||||
}
|
||||
|
||||
const IS_INVALID = "is-invalid";
|
||||
|
||||
class IdentificationStage extends Stage<IdentificationChallenge> {
|
||||
render() {
|
||||
this.html(`
|
||||
<form id="ident-form">
|
||||
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
|
||||
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
|
||||
${
|
||||
this.challenge.applicationPre
|
||||
? `<p>
|
||||
Log in to continue to ${this.challenge.applicationPre}.
|
||||
</p>`
|
||||
: ""
|
||||
}
|
||||
<div class="form-label-group my-3 has-validation">
|
||||
<input type="text" autofocus class="form-control" name="uid_field" placeholder="Email / Username">
|
||||
</div>
|
||||
${
|
||||
this.challenge.passwordFields
|
||||
? `<div class="form-label-group my-3 has-validation">
|
||||
<input type="password" class="form-control ${this.error("password").length > 0 ? IS_INVALID : ""}" name="password" placeholder="Password">
|
||||
${this.renderInputError("password")}
|
||||
</div>`
|
||||
: ""
|
||||
}
|
||||
${this.renderNonFieldErrors()}
|
||||
<button class="btn btn-primary w-100 py-2" type="submit">${this.challenge.primaryAction}</button>
|
||||
</form>`);
|
||||
$("#ident-form input[name=uid_field]").trigger("focus");
|
||||
$("#ident-form").on("submit", (ev) => {
|
||||
ev.preventDefault();
|
||||
const data = new FormData(ev.target as HTMLFormElement);
|
||||
this.executor.submit(data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class PasswordStage extends Stage<PasswordChallenge> {
|
||||
render() {
|
||||
this.html(`
|
||||
<form id="password-form">
|
||||
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
|
||||
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
|
||||
<div class="form-label-group my-3 has-validation">
|
||||
<input type="password" autofocus class="form-control ${this.error("password").length > 0 ? IS_INVALID : ""}" name="password" placeholder="Password">
|
||||
${this.renderInputError("password")}
|
||||
</div>
|
||||
<button class="btn btn-primary w-100 py-2" type="submit">Continue</button>
|
||||
</form>`);
|
||||
$("#password-form input").trigger("focus");
|
||||
$("#password-form").on("submit", (ev) => {
|
||||
ev.preventDefault();
|
||||
const data = new FormData(ev.target as HTMLFormElement);
|
||||
this.executor.submit(data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class RedirectStage extends Stage<RedirectChallenge> {
|
||||
render() {
|
||||
window.location.assign(this.challenge.to);
|
||||
}
|
||||
}
|
||||
|
||||
class AutosubmitStage extends Stage<AutosubmitChallenge> {
|
||||
render() {
|
||||
this.html(`
|
||||
<form id="autosubmit-form" action="${this.challenge.url}" method="POST">
|
||||
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
|
||||
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
|
||||
${Object.entries(this.challenge.attrs).map(([key, value]) => {
|
||||
return `<input
|
||||
type="hidden"
|
||||
name="${key}"
|
||||
value="${value}"
|
||||
/>`;
|
||||
})}
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>`);
|
||||
$("#autosubmit-form").submit();
|
||||
}
|
||||
}
|
||||
|
||||
export interface Assertion {
|
||||
id: string;
|
||||
rawId: string;
|
||||
type: string;
|
||||
registrationClientExtensions: string;
|
||||
response: {
|
||||
clientDataJSON: string;
|
||||
attestationObject: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AuthAssertion {
|
||||
id: string;
|
||||
rawId: string;
|
||||
type: string;
|
||||
assertionClientExtensions: string;
|
||||
response: {
|
||||
clientDataJSON: string;
|
||||
authenticatorData: string;
|
||||
signature: string;
|
||||
userHandle: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge> {
|
||||
deviceChallenge?: DeviceChallenge;
|
||||
|
||||
b64enc(buf: Uint8Array): string {
|
||||
return fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
||||
}
|
||||
|
||||
b64RawEnc(buf: Uint8Array): string {
|
||||
return fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_");
|
||||
}
|
||||
|
||||
u8arr(input: string): Uint8Array {
|
||||
return Uint8Array.from(atob(input.replace(/_/g, "/").replace(/-/g, "+")), (c) =>
|
||||
c.charCodeAt(0),
|
||||
);
|
||||
}
|
||||
|
||||
checkWebAuthnSupport(): boolean {
|
||||
if ("credentials" in navigator) {
|
||||
return true;
|
||||
}
|
||||
if (window.location.protocol === "http:" && window.location.hostname !== "localhost") {
|
||||
console.warn("WebAuthn requires this page to be accessed via HTTPS.");
|
||||
return false;
|
||||
}
|
||||
console.warn("WebAuthn not supported by browser.");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms items in the credentialCreateOptions generated on the server
|
||||
* into byte arrays expected by the navigator.credentials.create() call
|
||||
*/
|
||||
transformCredentialCreateOptions(
|
||||
credentialCreateOptions: PublicKeyCredentialCreationOptions,
|
||||
userId: string,
|
||||
): PublicKeyCredentialCreationOptions {
|
||||
const user = credentialCreateOptions.user;
|
||||
// Because json can't contain raw bytes, the server base64-encodes the User ID
|
||||
// So to get the base64 encoded byte array, we first need to convert it to a regular
|
||||
// string, then a byte array, re-encode it and wrap that in an array.
|
||||
const stringId = decodeURIComponent(window.atob(userId));
|
||||
user.id = this.u8arr(this.b64enc(this.u8arr(stringId)));
|
||||
const challenge = this.u8arr(credentialCreateOptions.challenge.toString());
|
||||
|
||||
return Object.assign({}, credentialCreateOptions, {
|
||||
challenge,
|
||||
user,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the binary data in the credential into base64 strings
|
||||
* for posting to the server.
|
||||
* @param {PublicKeyCredential} newAssertion
|
||||
*/
|
||||
transformNewAssertionForServer(newAssertion: PublicKeyCredential): Assertion {
|
||||
const attObj = new Uint8Array(
|
||||
(newAssertion.response as AuthenticatorAttestationResponse).attestationObject,
|
||||
);
|
||||
const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON);
|
||||
const rawId = new Uint8Array(newAssertion.rawId);
|
||||
|
||||
const registrationClientExtensions = newAssertion.getClientExtensionResults();
|
||||
return {
|
||||
id: newAssertion.id,
|
||||
rawId: this.b64enc(rawId),
|
||||
type: newAssertion.type,
|
||||
registrationClientExtensions: JSON.stringify(registrationClientExtensions),
|
||||
response: {
|
||||
clientDataJSON: this.b64enc(clientDataJSON),
|
||||
attestationObject: this.b64enc(attObj),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
transformCredentialRequestOptions(
|
||||
credentialRequestOptions: PublicKeyCredentialRequestOptions,
|
||||
): PublicKeyCredentialRequestOptions {
|
||||
const challenge = this.u8arr(credentialRequestOptions.challenge.toString());
|
||||
|
||||
const allowCredentials = (credentialRequestOptions.allowCredentials || []).map(
|
||||
(credentialDescriptor) => {
|
||||
const id = this.u8arr(credentialDescriptor.id.toString());
|
||||
return Object.assign({}, credentialDescriptor, { id });
|
||||
},
|
||||
);
|
||||
|
||||
return Object.assign({}, credentialRequestOptions, {
|
||||
challenge,
|
||||
allowCredentials,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the binary data in the assertion into strings for posting to the server.
|
||||
* @param {PublicKeyCredential} newAssertion
|
||||
*/
|
||||
transformAssertionForServer(newAssertion: PublicKeyCredential): AuthAssertion {
|
||||
const response = newAssertion.response as AuthenticatorAssertionResponse;
|
||||
const authData = new Uint8Array(response.authenticatorData);
|
||||
const clientDataJSON = new Uint8Array(response.clientDataJSON);
|
||||
const rawId = new Uint8Array(newAssertion.rawId);
|
||||
const sig = new Uint8Array(response.signature);
|
||||
const assertionClientExtensions = newAssertion.getClientExtensionResults();
|
||||
|
||||
return {
|
||||
id: newAssertion.id,
|
||||
rawId: this.b64enc(rawId),
|
||||
type: newAssertion.type,
|
||||
assertionClientExtensions: JSON.stringify(assertionClientExtensions),
|
||||
|
||||
response: {
|
||||
clientDataJSON: this.b64RawEnc(clientDataJSON),
|
||||
signature: this.b64RawEnc(sig),
|
||||
authenticatorData: this.b64RawEnc(authData),
|
||||
userHandle: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.deviceChallenge) {
|
||||
return this.renderChallengePicker();
|
||||
}
|
||||
switch (this.deviceChallenge.deviceClass) {
|
||||
case "static":
|
||||
case "totp":
|
||||
this.renderCodeInput();
|
||||
break;
|
||||
case "webauthn":
|
||||
this.renderWebauthn();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
renderChallengePicker() {
|
||||
const challenges = this.challenge.deviceChallenges.filter((challenge) =>
|
||||
challenge.deviceClass === "webauthn" && !this.checkWebAuthnSupport()
|
||||
? undefined
|
||||
: challenge,
|
||||
);
|
||||
this.html(`<form id="picker-form">
|
||||
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
|
||||
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
|
||||
${
|
||||
challenges.length > 0
|
||||
? "<p>Select an authentication method.</p>"
|
||||
: `
|
||||
<p>No compatible authentication method available</p>
|
||||
`
|
||||
}
|
||||
${challenges
|
||||
.map((challenge) => {
|
||||
let label = undefined;
|
||||
switch (challenge.deviceClass) {
|
||||
case "static":
|
||||
label = "Recovery keys";
|
||||
break;
|
||||
case "totp":
|
||||
label = "Traditional authenticator";
|
||||
break;
|
||||
case "webauthn":
|
||||
label = "Security key";
|
||||
break;
|
||||
}
|
||||
if (!label) {
|
||||
return "";
|
||||
}
|
||||
return `<div class="form-label-group my-3 has-validation">
|
||||
<button id="${challenge.deviceClass}-${challenge.deviceUid}" class="btn btn-secondary w-100 py-2" type="button">
|
||||
${label}
|
||||
</button>
|
||||
</div>`;
|
||||
})
|
||||
.join("")}
|
||||
</form>`);
|
||||
this.challenge.deviceChallenges.forEach((challenge) => {
|
||||
$(`#picker-form button#${challenge.deviceClass}-${challenge.deviceUid}`).on(
|
||||
"click",
|
||||
() => {
|
||||
this.deviceChallenge = challenge;
|
||||
this.render();
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
renderCodeInput() {
|
||||
this.html(`
|
||||
<form id="totp-form">
|
||||
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
|
||||
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
|
||||
<div class="form-label-group my-3 has-validation">
|
||||
<input type="text" autofocus class="form-control ${this.error("code").length > 0 ? IS_INVALID : ""}" name="code" placeholder="Please enter your code" autocomplete="one-time-code">
|
||||
${this.renderInputError("code")}
|
||||
</div>
|
||||
<button class="btn btn-primary w-100 py-2" type="submit">Continue</button>
|
||||
</form>`);
|
||||
$("#totp-form input").trigger("focus");
|
||||
$("#totp-form").on("submit", (ev) => {
|
||||
ev.preventDefault();
|
||||
const data = new FormData(ev.target as HTMLFormElement);
|
||||
this.executor.submit(data);
|
||||
});
|
||||
}
|
||||
|
||||
renderWebauthn() {
|
||||
this.html(`
|
||||
<form id="totp-form">
|
||||
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
|
||||
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
`);
|
||||
navigator.credentials
|
||||
.get({
|
||||
publicKey: this.transformCredentialRequestOptions(
|
||||
this.deviceChallenge?.challenge as PublicKeyCredentialRequestOptions,
|
||||
),
|
||||
})
|
||||
.then((assertion) => {
|
||||
if (!assertion) {
|
||||
throw new Error("No assertion");
|
||||
}
|
||||
try {
|
||||
// we now have an authentication assertion! encode the byte arrays contained
|
||||
// in the assertion data as strings for posting to the server
|
||||
const transformedAssertionForServer = this.transformAssertionForServer(
|
||||
assertion as PublicKeyCredential,
|
||||
);
|
||||
|
||||
// post the assertion to the server for verification.
|
||||
this.executor.submit({
|
||||
webauthn: transformedAssertionForServer,
|
||||
});
|
||||
} catch (err) {
|
||||
throw new Error(`Error when validating assertion on server: ${err}`);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn(error);
|
||||
this.deviceChallenge = undefined;
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const sfe = new SimpleFlowExecutor($("#flow-sfe-container")[0] as HTMLDivElement);
|
||||
sfe.start();
|
@ -1,7 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"types": ["jquery"],
|
||||
"esModuleInterop": true,
|
||||
"lib": ["DOM", "ES2015", "ES2017"]
|
||||
}
|
||||
}
|
25
web/paths.js
Normal file
25
web/paths.js
Normal file
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* @file Path constants for the web package.
|
||||
*/
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
/**
|
||||
* @typedef {'@goauthentik/web'} WebPackageIdentifier
|
||||
*/
|
||||
|
||||
/**
|
||||
* The root of the web package.
|
||||
*/
|
||||
export const PackageRoot = /** @type {WebPackageIdentifier} */ (resolve(__dirname));
|
||||
|
||||
/**
|
||||
* Path to the web package's distribution directory.
|
||||
*
|
||||
* This is where the built files are located after running the build process.
|
||||
*/
|
||||
export const DistDirectory = /** @type {`${WebPackageIdentifier}/dist`} */ (
|
||||
resolve(__dirname, "dist")
|
||||
);
|
90
web/scripts/build-sfe.mjs
Normal file
90
web/scripts/build-sfe.mjs
Normal file
@ -0,0 +1,90 @@
|
||||
/**
|
||||
* @file Build script for the simplified flow executor (SFE).
|
||||
*/
|
||||
import { DistDirectory, PackageRoot } from "@goauthentik/web/paths";
|
||||
import esbuild from "esbuild";
|
||||
import copy from "esbuild-plugin-copy";
|
||||
import { es5Plugin } from "esbuild-plugin-es5";
|
||||
import { createRequire } from "node:module";
|
||||
import * as path from "node:path";
|
||||
|
||||
/**
|
||||
* Builds the Simplified Flow Executor bundle.
|
||||
*
|
||||
* @remarks
|
||||
* The output directory and file names are referenced by the backend.
|
||||
* @see {@link ../../authentik/flows/templates/if/flow-sfe.html}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function buildSFE() {
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
const sourceDirectory = path.join(PackageRoot, "sfe");
|
||||
|
||||
const entryPoint = path.join(sourceDirectory, "main.js");
|
||||
const outDirectory = path.join(DistDirectory, "sfe");
|
||||
|
||||
const bootstrapCSSPath = require.resolve(
|
||||
path.join("bootstrap", "dist", "css", "bootstrap.min.css"),
|
||||
);
|
||||
|
||||
/**
|
||||
* @type {esbuild.BuildOptions}
|
||||
*/
|
||||
const config = {
|
||||
tsconfig: path.join(sourceDirectory, "tsconfig.json"),
|
||||
entryPoints: [entryPoint],
|
||||
minify: false,
|
||||
bundle: true,
|
||||
sourcemap: true,
|
||||
treeShaking: true,
|
||||
legalComments: "external",
|
||||
platform: "browser",
|
||||
format: "iife",
|
||||
alias: {
|
||||
"@swc/helpers": path.dirname(require.resolve("@swc/helpers/package.json")),
|
||||
},
|
||||
banner: {
|
||||
js: [
|
||||
// ---
|
||||
"// Simplified Flow Executor (SFE)",
|
||||
`// Bundled on ${new Date().toISOString()}`,
|
||||
"// @ts-nocheck",
|
||||
"",
|
||||
].join("\n"),
|
||||
},
|
||||
plugins: [
|
||||
copy({
|
||||
assets: [
|
||||
{
|
||||
from: bootstrapCSSPath,
|
||||
to: outDirectory,
|
||||
},
|
||||
],
|
||||
}),
|
||||
es5Plugin({
|
||||
swc: {
|
||||
jsc: {
|
||||
loose: false,
|
||||
externalHelpers: false,
|
||||
keepClassNames: false,
|
||||
},
|
||||
minify: false,
|
||||
},
|
||||
}),
|
||||
],
|
||||
target: ["es5"],
|
||||
outdir: outDirectory,
|
||||
};
|
||||
|
||||
esbuild.build(config);
|
||||
}
|
||||
|
||||
buildSFE()
|
||||
.then(() => {
|
||||
console.log("Build complete");
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Build failed", error);
|
||||
process.exit(1);
|
||||
});
|
@ -1,3 +1,4 @@
|
||||
import { DistDirectory, PackageRoot } from "@goauthentik/web/paths";
|
||||
import { execFileSync } from "child_process";
|
||||
import { deepmerge } from "deepmerge-ts";
|
||||
import esbuild from "esbuild";
|
||||
@ -170,7 +171,7 @@ function composeVersionID() {
|
||||
* @throws {Error} on build failure
|
||||
*/
|
||||
function createEntryPointOptions([source, dest], overrides = {}) {
|
||||
const outdir = path.join(__dirname, "..", "dist", dest);
|
||||
const outdir = path.join(DistDirectory, dest);
|
||||
|
||||
/**
|
||||
* @type {esbuild.BuildOptions}
|
||||
@ -233,7 +234,7 @@ async function doWatch() {
|
||||
buildObserverPlugin({
|
||||
serverURL,
|
||||
logPrefix: entryPoint[1],
|
||||
relativeRoot: path.join(__dirname, ".."),
|
||||
relativeRoot: PackageRoot,
|
||||
}),
|
||||
],
|
||||
define: {
|
||||
|
@ -13,7 +13,6 @@ const MAX_PARAMS = 5;
|
||||
// const MAX_COGNITIVE_COMPLEXITY = 9;
|
||||
|
||||
const rules = {
|
||||
"no-param-reassign": "error",
|
||||
"accessor-pairs": "error",
|
||||
"array-callback-return": "error",
|
||||
"block-scoped-var": "error",
|
||||
@ -85,6 +84,7 @@ const rules = {
|
||||
"no-obj-calls": "error",
|
||||
"no-octal": "error",
|
||||
"no-octal-escape": "error",
|
||||
"no-param-reassign": "error",
|
||||
"no-proto": "error",
|
||||
"no-redeclare": "error",
|
||||
"no-regex-spaces": "error",
|
||||
@ -134,7 +134,6 @@ const rules = {
|
||||
// "sonarjs/cognitive-complexity": ["off", MAX_COGNITIVE_COMPLEXITY],
|
||||
// "sonarjs/no-duplicate-string": "off",
|
||||
// "sonarjs/no-nested-template-literals": "off",
|
||||
" @typescript-eslint/no-namespace": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
|
@ -48,7 +48,6 @@ export default [
|
||||
// "sonarjs/no-duplicate-string": "off",
|
||||
// "sonarjs/no-nested-template-literals": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
|
191
web/sfe/lib/AuthenticatorValidateStage.js
Normal file
191
web/sfe/lib/AuthenticatorValidateStage.js
Normal file
@ -0,0 +1,191 @@
|
||||
/**
|
||||
* @import { AuthenticatorValidationChallenge, DeviceChallenge } from "@goauthentik/api";
|
||||
* @import { FlowExecutor } from './Stage.js';
|
||||
*/
|
||||
import {
|
||||
isWebAuthnSupported,
|
||||
transformAssertionForServer,
|
||||
transformCredentialRequestOptions,
|
||||
} from "@goauthentik/web/authentication";
|
||||
import $ from "jquery";
|
||||
|
||||
import { Stage } from "./Stage.js";
|
||||
import { ak } from "./utils.js";
|
||||
|
||||
//@ts-check
|
||||
|
||||
/**
|
||||
* @template {AuthenticatorValidationChallenge} T
|
||||
* @extends {Stage<T>}
|
||||
*/
|
||||
export class AuthenticatorValidateStage extends Stage {
|
||||
/**
|
||||
* @param {FlowExecutor} executor - The executor for this stage
|
||||
* @param {T} challenge - The challenge for this stage
|
||||
*/
|
||||
constructor(executor, challenge) {
|
||||
super(executor, challenge);
|
||||
|
||||
/**
|
||||
* @type {DeviceChallenge | null}
|
||||
*/
|
||||
this.deviceChallenge = null;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.deviceChallenge) {
|
||||
this.renderChallengePicker();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (this.deviceChallenge.deviceClass) {
|
||||
case "static":
|
||||
case "totp":
|
||||
this.renderCodeInput();
|
||||
break;
|
||||
case "webauthn":
|
||||
this.renderWebauthn();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
renderChallengePicker() {
|
||||
const challenges = this.challenge.deviceChallenges.filter((challenge) =>
|
||||
challenge.deviceClass === "webauthn" && !isWebAuthnSupported() ? undefined : challenge,
|
||||
);
|
||||
|
||||
this.html(/* html */ `<form id="picker-form">
|
||||
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
|
||||
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
|
||||
${
|
||||
challenges.length > 0
|
||||
? /* html */ `<p>Select an authentication method.</p>`
|
||||
: /* html */ `<p>No compatible authentication method available</p>`
|
||||
}
|
||||
${challenges
|
||||
.map((challenge) => {
|
||||
let label = undefined;
|
||||
|
||||
switch (challenge.deviceClass) {
|
||||
case "static":
|
||||
label = "Recovery keys";
|
||||
break;
|
||||
case "totp":
|
||||
label = "Traditional authenticator";
|
||||
break;
|
||||
case "webauthn":
|
||||
label = "Security key";
|
||||
break;
|
||||
}
|
||||
|
||||
if (!label) return "";
|
||||
|
||||
return /* html */ `<div class="form-label-group my-3 has-validation">
|
||||
<button id="${challenge.deviceClass}-${challenge.deviceUid}" class="btn btn-secondary w-100 py-2" type="button">
|
||||
${label}
|
||||
</button>
|
||||
</div>`;
|
||||
})
|
||||
.join("")}
|
||||
</form>`);
|
||||
|
||||
this.challenge.deviceChallenges.forEach((challenge) => {
|
||||
$(`#picker-form button#${challenge.deviceClass}-${challenge.deviceUid}`).on(
|
||||
"click",
|
||||
() => {
|
||||
this.deviceChallenge = challenge;
|
||||
this.render();
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
renderCodeInput() {
|
||||
this.html(/* html */ `
|
||||
<form id="totp-form">
|
||||
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
|
||||
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
|
||||
<div class="form-label-group my-3 has-validation">
|
||||
<input type="text" autofocus class="form-control ${this.error("code").length > 0 ? "is-invalid" : ""}" name="code" placeholder="Please enter your code" autocomplete="one-time-code">
|
||||
${this.renderInputError("code")}
|
||||
</div>
|
||||
<button class="btn btn-primary w-100 py-2" type="submit">Continue</button>
|
||||
</form>`);
|
||||
|
||||
$("#totp-form input").trigger("focus");
|
||||
|
||||
$("#totp-form").on("submit", (ev) => {
|
||||
ev.preventDefault();
|
||||
|
||||
const target = /** @type {HTMLFormElement} */ (ev.target);
|
||||
|
||||
const data = new FormData(target);
|
||||
this.executor.submit(data);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
renderWebauthn() {
|
||||
this.html(/* html */ `
|
||||
<form id="totp-form">
|
||||
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
|
||||
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
`);
|
||||
|
||||
const challenge = /** @type {PublicKeyCredentialRequestOptions} */ (
|
||||
this.deviceChallenge?.challenge
|
||||
);
|
||||
|
||||
navigator.credentials
|
||||
.get({
|
||||
publicKey: transformCredentialRequestOptions(challenge),
|
||||
})
|
||||
.then((credential) => {
|
||||
if (!credential) {
|
||||
throw new Error("No assertion");
|
||||
}
|
||||
|
||||
if (credential.type !== "public-key") {
|
||||
throw new Error("Invalid assertion type");
|
||||
}
|
||||
|
||||
try {
|
||||
// We now have an authentication assertion!
|
||||
// Encode the byte arrays contained in the assertion data as strings
|
||||
// for posting to the server.
|
||||
const transformedAssertionForServer = transformAssertionForServer(
|
||||
/** @type {PublicKeyCredential} */ (credential),
|
||||
);
|
||||
|
||||
// Post the assertion to the server for verification.
|
||||
this.executor.submit({
|
||||
webauthn: transformedAssertionForServer,
|
||||
});
|
||||
} catch (err) {
|
||||
throw new Error(`Error when validating assertion on server: ${err}`);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn(error);
|
||||
|
||||
this.deviceChallenge = null;
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
}
|
35
web/sfe/lib/AutosubmitStage.js
Normal file
35
web/sfe/lib/AutosubmitStage.js
Normal file
@ -0,0 +1,35 @@
|
||||
/**
|
||||
* @import { AutosubmitChallenge } from "@goauthentik/api";
|
||||
*/
|
||||
import $ from "jquery";
|
||||
|
||||
import { Stage } from "./Stage.js";
|
||||
import { ak } from "./utils.js";
|
||||
|
||||
/**
|
||||
* @template {AutosubmitChallenge} T
|
||||
* @extends {Stage<T>}
|
||||
*/
|
||||
export class AutosubmitStage extends Stage {
|
||||
render() {
|
||||
this.html(/* html */ `
|
||||
<form id="autosubmit-form" action="${this.challenge.url}" method="POST">
|
||||
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
|
||||
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
|
||||
${Object.entries(this.challenge.attrs).map(([key, value]) => {
|
||||
return /* html */ `<input
|
||||
type="hidden"
|
||||
name="${key}"
|
||||
value="${value}"
|
||||
/>`;
|
||||
})}
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>`);
|
||||
|
||||
$("#autosubmit-form").submit();
|
||||
}
|
||||
}
|
50
web/sfe/lib/IdentificationStage.js
Normal file
50
web/sfe/lib/IdentificationStage.js
Normal file
@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @import { IdentificationChallenge } from "@goauthentik/api";
|
||||
*/
|
||||
import $ from "jquery";
|
||||
|
||||
import { Stage } from "./Stage.js";
|
||||
import { ak } from "./utils.js";
|
||||
|
||||
/**
|
||||
* @template {IdentificationChallenge} T
|
||||
* @extends {Stage<T>}
|
||||
*/
|
||||
export class IdentificationStage extends Stage {
|
||||
render() {
|
||||
this.html(/* html */ `
|
||||
<form id="ident-form">
|
||||
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
|
||||
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
|
||||
${
|
||||
this.challenge.applicationPre
|
||||
? /* html */ `<p>
|
||||
Log in to continue to ${this.challenge.applicationPre}.
|
||||
</p>`
|
||||
: ""
|
||||
}
|
||||
<div class="form-label-group my-3 has-validation">
|
||||
<input type="text" autofocus class="form-control" name="uid_field" placeholder="Email / Username">
|
||||
</div>
|
||||
${
|
||||
this.challenge.passwordFields
|
||||
? /* html */ `<div class="form-label-group my-3 has-validation">
|
||||
<input type="password" class="form-control ${this.error("password").length > 0 ? "is-invalid" : ""}" name="password" placeholder="Password">
|
||||
${this.renderInputError("password")}
|
||||
</div>`
|
||||
: ""
|
||||
}
|
||||
${this.renderNonFieldErrors()}
|
||||
<button class="btn btn-primary w-100 py-2" type="submit">${this.challenge.primaryAction}</button>
|
||||
</form>`);
|
||||
|
||||
$("#ident-form input[name=uid_field]").trigger("focus");
|
||||
$("#ident-form").on("submit", (ev) => {
|
||||
ev.preventDefault();
|
||||
const target = /** @type {HTMLFormElement} */ (ev.target);
|
||||
|
||||
const data = new FormData(target);
|
||||
this.executor.submit(data);
|
||||
});
|
||||
}
|
||||
}
|
37
web/sfe/lib/PasswordStage.js
Normal file
37
web/sfe/lib/PasswordStage.js
Normal file
@ -0,0 +1,37 @@
|
||||
/**
|
||||
* @import { PasswordChallenge } from "@goauthentik/api";
|
||||
*/
|
||||
import $ from "jquery";
|
||||
|
||||
import { Stage } from "./Stage.js";
|
||||
import { ak } from "./utils.js";
|
||||
|
||||
/**
|
||||
* @template {PasswordChallenge} T
|
||||
* @extends {Stage<T>}
|
||||
*/
|
||||
export class PasswordStage extends Stage {
|
||||
render() {
|
||||
this.html(/* html */ `
|
||||
<form id="password-form">
|
||||
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
|
||||
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
|
||||
<div class="form-label-group my-3 has-validation">
|
||||
<input type="password" autofocus class="form-control ${this.error("password").length > 0 ? "is-invalid" : ""}" name="password" placeholder="Password">
|
||||
${this.renderInputError("password")}
|
||||
</div>
|
||||
<button class="btn btn-primary w-100 py-2" type="submit">Continue</button>
|
||||
</form>`);
|
||||
|
||||
$("#password-form input").trigger("focus");
|
||||
|
||||
$("#password-form").on("submit", (ev) => {
|
||||
ev.preventDefault();
|
||||
|
||||
const target = /** @type {HTMLFormElement} */ (ev.target);
|
||||
|
||||
const data = new FormData(target);
|
||||
this.executor.submit(data);
|
||||
});
|
||||
}
|
||||
}
|
14
web/sfe/lib/RedirectStage.js
Normal file
14
web/sfe/lib/RedirectStage.js
Normal file
@ -0,0 +1,14 @@
|
||||
/**
|
||||
* @import { RedirectChallenge } from "@goauthentik/api";
|
||||
*/
|
||||
import { Stage } from "./Stage.js";
|
||||
|
||||
/**
|
||||
* @template {RedirectChallenge} T
|
||||
* @extends {Stage<T>}
|
||||
*/
|
||||
export class RedirectStage extends Stage {
|
||||
render() {
|
||||
window.location.assign(this.challenge.to);
|
||||
}
|
||||
}
|
113
web/sfe/lib/SimpleFlowExecutor.js
Normal file
113
web/sfe/lib/SimpleFlowExecutor.js
Normal file
@ -0,0 +1,113 @@
|
||||
/**
|
||||
* @import { ChallengeTypes } from "@goauthentik/api";
|
||||
* @import { FlowExecutor } from './Stage.js';
|
||||
*/
|
||||
import $ from "jquery";
|
||||
|
||||
import { ChallengeTypesFromJSON } from "@goauthentik/api";
|
||||
|
||||
import { AuthenticatorValidateStage } from "./AuthenticatorValidateStage.js";
|
||||
import { AutosubmitStage } from "./AutosubmitStage.js";
|
||||
import { IdentificationStage } from "./IdentificationStage.js";
|
||||
import { PasswordStage } from "./PasswordStage.js";
|
||||
import { RedirectStage } from "./RedirectStage.js";
|
||||
import { ak } from "./utils.js";
|
||||
|
||||
/**
|
||||
* Simple Flow Executor lifecycle.
|
||||
*
|
||||
* @implements {FlowExecutor}
|
||||
*/
|
||||
export class SimpleFlowExecutor {
|
||||
/**
|
||||
*
|
||||
* @param {HTMLDivElement} container
|
||||
*/
|
||||
constructor(container) {
|
||||
/**
|
||||
* @type {ChallengeTypes | null} The current challenge.
|
||||
*/
|
||||
this.challenge = null;
|
||||
/**
|
||||
* @type {string} The flow slug.
|
||||
*/
|
||||
this.flowSlug = window.location.pathname.split("/")[3] || "";
|
||||
/**
|
||||
* @type {HTMLDivElement} The container element for the flow executor.
|
||||
*/
|
||||
this.container = container;
|
||||
}
|
||||
|
||||
get apiURL() {
|
||||
return `${ak().api.base}api/v3/flows/executor/${this.flowSlug}/?query=${encodeURIComponent(window.location.search.substring(1))}`;
|
||||
}
|
||||
|
||||
start() {
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url: this.apiURL,
|
||||
success: (data) => {
|
||||
this.challenge = ChallengeTypesFromJSON(data);
|
||||
|
||||
this.renderChallenge();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the form data.
|
||||
* @param {Record<string, unknown> | FormData} payload
|
||||
*/
|
||||
submit(payload) {
|
||||
$("button[type=submit]").addClass("disabled")
|
||||
.html(`<span class="spinner-border spinner-border-sm" aria-hidden="true"></span>
|
||||
<span role="status">Loading...</span>`);
|
||||
/**
|
||||
* @type {Record<string, unknown>}
|
||||
*/
|
||||
let finalData;
|
||||
|
||||
if (payload instanceof FormData) {
|
||||
finalData = {};
|
||||
|
||||
payload.forEach((value, key) => {
|
||||
finalData[key] = value;
|
||||
});
|
||||
} else {
|
||||
finalData = payload;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: this.apiURL,
|
||||
data: JSON.stringify(finalData),
|
||||
success: (data) => {
|
||||
this.challenge = ChallengeTypesFromJSON(data);
|
||||
this.renderChallenge();
|
||||
},
|
||||
contentType: "application/json",
|
||||
dataType: "json",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
renderChallenge() {
|
||||
switch (this.challenge?.component) {
|
||||
case "ak-stage-identification":
|
||||
return new IdentificationStage(this, this.challenge).render();
|
||||
case "ak-stage-password":
|
||||
return new PasswordStage(this, this.challenge).render();
|
||||
case "xak-flow-redirect":
|
||||
return new RedirectStage(this, this.challenge).render();
|
||||
case "ak-stage-autosubmit":
|
||||
return new AutosubmitStage(this, this.challenge).render();
|
||||
case "ak-stage-authenticator-validate":
|
||||
return new AuthenticatorValidateStage(this, this.challenge).render();
|
||||
default:
|
||||
this.container.innerText = `Unsupported stage: ${this.challenge?.component}`;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
116
web/sfe/lib/Stage.js
Normal file
116
web/sfe/lib/Stage.js
Normal file
@ -0,0 +1,116 @@
|
||||
/**
|
||||
* @import { ContextualFlowInfo, ErrorDetail } from "@goauthentik/api";
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} FlowInfoChallenge
|
||||
* @property {ContextualFlowInfo} [flowInfo]
|
||||
* @property {Record<string, Array<ErrorDetail>>} [responseErrors]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
*/
|
||||
export class FlowExecutor {
|
||||
constructor() {
|
||||
/**
|
||||
* The DOM container element.
|
||||
*
|
||||
* @type {HTMLElement}
|
||||
* @abstract
|
||||
* @returns {void}
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
this.container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the form data.
|
||||
*
|
||||
* @param {Record<string, unknown> | FormData} data The data to submit.
|
||||
* @abstract
|
||||
* @returns {void}
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
submit(data) {
|
||||
throw new Error(`Method 'submit' not implemented in ${this.constructor.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a stage in a flow
|
||||
* @template {FlowInfoChallenge} T
|
||||
* @abstract
|
||||
*/
|
||||
export class Stage {
|
||||
/**
|
||||
* @param {FlowExecutor} executor - The executor for this stage
|
||||
* @param {T} challenge - The challenge for this stage
|
||||
*/
|
||||
constructor(executor, challenge) {
|
||||
/** @type {FlowExecutor} */
|
||||
this.executor = executor;
|
||||
|
||||
/** @type {T} */
|
||||
this.challenge = challenge;
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
* @param {string} fieldName
|
||||
*/
|
||||
error(fieldName) {
|
||||
if (!this.challenge.responseErrors) {
|
||||
return [];
|
||||
}
|
||||
return this.challenge.responseErrors[fieldName] || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
* @param {string} fieldName
|
||||
* @returns {string}
|
||||
*/
|
||||
renderInputError(fieldName) {
|
||||
return `${this.error(fieldName)
|
||||
.map((error) => {
|
||||
return /* html */ `<div class="invalid-feedback">
|
||||
${error.string}
|
||||
</div>`;
|
||||
})
|
||||
.join("")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
* @returns {string}
|
||||
*/
|
||||
renderNonFieldErrors() {
|
||||
return `${this.error("non_field_errors")
|
||||
.map((error) => {
|
||||
return /* html */ `<div class="alert alert-danger" role="alert">
|
||||
${error.string}
|
||||
</div>`;
|
||||
})
|
||||
.join("")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
* @param {string} innerHTML
|
||||
* @returns {void}
|
||||
*/
|
||||
html(innerHTML) {
|
||||
this.executor.container.innerHTML = innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the stage (must be implemented by subclasses)
|
||||
*
|
||||
* @abstract
|
||||
* @returns {void}
|
||||
*/
|
||||
render() {
|
||||
throw new Error("Abstract method");
|
||||
}
|
||||
}
|
12
web/sfe/lib/index.js
Normal file
12
web/sfe/lib/index.js
Normal file
@ -0,0 +1,12 @@
|
||||
/**
|
||||
* @file Simplified Flow Executor (SFE) library module.
|
||||
*/
|
||||
|
||||
export * from "./Stage.js";
|
||||
export * from "./SimpleFlowExecutor.js";
|
||||
export * from "./AuthenticatorValidateStage.js";
|
||||
export * from "./AutosubmitStage.js";
|
||||
export * from "./IdentificationStage.js";
|
||||
export * from "./PasswordStage.js";
|
||||
export * from "./RedirectStage.js";
|
||||
export * from "./utils.js";
|
20
web/sfe/lib/utils.js
Normal file
20
web/sfe/lib/utils.js
Normal file
@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @typedef {object} GlobalAuthentik
|
||||
* @property {object} brand
|
||||
* @property {string} brand.branding_logo
|
||||
* @property {object} api
|
||||
* @property {string} api.base
|
||||
*/
|
||||
|
||||
/**
|
||||
* Retrieves the global authentik object from the window.
|
||||
* @throws {Error} If the object not found
|
||||
* @returns {GlobalAuthentik}
|
||||
*/
|
||||
export function ak() {
|
||||
if (!("authentik" in window)) {
|
||||
throw new Error("No authentik object found in window");
|
||||
}
|
||||
|
||||
return /** @type {GlobalAuthentik} */ (window.authentik);
|
||||
}
|
17
web/sfe/main.js
Normal file
17
web/sfe/main.js
Normal file
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @file Simplified Flow Executor (SFE) entry point.
|
||||
*/
|
||||
import "formdata-polyfill";
|
||||
import $ from "jquery";
|
||||
|
||||
import { SimpleFlowExecutor } from "./lib/index.js";
|
||||
|
||||
const flowContainer = /** @type {HTMLDivElement} */ ($("#flow-sfe-container")[0]);
|
||||
|
||||
if (!flowContainer) {
|
||||
throw new Error("No flow container element found");
|
||||
}
|
||||
|
||||
const sfe = new SimpleFlowExecutor(flowContainer);
|
||||
|
||||
sfe.start();
|
46
web/sfe/tsconfig.json
Normal file
46
web/sfe/tsconfig.json
Normal file
@ -0,0 +1,46 @@
|
||||
{
|
||||
// TODO: Replace with @goauthentik/tsconfig after project compilation.
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@goauthentik/web/authentication": ["../authentication/index.js"]
|
||||
},
|
||||
"alwaysStrict": true,
|
||||
"baseUrl": ".",
|
||||
"rootDir": "../",
|
||||
"composite": true,
|
||||
"declaration": true,
|
||||
"allowJs": true,
|
||||
"declarationMap": true,
|
||||
"isolatedModules": true,
|
||||
"incremental": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["DOM", "ES2015", "ES2017"],
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"newLine": "lf",
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitOverride": false,
|
||||
"outDir": "${configDir}/out",
|
||||
"pretty": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"target": "ESNext",
|
||||
"useUnknownInCatchVariables": true
|
||||
},
|
||||
"exclude": [
|
||||
// ---
|
||||
"./out/**/*",
|
||||
"./dist/**/*"
|
||||
],
|
||||
"include": [
|
||||
// ---
|
||||
"./**/*.js",
|
||||
"../authentication/**/*.js"
|
||||
]
|
||||
}
|
@ -59,7 +59,7 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton))
|
||||
|
||||
renderModal() {
|
||||
let product = globalAK().brand.brandingTitle || DefaultBrand.brandingTitle;
|
||||
if (this.licenseSummary.status !== LicenseSummaryStatusEnum.Unlicensed) {
|
||||
if (this.licenseSummary.status != LicenseSummaryStatusEnum.Unlicensed) {
|
||||
product += ` ${msg("Enterprise")}`;
|
||||
}
|
||||
return html`<div
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { parseAPIResponseError, pluckErrorDetail } from "@goauthentik/common/errors/network";
|
||||
import { MessageLevel } from "@goauthentik/common/messages";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/PageHeader";
|
||||
@ -54,10 +55,12 @@ export class DebugPage extends AKElement {
|
||||
message: "Success",
|
||||
});
|
||||
})
|
||||
.catch((exc) => {
|
||||
.catch(async (error) => {
|
||||
const parsedError = await parseAPIResponseError(error);
|
||||
|
||||
showMessage({
|
||||
level: MessageLevel.error,
|
||||
message: exc,
|
||||
message: pluckErrorDetail(parsedError),
|
||||
});
|
||||
});
|
||||
}}
|
||||
|
@ -1,13 +1,16 @@
|
||||
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||
import { PFSize } from "@goauthentik/common/enums.js";
|
||||
import {
|
||||
APIError,
|
||||
parseAPIResponseError,
|
||||
pluckErrorDetail,
|
||||
} from "@goauthentik/common/errors/network";
|
||||
import { AggregateCard } from "@goauthentik/elements/cards/AggregateCard";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { PropertyValues, TemplateResult, html, nothing } from "lit";
|
||||
import { state } from "lit/decorators.js";
|
||||
|
||||
import { ResponseError } from "@goauthentik/api";
|
||||
|
||||
export interface AdminStatus {
|
||||
icon: string;
|
||||
message?: TemplateResult;
|
||||
@ -29,7 +32,7 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
|
||||
|
||||
// Current error state if any request fails
|
||||
@state()
|
||||
protected error?: string;
|
||||
protected error?: APIError;
|
||||
|
||||
// Abstract methods to be implemented by subclasses
|
||||
abstract getPrimaryValue(): Promise<T>;
|
||||
@ -59,9 +62,9 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
|
||||
this.value = value; // Triggers shouldUpdate
|
||||
this.error = undefined;
|
||||
})
|
||||
.catch((err: ResponseError) => {
|
||||
.catch(async (error: unknown) => {
|
||||
this.status = undefined;
|
||||
this.error = err?.response?.statusText ?? msg("Unknown error");
|
||||
this.error = await parseAPIResponseError(error);
|
||||
});
|
||||
}
|
||||
|
||||
@ -79,9 +82,9 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
|
||||
this.status = status;
|
||||
this.error = undefined;
|
||||
})
|
||||
.catch((err: ResponseError) => {
|
||||
.catch(async (error: unknown) => {
|
||||
this.status = undefined;
|
||||
this.error = err?.response?.statusText ?? msg("Unknown error");
|
||||
this.error = await parseAPIResponseError(error);
|
||||
});
|
||||
|
||||
// Prevent immediate re-render if only value changed
|
||||
@ -120,8 +123,8 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
|
||||
*/
|
||||
private renderError(error: string): TemplateResult {
|
||||
return html`
|
||||
<p><i class="fa fa-times"></i> ${error}</p>
|
||||
<p class="subtext">${msg("Failed to fetch")}</p>
|
||||
<p><i class="fa fa-times"></i> ${msg("Failed to fetch")}</p>
|
||||
<p class="subtext">${error}</p>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -146,7 +149,7 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
|
||||
this.status
|
||||
? this.renderStatus(this.status) // Status available
|
||||
: this.error
|
||||
? this.renderError(this.error) // Error state
|
||||
? this.renderError(pluckErrorDetail(this.error)) // Error state
|
||||
: this.renderLoading() // Loading state
|
||||
}
|
||||
</p>
|
||||
|
@ -10,6 +10,7 @@ import "@goauthentik/elements/buttons/ModalButton";
|
||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||
import { Table, TableColumn } from "@goauthentik/elements/table/Table";
|
||||
import { SlottedTemplateResult } from "@goauthentik/elements/types";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||
@ -68,7 +69,7 @@ export class RecentEventsCard extends Table<Event> {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
row(item: EventWithContext): TemplateResult[] {
|
||||
row(item: EventWithContext): SlottedTemplateResult[] {
|
||||
return [
|
||||
html`<div><a href="${`#/events/log/${item.pk}`}">${actionToLabel(item.action)}</a></div>
|
||||
<small>${item.app}</small>`,
|
||||
@ -81,7 +82,11 @@ export class RecentEventsCard extends Table<Event> {
|
||||
];
|
||||
}
|
||||
|
||||
renderEmpty(): TemplateResult {
|
||||
renderEmpty(inner?: SlottedTemplateResult): TemplateResult {
|
||||
if (this.error) {
|
||||
return super.renderEmpty(inner);
|
||||
}
|
||||
|
||||
return super.renderEmpty(
|
||||
html`<ak-empty-state header=${msg("No Events found.")}>
|
||||
<div slot="body">${msg("No matching events could be found.")}</div>
|
||||
|
@ -46,7 +46,7 @@ export class SystemStatusCard extends AdminStatusCard<SystemInfo> {
|
||||
return;
|
||||
}
|
||||
const outpost = outposts.results[0];
|
||||
outpost.config.authentik_host = window.location.origin;
|
||||
outpost.config["authentik_host"] = window.location.origin;
|
||||
await new OutpostsApi(DEFAULT_CONFIG).outpostsInstancesUpdate({
|
||||
uuid: outpost.pk,
|
||||
outpostRequest: outpost,
|
||||
|
@ -28,18 +28,16 @@ export class WorkersStatusCard extends AdminStatusCard<Worker[]> {
|
||||
icon: "fa fa-times-circle pf-m-danger",
|
||||
message: html`${msg("No workers connected. Background tasks will not run.")}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (value.filter((w) => !w.versionMatching).length > 0) {
|
||||
} else if (value.filter((w) => !w.versionMatching).length > 0) {
|
||||
return Promise.resolve<AdminStatus>({
|
||||
icon: "fa fa-times-circle pf-m-danger",
|
||||
message: html`${msg("Worker with incorrect version connected.")}`,
|
||||
});
|
||||
} else {
|
||||
return Promise.resolve<AdminStatus>({
|
||||
icon: "fa fa-check-circle pf-m-success",
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve<AdminStatus>({
|
||||
icon: "fa fa-check-circle pf-m-success",
|
||||
});
|
||||
}
|
||||
|
||||
renderValue() {
|
||||
|
@ -127,7 +127,7 @@ export class SyncStatusChart extends AKChart<SummarizedSyncStatus[]> {
|
||||
msg("LDAP Source"),
|
||||
),
|
||||
];
|
||||
this.centerText = statuses.reduce((total, el) => total + el.total, 0).toString();
|
||||
this.centerText = statuses.reduce((total, el) => (total += el.total), 0).toString();
|
||||
return statuses;
|
||||
}
|
||||
|
||||
|
@ -6,26 +6,26 @@ import { html } from "lit";
|
||||
import "../AdminSettingsFooterLinks.js";
|
||||
|
||||
describe("ak-admin-settings-footer-link", () => {
|
||||
afterEach(() => {
|
||||
return browser.execute(() => {
|
||||
document.body.querySelector("ak-admin-settings-footer-link")?.remove();
|
||||
|
||||
if ("_$litPart$" in document.body) {
|
||||
delete document.body._$litPart$;
|
||||
afterEach(async () => {
|
||||
await browser.execute(async () => {
|
||||
await document.body.querySelector("ak-admin-settings-footer-link")?.remove();
|
||||
if (document.body["_$litPart$"]) {
|
||||
// @ts-expect-error expression of type '"_$litPart$"' is added by Lit
|
||||
await delete document.body["_$litPart$"];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should render an empty control", async () => {
|
||||
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
|
||||
const link = $("ak-admin-settings-footer-link");
|
||||
const link = await $("ak-admin-settings-footer-link");
|
||||
await expect(await link.getProperty("isValid")).toStrictEqual(false);
|
||||
await expect(await link.getProperty("toJson")).toEqual({ name: "", href: "" });
|
||||
});
|
||||
|
||||
it("should not be valid if just a name is filled in", async () => {
|
||||
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
|
||||
const link = $("ak-admin-settings-footer-link");
|
||||
const link = await $("ak-admin-settings-footer-link");
|
||||
await link.$('input[name="name"]').setValue("foo");
|
||||
await expect(await link.getProperty("isValid")).toStrictEqual(false);
|
||||
await expect(await link.getProperty("toJson")).toEqual({ name: "foo", href: "" });
|
||||
@ -33,7 +33,7 @@ describe("ak-admin-settings-footer-link", () => {
|
||||
|
||||
it("should be valid if just a URL is filled in", async () => {
|
||||
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
|
||||
const link = $("ak-admin-settings-footer-link");
|
||||
const link = await $("ak-admin-settings-footer-link");
|
||||
await link.$('input[name="href"]').setValue("https://foo.com");
|
||||
await expect(await link.getProperty("isValid")).toStrictEqual(true);
|
||||
await expect(await link.getProperty("toJson")).toEqual({
|
||||
@ -44,7 +44,7 @@ describe("ak-admin-settings-footer-link", () => {
|
||||
|
||||
it("should be valid if both are filled in", async () => {
|
||||
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
|
||||
const link = $("ak-admin-settings-footer-link");
|
||||
const link = await $("ak-admin-settings-footer-link");
|
||||
await link.$('input[name="name"]').setValue("foo");
|
||||
await link.$('input[name="href"]').setValue("https://foo.com");
|
||||
await expect(await link.getProperty("isValid")).toStrictEqual(true);
|
||||
@ -56,7 +56,7 @@ describe("ak-admin-settings-footer-link", () => {
|
||||
|
||||
it("should not be valid if the URL is not valid", async () => {
|
||||
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
|
||||
const link = $("ak-admin-settings-footer-link");
|
||||
const link = await $("ak-admin-settings-footer-link");
|
||||
await link.$('input[name="name"]').setValue("foo");
|
||||
await link.$('input[name="href"]').setValue("never://foo.com");
|
||||
await expect(await link.getProperty("toJson")).toEqual({
|
||||
|
@ -79,7 +79,7 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
|
||||
});
|
||||
}
|
||||
if (this.can(CapabilitiesEnum.CanSaveMedia)) {
|
||||
const icon = this.getFormFiles().metaIcon;
|
||||
const icon = this.getFormFiles()["metaIcon"];
|
||||
if (icon || this.clearIcon) {
|
||||
await new CoreApi(DEFAULT_CONFIG).coreApplicationsSetIconCreate({
|
||||
slug: app.slug,
|
||||
@ -117,7 +117,7 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
|
||||
if (!(ev instanceof InputEvent) || !ev.target) {
|
||||
return;
|
||||
}
|
||||
this.clearIcon = Boolean((ev.target as HTMLInputElement).checked);
|
||||
this.clearIcon = !!(ev.target as HTMLInputElement).checked;
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
|
@ -6,7 +6,7 @@ import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
||||
import "@goauthentik/elements/forms/Radio";
|
||||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
import * as YAML from "yaml";
|
||||
import YAML from "yaml";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult } from "lit";
|
||||
@ -31,8 +31,9 @@ export class ApplicationEntitlementForm extends ModelForm<ApplicationEntitlement
|
||||
getSuccessMessage(): string {
|
||||
if (this.instance?.pbmUuid) {
|
||||
return msg("Successfully updated entitlement.");
|
||||
} else {
|
||||
return msg("Successfully created entitlement.");
|
||||
}
|
||||
return msg("Successfully created entitlement.");
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
@ -48,10 +49,11 @@ export class ApplicationEntitlementForm extends ModelForm<ApplicationEntitlement
|
||||
pbmUuid: this.instance.pbmUuid || "",
|
||||
applicationEntitlementRequest: data,
|
||||
});
|
||||
} else {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreApplicationEntitlementsCreate({
|
||||
applicationEntitlementRequest: data,
|
||||
});
|
||||
}
|
||||
return new CoreApi(DEFAULT_CONFIG).coreApplicationEntitlementsCreate({
|
||||
applicationEntitlementRequest: data,
|
||||
});
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { styles } from "@goauthentik/admin/applications/wizard/ApplicationWizardFormStepStyles.css.js";
|
||||
import { WizardStep } from "@goauthentik/components/ak-wizard/WizardStep.js";
|
||||
import {
|
||||
NavigationUpdate,
|
||||
NavigationEventInit,
|
||||
WizardNavigationEvent,
|
||||
WizardUpdateEvent,
|
||||
} from "@goauthentik/components/ak-wizard/events";
|
||||
@ -14,9 +14,9 @@ import { property, query } from "lit/decorators.js";
|
||||
import { ValidationError } from "@goauthentik/api";
|
||||
|
||||
import {
|
||||
ApplicationTransactionValidationError,
|
||||
type ApplicationWizardState,
|
||||
type ApplicationWizardStateUpdate,
|
||||
ExtendedValidationError,
|
||||
} from "./types";
|
||||
|
||||
export class ApplicationWizardStep extends WizardStep {
|
||||
@ -48,7 +48,7 @@ export class ApplicationWizardStep extends WizardStep {
|
||||
}
|
||||
|
||||
protected removeErrors(
|
||||
keyToDelete: keyof ExtendedValidationError,
|
||||
keyToDelete: keyof ApplicationTransactionValidationError,
|
||||
): ValidationError | undefined {
|
||||
if (!this.wizard.errors) {
|
||||
return undefined;
|
||||
@ -71,7 +71,7 @@ export class ApplicationWizardStep extends WizardStep {
|
||||
public handleUpdate(
|
||||
update?: ApplicationWizardStateUpdate,
|
||||
destination?: string,
|
||||
enable?: NavigationUpdate,
|
||||
enable?: NavigationEventInit,
|
||||
) {
|
||||
// Inform ApplicationWizard of content state
|
||||
if (update) {
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
ProviderModelEnum,
|
||||
ProxyMode,
|
||||
ProxyProvider,
|
||||
RACProvider,
|
||||
RadiusProvider,
|
||||
RedirectURI,
|
||||
SAMLProvider,
|
||||
@ -50,8 +51,9 @@ function renderRadiusOverview(rawProvider: OneOfProvider) {
|
||||
]);
|
||||
}
|
||||
|
||||
function renderRACOverview(_rawProvider: OneOfProvider) {
|
||||
// const _provider = rawProvider as RACProvider;
|
||||
function renderRACOverview(rawProvider: OneOfProvider) {
|
||||
// @ts-expect-error TS6133
|
||||
const _provider = rawProvider as RACProvider;
|
||||
}
|
||||
|
||||
function formatRedirectUris(uris: RedirectURI[] = []) {
|
||||
|
@ -1,13 +1,14 @@
|
||||
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, isURLInput } from "@goauthentik/common/utils.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";
|
||||
|
||||
@ -20,25 +21,13 @@ import { type ApplicationRequest } from "@goauthentik/api";
|
||||
|
||||
import { ApplicationWizardStateUpdate, ValidationRecord } from "../types";
|
||||
|
||||
/**
|
||||
* Plucks the specified keys from an object, trimming their values if they are strings.
|
||||
*
|
||||
* @template T - The type of the input object.
|
||||
* @template K - The keys to be plucked from the input object.
|
||||
*
|
||||
* @param {T} input - The input object.
|
||||
* @param {Array<K>} keys - The keys to be plucked from the input object.
|
||||
*/
|
||||
function trimMany<T extends object, K extends keyof T>(input: T, keys: Array<K>): Pick<T, K> {
|
||||
const result: Partial<T> = {};
|
||||
const autoTrim = (v: unknown) => (typeof v === "string" ? v.trim() : v);
|
||||
|
||||
for (const key of keys) {
|
||||
const value = input[key];
|
||||
result[key] = (typeof value === "string" ? value.trim() : value) as T[K];
|
||||
}
|
||||
const trimMany = (o: KeyUnknown, vs: string[]) =>
|
||||
Object.fromEntries(vs.map((v) => [v, autoTrim(o[v])]));
|
||||
|
||||
return result as Pick<T, K>;
|
||||
}
|
||||
// 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 {
|
||||
@ -48,7 +37,7 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
|
||||
errors = new Map<string, string>();
|
||||
|
||||
@query("form#applicationform")
|
||||
declare form: HTMLFormElement;
|
||||
form!: HTMLFormElement;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@ -65,34 +54,27 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
|
||||
}
|
||||
|
||||
get buttons(): WizardButton[] {
|
||||
return [
|
||||
// ---
|
||||
{ kind: "next", destination: "provider-choice" },
|
||||
{ kind: "cancel" },
|
||||
];
|
||||
return [{ kind: "next", destination: "provider-choice" }, { kind: "cancel" }];
|
||||
}
|
||||
|
||||
get valid() {
|
||||
this.errors = new Map();
|
||||
const values = trimMany(this.formValues ?? {}, ["metaLaunchUrl", "name", "slug"]);
|
||||
|
||||
const trimmed = trimMany((this.formValues || {}) as Partial<ApplicationRequest>, [
|
||||
"name",
|
||||
"slug",
|
||||
"metaLaunchUrl",
|
||||
]);
|
||||
|
||||
if (!trimmed.name) {
|
||||
if (values["name"] === "") {
|
||||
this.errors.set("name", msg("An application name is required"));
|
||||
}
|
||||
|
||||
if (!isURLInput(trimmed.metaLaunchUrl)) {
|
||||
if (
|
||||
!(
|
||||
isStr(values["metaLaunchUrl"]) &&
|
||||
(values["metaLaunchUrl"] === "" || URL.canParse(values["metaLaunchUrl"]))
|
||||
)
|
||||
) {
|
||||
this.errors.set("metaLaunchUrl", msg("Not a valid URL"));
|
||||
}
|
||||
|
||||
if (!isSlug(trimmed.slug)) {
|
||||
if (!(isStr(values["slug"]) && values["slug"] !== "" && isSlug(values["slug"]))) {
|
||||
this.errors.set("slug", msg("Not a valid slug"));
|
||||
}
|
||||
|
||||
return this.errors.size === 0;
|
||||
}
|
||||
|
||||
@ -100,39 +82,27 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
|
||||
if (button.kind === "next") {
|
||||
if (!this.valid) {
|
||||
this.handleEnabling({
|
||||
disabled: [
|
||||
// ---
|
||||
"provider-choice",
|
||||
"provider",
|
||||
"bindings",
|
||||
"submit",
|
||||
],
|
||||
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);
|
||||
}
|
||||
|
||||
@ -211,7 +181,6 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
|
||||
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 ?? {},
|
||||
|
@ -45,7 +45,7 @@ export class ApplicationWizardEditBindingStep extends ApplicationWizardStep {
|
||||
hide = true;
|
||||
|
||||
@query("form#bindingform")
|
||||
declare form: HTMLFormElement;
|
||||
form!: HTMLFormElement;
|
||||
|
||||
@query(".policy-search-select")
|
||||
searchSelect!: SearchSelectBase<Policy> | SearchSelectBase<Group> | SearchSelectBase<User>;
|
||||
|
@ -1,9 +1,10 @@
|
||||
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 { parseAPIResponseError } from "@goauthentik/common/errors/network";
|
||||
import { WizardNavigationEvent } from "@goauthentik/components/ak-wizard/events.js";
|
||||
import { type WizardButton } from "@goauthentik/components/ak-wizard/types";
|
||||
import { showAPIErrorMessage } from "@goauthentik/elements/messages/MessageContainer";
|
||||
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||
import { P, match } from "ts-pattern";
|
||||
|
||||
@ -30,10 +31,11 @@ import {
|
||||
type TransactionApplicationRequest,
|
||||
type TransactionApplicationResponse,
|
||||
type TransactionPolicyBindingRequest,
|
||||
instanceOfValidationError,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import { ApplicationWizardStep } from "../ApplicationWizardStep.js";
|
||||
import { ExtendedValidationError, OneOfProvider } from "../types.js";
|
||||
import { OneOfProvider, isApplicationTransactionValidationError } from "../types.js";
|
||||
import { providerRenderers } from "./SubmitStepOverviewRenderers.js";
|
||||
|
||||
const _submitStates = ["reviewing", "running", "submitted"] as const;
|
||||
@ -131,39 +133,46 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
|
||||
|
||||
this.state = "running";
|
||||
|
||||
return (
|
||||
new CoreApi(DEFAULT_CONFIG)
|
||||
.coreTransactionalApplicationsUpdate({
|
||||
transactionApplicationRequest: request,
|
||||
})
|
||||
.then((_response: TransactionApplicationResponse) => {
|
||||
this.dispatchCustomEvent(EVENT_REFRESH);
|
||||
this.state = "submitted";
|
||||
})
|
||||
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;
|
||||
.catch(async (error) => {
|
||||
const parsedError = await parseAPIResponseError(error);
|
||||
|
||||
// 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;
|
||||
if (!instanceOfValidationError(parsedError)) {
|
||||
showAPIErrorMessage(parsedError);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isApplicationTransactionValidationError(parsedError)) {
|
||||
// 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(parsedError.app?.provider)) {
|
||||
const providerError = parsedError.app.provider;
|
||||
|
||||
parsedError.provider = {
|
||||
...parsedError.provider,
|
||||
name: providerError,
|
||||
};
|
||||
|
||||
delete parsedError.app.provider;
|
||||
|
||||
if (Object.keys(parsedError.app).length === 0) {
|
||||
delete parsedError.app;
|
||||
}
|
||||
}
|
||||
this.handleUpdate({ errors });
|
||||
this.state = "reviewing";
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
this.handleUpdate({ errors: parsedError });
|
||||
this.state = "reviewing";
|
||||
});
|
||||
}
|
||||
|
||||
override handleButton(button: WizardButton) {
|
||||
@ -225,22 +234,20 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
|
||||
}
|
||||
|
||||
renderError() {
|
||||
if (Object.keys(this.wizard.errors).length === 0) {
|
||||
return nothing;
|
||||
}
|
||||
const { errors } = this.wizard;
|
||||
|
||||
if (Object.keys(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)
|
||||
${match(errors)
|
||||
.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
|
||||
>
|
||||
<a @click=${WizardNavigationEvent.toListener(this, "application")}>
|
||||
${msg("Review the application.")}
|
||||
</a>
|
||||
</p>`,
|
||||
)
|
||||
.with(
|
||||
@ -248,13 +255,20 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
|
||||
() =>
|
||||
html`<p>${msg("There was an error in the provider.")}</p>
|
||||
<p>
|
||||
<a @click=${navTo("provider")}>${msg("Review the provider.")}</a>
|
||||
<a @click=${WizardNavigationEvent.toListener(this, "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>`,
|
||||
html`<p>
|
||||
${msg(
|
||||
"There was an error. Please go back and review the application.",
|
||||
)}:
|
||||
${errors.detail}
|
||||
</p>`,
|
||||
)
|
||||
.with(
|
||||
{
|
||||
@ -264,7 +278,7 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
|
||||
html`<p>${msg("There was an error:")}:</p>
|
||||
<ul>
|
||||
${(errors.nonFieldErrors ?? []).map(
|
||||
(e: string) => html`<li>${e}</li>`,
|
||||
(reason) => html`<li>${reason}</li>`,
|
||||
)}
|
||||
</ul>
|
||||
<p>${msg("Please go back and review the application.")}</p>`,
|
||||
|
@ -9,7 +9,7 @@ import { customElement, state } from "lit/decorators.js";
|
||||
import { OAuth2ProviderRequest, SourcesApi } from "@goauthentik/api";
|
||||
import { type OAuth2Provider, type PaginatedOAuthSourceList } from "@goauthentik/api";
|
||||
|
||||
import { ExtendedValidationError } from "../../types.js";
|
||||
import { ApplicationTransactionValidationError } from "../../types.js";
|
||||
import { ApplicationWizardProviderForm } from "./ApplicationWizardProviderForm.js";
|
||||
|
||||
@customElement("ak-application-wizard-provider-for-oauth")
|
||||
@ -34,7 +34,7 @@ export class ApplicationWizardOauth2ProviderForm extends ApplicationWizardProvid
|
||||
});
|
||||
}
|
||||
|
||||
renderForm(provider: OAuth2Provider, errors: ExtendedValidationError) {
|
||||
renderForm(provider: OAuth2Provider, errors: ApplicationTransactionValidationError) {
|
||||
const showClientSecretCallback = (show: boolean) => {
|
||||
this.showClientSecret = show;
|
||||
};
|
||||
|
@ -22,7 +22,7 @@ export class ApplicationWizardProviderSamlForm extends ApplicationWizardProvider
|
||||
const setHasSigningKp = (ev: InputEvent) => {
|
||||
const target = ev.target as AkCryptoCertificateSearch;
|
||||
if (!target) return;
|
||||
this.hasSigningKp = Boolean(target.selectedKeypair);
|
||||
this.hasSigningKp = !!target.selectedKeypair;
|
||||
};
|
||||
|
||||
return html` <ak-wizard-title>${this.label}</ak-wizard-title>
|
||||
|
@ -25,16 +25,30 @@ export type OneOfProvider =
|
||||
|
||||
export type ValidationRecord = { [key: string]: string[] };
|
||||
|
||||
// TODO: Elf, extend this type and apply it to every object in the wizard. Then run
|
||||
// the type-checker again.
|
||||
|
||||
export type ExtendedValidationError = ValidationError & {
|
||||
/**
|
||||
* An error that occurs during the creation or modification of an application.
|
||||
*
|
||||
* @todo (Elf) Extend this type to include all possible errors that can occur during the creation or modification of an application.
|
||||
*/
|
||||
export interface ApplicationTransactionValidationError extends ValidationError {
|
||||
app?: ValidationRecord;
|
||||
provider?: ValidationRecord;
|
||||
bindings?: ValidationRecord;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
detail?: any;
|
||||
};
|
||||
detail?: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-guard to determine if an API response is shaped like an {@linkcode ApplicationTransactionValidationError}.
|
||||
*/
|
||||
export function isApplicationTransactionValidationError(
|
||||
error: ValidationError,
|
||||
): error is ApplicationTransactionValidationError {
|
||||
if ("app" in error) return true;
|
||||
if ("provider" in error) return true;
|
||||
if ("bindings" in error) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// We use the PolicyBinding instead of the PolicyBindingRequest here, because that gives us a slot
|
||||
// in which to preserve the retrieved policy, group, or user object from the SearchSelect used to
|
||||
@ -49,7 +63,7 @@ export interface ApplicationWizardState {
|
||||
proxyMode: ProxyMode;
|
||||
bindings: PolicyBinding[];
|
||||
currentBinding: number;
|
||||
errors: ExtendedValidationError;
|
||||
errors: ValidationError | ApplicationTransactionValidationError;
|
||||
}
|
||||
|
||||
export interface ApplicationWizardStateUpdate {
|
||||
|
@ -8,7 +8,7 @@ import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
||||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
import * as YAML from "yaml";
|
||||
import YAML from "yaml";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, html } from "lit";
|
||||
@ -59,10 +59,11 @@ export class BlueprintForm extends ModelForm<BlueprintInstance, string> {
|
||||
instanceUuid: this.instance.pk,
|
||||
blueprintInstanceRequest: data,
|
||||
});
|
||||
} else {
|
||||
return new ManagedApi(DEFAULT_CONFIG).managedBlueprintsCreate({
|
||||
blueprintInstanceRequest: data,
|
||||
});
|
||||
}
|
||||
return new ManagedApi(DEFAULT_CONFIG).managedBlueprintsCreate({
|
||||
blueprintInstanceRequest: data,
|
||||
});
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
|
@ -9,7 +9,7 @@ import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
||||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand";
|
||||
import * as YAML from "yaml";
|
||||
import YAML from "yaml";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
@ -43,10 +43,11 @@ export class BrandForm extends ModelForm<Brand, string> {
|
||||
brandUuid: this.instance.brandUuid,
|
||||
brandRequest: data,
|
||||
});
|
||||
} else {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreBrandsCreate({
|
||||
brandRequest: data,
|
||||
});
|
||||
}
|
||||
return new CoreApi(DEFAULT_CONFIG).coreBrandsCreate({
|
||||
brandRequest: data,
|
||||
});
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
|
@ -107,7 +107,7 @@ export class AkCryptoCertificateSearch extends CustomListenerElement(AKElement)
|
||||
selected(item: CertificateKeyPair, items: CertificateKeyPair[]) {
|
||||
return (
|
||||
(this.singleton && !this.certificate && items.length === 1) ||
|
||||
(Boolean(this.certificate) && this.certificate === item.pk)
|
||||
(!!this.certificate && this.certificate === item.pk)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,7 @@ const metadata: Meta<AkCryptoCertificateSearch> = {
|
||||
argTypes: {
|
||||
// Typescript is unaware that arguments for components are treated as properties, and
|
||||
// properties are typically renamed to lower case, even if the variable is not.
|
||||
// @ts-expect-error TODO: Explain.
|
||||
// @ts-expect-error
|
||||
nokey: {
|
||||
control: "boolean",
|
||||
description:
|
||||
@ -75,7 +75,7 @@ export const CryptoCertificateSearch = () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const showMessage = (ev: CustomEvent<any>) => {
|
||||
const detail = ev.detail;
|
||||
delete detail.target;
|
||||
delete detail["target"];
|
||||
document.getElementById("message-pad")!.innerText = `Event: ${JSON.stringify(
|
||||
detail,
|
||||
null,
|
||||
|
@ -30,10 +30,11 @@ export class CertificateKeyPairForm extends ModelForm<CertificateKeyPair, string
|
||||
kpUuid: this.instance.pk || "",
|
||||
patchedCertificateKeyPairRequest: data,
|
||||
});
|
||||
} else {
|
||||
return new CryptoApi(DEFAULT_CONFIG).cryptoCertificatekeypairsCreate({
|
||||
certificateKeyPairRequest: data as unknown as CertificateKeyPairRequest,
|
||||
});
|
||||
}
|
||||
return new CryptoApi(DEFAULT_CONFIG).cryptoCertificatekeypairsCreate({
|
||||
certificateKeyPairRequest: data as unknown as CertificateKeyPairRequest,
|
||||
});
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
|
@ -8,6 +8,7 @@ import "@goauthentik/components/ak-event-info";
|
||||
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||
import { TableColumn } from "@goauthentik/elements/table/Table";
|
||||
import { TablePage } from "@goauthentik/elements/table/TablePage";
|
||||
import { SlottedTemplateResult } from "@goauthentik/elements/types";
|
||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
@ -72,7 +73,7 @@ export class EventListPage extends TablePage<Event> {
|
||||
`;
|
||||
}
|
||||
|
||||
row(item: EventWithContext): TemplateResult[] {
|
||||
row(item: EventWithContext): SlottedTemplateResult[] {
|
||||
return [
|
||||
html`<div>${actionToLabel(item.action)}</div>
|
||||
<small>${item.app}</small>`,
|
||||
|
@ -51,10 +51,11 @@ export class RuleForm extends ModelForm<NotificationRule, string> {
|
||||
pbmUuid: this.instance.pk || "",
|
||||
notificationRuleRequest: data,
|
||||
});
|
||||
} else {
|
||||
return new EventsApi(DEFAULT_CONFIG).eventsRulesCreate({
|
||||
notificationRuleRequest: data,
|
||||
});
|
||||
}
|
||||
return new EventsApi(DEFAULT_CONFIG).eventsRulesCreate({
|
||||
notificationRuleRequest: data,
|
||||
});
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
|
@ -47,10 +47,11 @@ export class TransportForm extends ModelForm<NotificationTransport, string> {
|
||||
uuid: this.instance.pk || "",
|
||||
notificationTransportRequest: data,
|
||||
});
|
||||
} else {
|
||||
return new EventsApi(DEFAULT_CONFIG).eventsTransportsCreate({
|
||||
notificationTransportRequest: data,
|
||||
});
|
||||
}
|
||||
return new EventsApi(DEFAULT_CONFIG).eventsTransportsCreate({
|
||||
notificationTransportRequest: data,
|
||||
});
|
||||
}
|
||||
|
||||
onModeChange(mode: string | undefined): void {
|
||||
|
@ -1,27 +1,31 @@
|
||||
import { EventWithContext } from "@goauthentik/common/events";
|
||||
import { truncate } from "@goauthentik/common/utils";
|
||||
import { KeyUnknown } from "@goauthentik/elements/forms/Form";
|
||||
import { SlottedTemplateResult } from "@goauthentik/elements/types";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { html, nothing } from "lit";
|
||||
|
||||
export function EventGeo(event: EventWithContext): TemplateResult {
|
||||
let geo: KeyUnknown | undefined = undefined;
|
||||
if (Object.hasOwn(event.context, "geo")) {
|
||||
geo = event.context.geo as KeyUnknown;
|
||||
const parts = [geo.city, geo.country, geo.continent].filter(
|
||||
(v) => v !== "" && v !== undefined,
|
||||
);
|
||||
return html`${parts.join(", ")}`;
|
||||
}
|
||||
return html``;
|
||||
/**
|
||||
* Given event with a geographical context, format it into a string for display.
|
||||
*/
|
||||
export function EventGeo(event: EventWithContext): SlottedTemplateResult {
|
||||
if (!event.context.geo) return nothing;
|
||||
|
||||
const { city, country, continent } = event.context.geo;
|
||||
|
||||
const parts = [city, country, continent].filter(Boolean);
|
||||
|
||||
return html`${parts.join(", ")}`;
|
||||
}
|
||||
|
||||
export function EventUser(event: EventWithContext, truncateUsername?: number): TemplateResult {
|
||||
if (!event.user.username) {
|
||||
return html`-`;
|
||||
}
|
||||
let body = html``;
|
||||
export function EventUser(
|
||||
event: EventWithContext,
|
||||
truncateUsername?: number,
|
||||
): SlottedTemplateResult {
|
||||
if (!event.user.username) return html`-`;
|
||||
|
||||
let body: SlottedTemplateResult = nothing;
|
||||
|
||||
if (event.user.is_anonymous) {
|
||||
body = html`<div>${msg("Anonymous user")}</div>`;
|
||||
} else {
|
||||
@ -33,12 +37,14 @@ export function EventUser(event: EventWithContext, truncateUsername?: number): T
|
||||
>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (event.user.on_behalf_of) {
|
||||
body = html`${body}<small>
|
||||
return html`${body}<small>
|
||||
<a href="#/identity/users/${event.user.on_behalf_of.pk}"
|
||||
>${msg(str`On behalf of ${event.user.on_behalf_of.username}`)}</a
|
||||
>
|
||||
</small>`;
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ export class FlowForm extends WithCapabilitiesConfig(ModelForm<Flow, string>) {
|
||||
}
|
||||
|
||||
if (this.can(CapabilitiesEnum.CanSaveMedia)) {
|
||||
const icon = this.getFormFiles().background;
|
||||
const icon = this.getFormFiles()["background"];
|
||||
if (icon || this.clearBackground) {
|
||||
await new FlowsApi(DEFAULT_CONFIG).flowsInstancesSetBackgroundCreate({
|
||||
slug: flow.slug,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { SentryIgnoredError } from "@goauthentik/common/errors";
|
||||
import { SentryIgnoredError } from "@goauthentik/common/sentry";
|
||||
import "@goauthentik/components/ak-status-label";
|
||||
import "@goauthentik/elements/events/LogViewer";
|
||||
import { Form } from "@goauthentik/elements/forms/Form";
|
||||
@ -27,7 +27,7 @@ export class FlowImportForm extends Form<Flow> {
|
||||
}
|
||||
|
||||
async send(): Promise<FlowImportResult> {
|
||||
const file = this.getFormFiles().flow;
|
||||
const file = this.getFormFiles()["flow"];
|
||||
if (!file) {
|
||||
throw new SentryIgnoredError("No form data");
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import { DesignationToLabel } from "@goauthentik/admin/flows/utils";
|
||||
import "@goauthentik/admin/policies/BoundPoliciesList";
|
||||
import "@goauthentik/admin/rbac/ObjectPermissionsPage";
|
||||
import { AndNext, DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { isResponseErrorLike } from "@goauthentik/common/errors/network";
|
||||
import "@goauthentik/components/events/ObjectChangelog";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/PageHeader";
|
||||
@ -23,12 +24,7 @@ import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
||||
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import {
|
||||
Flow,
|
||||
FlowsApi,
|
||||
RbacPermissionsAssignedByUsersListModelEnum,
|
||||
ResponseError,
|
||||
} from "@goauthentik/api";
|
||||
import { Flow, FlowsApi, RbacPermissionsAssignedByUsersListModelEnum } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-flow-view")
|
||||
export class FlowViewPage extends AKElement {
|
||||
@ -195,13 +191,15 @@ export class FlowViewPage extends AKElement {
|
||||
)}`;
|
||||
window.open(finalURL, "_blank");
|
||||
})
|
||||
.catch((exc: ResponseError) => {
|
||||
// This request can return a HTTP 400 when a flow
|
||||
// is not applicable.
|
||||
window.open(
|
||||
exc.response.url,
|
||||
"_blank",
|
||||
);
|
||||
.catch(async (error: unknown) => {
|
||||
if (isResponseErrorLike(error)) {
|
||||
// This request can return a HTTP 400 when a flow
|
||||
// is not applicable.
|
||||
window.open(
|
||||
error.response.url,
|
||||
"_blank",
|
||||
);
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
@ -39,8 +39,9 @@ export class StageBindingForm extends ModelForm<FlowStageBinding, string> {
|
||||
getSuccessMessage(): string {
|
||||
if (this.instance?.pk) {
|
||||
return msg("Successfully updated binding.");
|
||||
} else {
|
||||
return msg("Successfully created binding.");
|
||||
}
|
||||
return msg("Successfully created binding.");
|
||||
}
|
||||
|
||||
send(data: FlowStageBinding): Promise<unknown> {
|
||||
@ -49,13 +50,14 @@ export class StageBindingForm extends ModelForm<FlowStageBinding, string> {
|
||||
fsbUuid: this.instance.pk,
|
||||
patchedFlowStageBindingRequest: data,
|
||||
});
|
||||
} else {
|
||||
if (this.targetPk) {
|
||||
data.target = this.targetPk;
|
||||
}
|
||||
return new FlowsApi(DEFAULT_CONFIG).flowsBindingsCreate({
|
||||
flowStageBindingRequest: data,
|
||||
});
|
||||
}
|
||||
if (this.targetPk) {
|
||||
data.target = this.targetPk;
|
||||
}
|
||||
return new FlowsApi(DEFAULT_CONFIG).flowsBindingsCreate({
|
||||
flowStageBindingRequest: data,
|
||||
});
|
||||
}
|
||||
|
||||
async getOrder(): Promise<number> {
|
||||
|
@ -10,7 +10,7 @@ import "@goauthentik/elements/chips/ChipGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
||||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
import * as YAML from "yaml";
|
||||
import YAML from "yaml";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||
@ -55,11 +55,12 @@ export class GroupForm extends ModelForm<Group, string> {
|
||||
groupUuid: this.instance.pk,
|
||||
patchedGroupRequest: data,
|
||||
});
|
||||
} else {
|
||||
data.users = [];
|
||||
return new CoreApi(DEFAULT_CONFIG).coreGroupsCreate({
|
||||
groupRequest: data,
|
||||
});
|
||||
}
|
||||
data.users = [];
|
||||
return new CoreApi(DEFAULT_CONFIG).coreGroupsCreate({
|
||||
groupRequest: data,
|
||||
});
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
|
@ -125,8 +125,7 @@ export class RelatedGroupList extends Table<Group> {
|
||||
buttonLabel=${msg("Remove")}
|
||||
.objects=${this.selectedElements}
|
||||
.delete=${(item: Group) => {
|
||||
if (!this.targetUser) return null;
|
||||
|
||||
if (!this.targetUser) return;
|
||||
return new CoreApi(DEFAULT_CONFIG).coreGroupsRemoveUserCreate({
|
||||
groupUuid: item.pk,
|
||||
userAccountRequest: {
|
||||
|
@ -6,6 +6,7 @@ import "@goauthentik/admin/users/UserPasswordForm";
|
||||
import "@goauthentik/admin/users/UserResetEmailForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { PFSize } from "@goauthentik/common/enums.js";
|
||||
import { parseAPIResponseError, pluckErrorDetail } from "@goauthentik/common/errors/network";
|
||||
import { MessageLevel } from "@goauthentik/common/messages";
|
||||
import { me } from "@goauthentik/common/users";
|
||||
import { getRelativeTime } from "@goauthentik/common/utils";
|
||||
@ -37,21 +38,10 @@ import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
|
||||
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
|
||||
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
|
||||
|
||||
import {
|
||||
CoreApi,
|
||||
CoreUsersListTypeEnum,
|
||||
Group,
|
||||
ResponseError,
|
||||
SessionUser,
|
||||
User,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
interface AddUsersToGroupFormData {
|
||||
users: number[];
|
||||
}
|
||||
import { CoreApi, CoreUsersListTypeEnum, Group, SessionUser, User } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-user-related-add")
|
||||
export class RelatedUserAdd extends Form<AddUsersToGroupFormData> {
|
||||
export class RelatedUserAdd extends Form<{ users: number[] }> {
|
||||
@property({ attribute: false })
|
||||
group?: Group;
|
||||
|
||||
@ -62,7 +52,7 @@ export class RelatedUserAdd extends Form<AddUsersToGroupFormData> {
|
||||
return msg("Successfully added user(s).");
|
||||
}
|
||||
|
||||
async send(data: AddUsersToGroupFormData): Promise<AddUsersToGroupFormData> {
|
||||
async send(data: { users: number[] }): Promise<{ users: number[] }> {
|
||||
await Promise.all(
|
||||
data.users.map((user) => {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreGroupsAddUserCreate({
|
||||
@ -73,7 +63,6 @@ export class RelatedUserAdd extends Form<AddUsersToGroupFormData> {
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@ -324,14 +313,16 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
|
||||
description: rec.link,
|
||||
});
|
||||
})
|
||||
.catch((ex: ResponseError) => {
|
||||
ex.response.json().then(() => {
|
||||
showMessage({
|
||||
level: MessageLevel.error,
|
||||
message: msg(
|
||||
"No recovery flow is configured.",
|
||||
),
|
||||
});
|
||||
.catch(async (error: unknown) => {
|
||||
const parsedError =
|
||||
await parseAPIResponseError(
|
||||
error,
|
||||
);
|
||||
|
||||
showMessage({
|
||||
level: MessageLevel.error,
|
||||
message:
|
||||
pluckErrorDetail(parsedError),
|
||||
});
|
||||
});
|
||||
}}
|
||||
|
@ -65,7 +65,7 @@ export class OutpostDeploymentModal extends ModalButton {
|
||||
</label>
|
||||
<input class="pf-c-form-control" readonly type="text" value="true" />
|
||||
</div>
|
||||
${this.outpost?.type === OutpostTypeEnum.Proxy
|
||||
${this.outpost?.type == OutpostTypeEnum.Proxy
|
||||
? html`
|
||||
<h3>
|
||||
${msg(
|
||||
|
@ -10,7 +10,7 @@ import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
||||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||
import * as YAML from "yaml";
|
||||
import YAML from "yaml";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
@ -129,10 +129,11 @@ export class OutpostForm extends ModelForm<Outpost, string> {
|
||||
uuid: this.instance.pk || "",
|
||||
outpostRequest: data,
|
||||
});
|
||||
} else {
|
||||
return new OutpostsApi(DEFAULT_CONFIG).outpostsInstancesCreate({
|
||||
outpostRequest: data,
|
||||
});
|
||||
}
|
||||
return new OutpostsApi(DEFAULT_CONFIG).outpostsInstancesCreate({
|
||||
outpostRequest: data,
|
||||
});
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
|
@ -32,10 +32,11 @@ export class ServiceConnectionDockerForm extends ModelForm<DockerServiceConnecti
|
||||
uuid: this.instance.pk || "",
|
||||
dockerServiceConnectionRequest: data,
|
||||
});
|
||||
} else {
|
||||
return new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsDockerCreate({
|
||||
dockerServiceConnectionRequest: data,
|
||||
});
|
||||
}
|
||||
return new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsDockerCreate({
|
||||
dockerServiceConnectionRequest: data,
|
||||
});
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
|
@ -4,7 +4,7 @@ import "@goauthentik/elements/CodeMirror";
|
||||
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
||||
import * as YAML from "yaml";
|
||||
import YAML from "yaml";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
@ -36,10 +36,11 @@ export class ServiceConnectionKubernetesForm extends ModelForm<
|
||||
uuid: this.instance.pk || "",
|
||||
kubernetesServiceConnectionRequest: data,
|
||||
});
|
||||
} else {
|
||||
return new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsKubernetesCreate({
|
||||
kubernetesServiceConnectionRequest: data,
|
||||
});
|
||||
}
|
||||
return new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsKubernetesCreate({
|
||||
kubernetesServiceConnectionRequest: data,
|
||||
});
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
|
@ -72,8 +72,9 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
|
||||
return msg(str`Group ${item.groupObj?.name}`);
|
||||
} else if (item.user) {
|
||||
return msg(str`User ${item.userObj?.name}`);
|
||||
} else {
|
||||
return msg("-");
|
||||
}
|
||||
return msg("-");
|
||||
}
|
||||
|
||||
getPolicyUserGroupRow(item: PolicyBinding): TemplateResult {
|
||||
@ -122,8 +123,9 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
|
||||
${msg("Edit User")}
|
||||
</button>
|
||||
</ak-forms-modal>`;
|
||||
} else {
|
||||
return html``;
|
||||
}
|
||||
return html``;
|
||||
}
|
||||
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
|
@ -72,8 +72,9 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
|
||||
getSuccessMessage(): string {
|
||||
if (this.instance?.pk) {
|
||||
return msg("Successfully updated binding.");
|
||||
} else {
|
||||
return msg("Successfully created binding.");
|
||||
}
|
||||
return msg("Successfully created binding.");
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
@ -110,10 +111,11 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
|
||||
policyBindingUuid: this.instance.pk,
|
||||
policyBindingRequest: data,
|
||||
});
|
||||
} else {
|
||||
return new PoliciesApi(DEFAULT_CONFIG).policiesBindingsCreate({
|
||||
policyBindingRequest: data,
|
||||
});
|
||||
}
|
||||
return new PoliciesApi(DEFAULT_CONFIG).policiesBindingsCreate({
|
||||
policyBindingRequest: data,
|
||||
});
|
||||
}
|
||||
|
||||
async getOrder(): Promise<number> {
|
||||
|
@ -7,7 +7,7 @@ import "@goauthentik/elements/events/LogViewer";
|
||||
import { Form } from "@goauthentik/elements/forms/Form";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
import * as YAML from "yaml";
|
||||
import YAML from "yaml";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, html } from "lit";
|
||||
|
@ -25,11 +25,11 @@ export class DummyPolicyForm extends BasePolicyForm<DummyPolicy> {
|
||||
policyUuid: this.instance.pk || "",
|
||||
dummyPolicyRequest: data,
|
||||
});
|
||||
} else {
|
||||
return new PoliciesApi(DEFAULT_CONFIG).policiesDummyCreate({
|
||||
dummyPolicyRequest: data,
|
||||
});
|
||||
}
|
||||
|
||||
return new PoliciesApi(DEFAULT_CONFIG).policiesDummyCreate({
|
||||
dummyPolicyRequest: data,
|
||||
});
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
|
@ -37,10 +37,11 @@ export class EventMatcherPolicyForm extends BasePolicyForm<EventMatcherPolicy> {
|
||||
policyUuid: this.instance.pk || "",
|
||||
eventMatcherPolicyRequest: data,
|
||||
});
|
||||
} else {
|
||||
return new PoliciesApi(DEFAULT_CONFIG).policiesEventMatcherCreate({
|
||||
eventMatcherPolicyRequest: data,
|
||||
});
|
||||
}
|
||||
return new PoliciesApi(DEFAULT_CONFIG).policiesEventMatcherCreate({
|
||||
eventMatcherPolicyRequest: data,
|
||||
});
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user