Compare commits

...
This repository has been archived on 2024-05-31. You can view files and clone it, but cannot push or open issues or pull requests.

5 Commits

Author SHA1 Message Date
Jens Langhammer 8543e140ef stages/consent: fix error when requests with identical empty permissions
closes #3280

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-08-01 20:58:25 +02:00
Jens Langhammer e2cf578afd root: use updated schema
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-08-01 10:11:06 +02:00
Jens Langhammer 0ce9fd9b2e web: fix updates
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-08-01 09:59:57 +02:00
Jens Langhammer d17ad65435 web: bump api client to match
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-31 23:54:29 +02:00
Jens L 01529d3894 flows/stages/consent: fix for post requests (#3339)
add unique token to consent stage to ensure it is shown

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2022-07-31 23:53:06 +02:00
11 changed files with 950 additions and 61 deletions

View File

@ -1,5 +1,6 @@
"""authentik consent stage""" """authentik consent stage"""
from typing import Optional from typing import Optional
from uuid import uuid4
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.utils.timezone import now from django.utils.timezone import now
@ -20,7 +21,8 @@ from authentik.stages.consent.models import ConsentMode, ConsentStage, UserConse
PLAN_CONTEXT_CONSENT_TITLE = "consent_title" PLAN_CONTEXT_CONSENT_TITLE = "consent_title"
PLAN_CONTEXT_CONSENT_HEADER = "consent_header" PLAN_CONTEXT_CONSENT_HEADER = "consent_header"
PLAN_CONTEXT_CONSENT_PERMISSIONS = "consent_permissions" PLAN_CONTEXT_CONSENT_PERMISSIONS = "consent_permissions"
PLAN_CONTEXT_CONSNET_EXTRA_PERMISSIONS = "consent_additional_permissions" PLAN_CONTEXT_CONSENT_EXTRA_PERMISSIONS = "consent_additional_permissions"
SESSION_KEY_CONSENT_TOKEN = "authentik/stages/consent/token" # nosec
class ConsentChallenge(WithUserInfoChallenge): class ConsentChallenge(WithUserInfoChallenge):
@ -30,12 +32,14 @@ class ConsentChallenge(WithUserInfoChallenge):
permissions = PermissionSerializer(many=True) permissions = PermissionSerializer(many=True)
additional_permissions = PermissionSerializer(many=True) additional_permissions = PermissionSerializer(many=True)
component = CharField(default="ak-stage-consent") component = CharField(default="ak-stage-consent")
token = CharField(required=True)
class ConsentChallengeResponse(ChallengeResponse): class ConsentChallengeResponse(ChallengeResponse):
"""Consent challenge response, any valid response request is valid""" """Consent challenge response, any valid response request is valid"""
component = CharField(default="ak-stage-consent") component = CharField(default="ak-stage-consent")
token = CharField(required=True)
class ConsentStageView(ChallengeStageView): class ConsentStageView(ChallengeStageView):
@ -44,12 +48,15 @@ class ConsentStageView(ChallengeStageView):
response_class = ConsentChallengeResponse response_class = ConsentChallengeResponse
def get_challenge(self) -> Challenge: def get_challenge(self) -> Challenge:
token = str(uuid4())
self.request.session[SESSION_KEY_CONSENT_TOKEN] = token
data = { data = {
"type": ChallengeTypes.NATIVE.value, "type": ChallengeTypes.NATIVE.value,
"permissions": self.executor.plan.context.get(PLAN_CONTEXT_CONSENT_PERMISSIONS, []), "permissions": self.executor.plan.context.get(PLAN_CONTEXT_CONSENT_PERMISSIONS, []),
"additional_permissions": self.executor.plan.context.get( "additional_permissions": self.executor.plan.context.get(
PLAN_CONTEXT_CONSNET_EXTRA_PERMISSIONS, [] PLAN_CONTEXT_CONSENT_EXTRA_PERMISSIONS, []
), ),
"token": token,
} }
if PLAN_CONTEXT_CONSENT_TITLE in self.executor.plan.context: if PLAN_CONTEXT_CONSENT_TITLE in self.executor.plan.context:
data["title"] = self.executor.plan.context[PLAN_CONTEXT_CONSENT_TITLE] data["title"] = self.executor.plan.context[PLAN_CONTEXT_CONSENT_TITLE]
@ -85,14 +92,14 @@ class ConsentStageView(ChallengeStageView):
if consent: if consent:
perms = self.executor.plan.context.get(PLAN_CONTEXT_CONSENT_PERMISSIONS, []) perms = self.executor.plan.context.get(PLAN_CONTEXT_CONSENT_PERMISSIONS, [])
allowed_perms = set(consent.permissions.split(" ")) allowed_perms = set(consent.permissions.split(" ") if consent.permissions != "" else [])
requested_perms = set(x["id"] for x in perms) requested_perms = set(x["id"] for x in perms)
if allowed_perms != requested_perms: if allowed_perms != requested_perms:
self.executor.plan.context[PLAN_CONTEXT_CONSENT_PERMISSIONS] = [ self.executor.plan.context[PLAN_CONTEXT_CONSENT_PERMISSIONS] = [
x for x in perms if x["id"] in allowed_perms x for x in perms if x["id"] in allowed_perms
] ]
self.executor.plan.context[PLAN_CONTEXT_CONSNET_EXTRA_PERMISSIONS] = [ self.executor.plan.context[PLAN_CONTEXT_CONSENT_EXTRA_PERMISSIONS] = [
x for x in perms if x["id"] in requested_perms.difference(allowed_perms) x for x in perms if x["id"] in requested_perms.difference(allowed_perms)
] ]
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
@ -102,13 +109,15 @@ class ConsentStageView(ChallengeStageView):
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse: def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
if response.data["token"] != self.request.session[SESSION_KEY_CONSENT_TOKEN]:
return self.get(self.request)
current_stage: ConsentStage = self.executor.current_stage current_stage: ConsentStage = self.executor.current_stage
if PLAN_CONTEXT_APPLICATION not in self.executor.plan.context: if PLAN_CONTEXT_APPLICATION not in self.executor.plan.context:
return self.executor.stage_ok() return self.executor.stage_ok()
application = self.executor.plan.context[PLAN_CONTEXT_APPLICATION] application = self.executor.plan.context[PLAN_CONTEXT_APPLICATION]
permissions = self.executor.plan.context.get( permissions = self.executor.plan.context.get(
PLAN_CONTEXT_CONSENT_PERMISSIONS, [] PLAN_CONTEXT_CONSENT_PERMISSIONS, []
) + self.executor.plan.context.get(PLAN_CONTEXT_CONSNET_EXTRA_PERMISSIONS, []) ) + self.executor.plan.context.get(PLAN_CONTEXT_CONSENT_EXTRA_PERMISSIONS, [])
permissions_string = " ".join(x["id"] for x in permissions) permissions_string = " ".join(x["id"] for x in permissions)
# Make this StageView work when injected, in which case `current_stage` is an instance # Make this StageView work when injected, in which case `current_stage` is an instance
# of the base class, and we don't save any consent, as it is assumed to be a one-time # of the base class, and we don't save any consent, as it is assumed to be a one-time
@ -116,18 +125,14 @@ class ConsentStageView(ChallengeStageView):
if not isinstance(current_stage, ConsentStage): if not isinstance(current_stage, ConsentStage):
return self.executor.stage_ok() return self.executor.stage_ok()
# Since we only get here when no consent exists, we can create it without update # Since we only get here when no consent exists, we can create it without update
consent = UserConsent(
user=self.request.user,
application=application,
permissions=permissions_string,
)
if current_stage.mode == ConsentMode.PERMANENT: if current_stage.mode == ConsentMode.PERMANENT:
UserConsent.objects.create( consent.expiring = False
user=self.request.user,
application=application,
expiring=False,
permissions=permissions_string,
)
if current_stage.mode == ConsentMode.EXPIRING: if current_stage.mode == ConsentMode.EXPIRING:
UserConsent.objects.create( consent.expires = now() + timedelta_from_string(current_stage.consent_expire_in)
user=self.request.user, consent.save()
application=application,
expires=now() + timedelta_from_string(current_stage.consent_expire_in),
permissions=permissions_string,
)
return self.executor.stage_ok() return self.executor.stage_ok()

View File

@ -1,5 +1,6 @@
"""consent tests""" """consent tests"""
from time import sleep from time import sleep
from uuid import uuid4
from django.urls import reverse from django.urls import reverse
@ -14,7 +15,10 @@ from authentik.flows.tests import FlowTestCase
from authentik.flows.views.executor import SESSION_KEY_PLAN from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id
from authentik.stages.consent.models import ConsentMode, ConsentStage, UserConsent from authentik.stages.consent.models import ConsentMode, ConsentStage, UserConsent
from authentik.stages.consent.stage import PLAN_CONTEXT_CONSENT_PERMISSIONS from authentik.stages.consent.stage import (
PLAN_CONTEXT_CONSENT_PERMISSIONS,
SESSION_KEY_CONSENT_TOKEN,
)
class TestConsentStage(FlowTestCase): class TestConsentStage(FlowTestCase):
@ -37,10 +41,13 @@ class TestConsentStage(FlowTestCase):
plan = FlowPlan(flow_pk=flow.pk.hex, bindings=[binding], markers=[StageMarker()]) plan = FlowPlan(flow_pk=flow.pk.hex, bindings=[binding], markers=[StageMarker()])
session = self.client.session session = self.client.session
session[SESSION_KEY_PLAN] = plan session[SESSION_KEY_PLAN] = plan
session[SESSION_KEY_CONSENT_TOKEN] = str(uuid4())
session.save() session.save()
response = self.client.post( response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
{}, {
"token": session[SESSION_KEY_CONSENT_TOKEN],
},
) )
# pylint: disable=no-member # pylint: disable=no-member
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@ -62,10 +69,13 @@ class TestConsentStage(FlowTestCase):
) )
session = self.client.session session = self.client.session
session[SESSION_KEY_PLAN] = plan session[SESSION_KEY_PLAN] = plan
session[SESSION_KEY_CONSENT_TOKEN] = str(uuid4())
session.save() session.save()
response = self.client.post( response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
{}, {
"token": session[SESSION_KEY_CONSENT_TOKEN],
},
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
@ -96,7 +106,7 @@ class TestConsentStage(FlowTestCase):
{}, {},
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertStageResponse( raw_res = self.assertStageResponse(
response, response,
flow, flow,
self.user, self.user,
@ -105,7 +115,9 @@ class TestConsentStage(FlowTestCase):
) )
response = self.client.post( response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
{}, {
"token": raw_res["token"],
},
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
@ -144,7 +156,7 @@ class TestConsentStage(FlowTestCase):
{}, {},
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertStageResponse( raw_res = self.assertStageResponse(
response, response,
flow, flow,
self.user, self.user,
@ -155,7 +167,9 @@ class TestConsentStage(FlowTestCase):
) )
response = self.client.post( response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
{}, {
"token": raw_res["token"],
},
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
@ -187,7 +201,7 @@ class TestConsentStage(FlowTestCase):
{}, {},
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertStageResponse( raw_res = self.assertStageResponse(
response, response,
flow, flow,
self.user, self.user,
@ -200,7 +214,9 @@ class TestConsentStage(FlowTestCase):
) )
response = self.client.post( response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
{}, {
"token": raw_res["token"],
},
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
@ -209,3 +225,70 @@ class TestConsentStage(FlowTestCase):
user=self.user, application=self.application, permissions="foo bar" user=self.user, application=self.application, permissions="foo bar"
).exists() ).exists()
) )
def test_permanent_same(self):
"""Test permanent consent from user"""
self.client.force_login(self.user)
flow = create_test_flow(FlowDesignation.AUTHENTICATION)
stage = ConsentStage.objects.create(name=generate_id(), mode=ConsentMode.PERMANENT)
binding = FlowStageBinding.objects.create(target=flow, stage=stage, order=2)
plan = FlowPlan(
flow_pk=flow.pk.hex,
bindings=[binding],
markers=[StageMarker()],
context={
PLAN_CONTEXT_APPLICATION: self.application,
},
)
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
# First, consent with a single permission
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
{},
)
self.assertEqual(response.status_code, 200)
raw_res = self.assertStageResponse(
response,
flow,
self.user,
permissions=[],
additional_permissions=[],
)
response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
{
"token": raw_res["token"],
},
)
self.assertEqual(response.status_code, 200)
self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
self.assertTrue(
UserConsent.objects.filter(
user=self.user, application=self.application, permissions=""
).exists()
)
# Request again with the same perms
plan = FlowPlan(
flow_pk=flow.pk.hex,
bindings=[binding],
markers=[StageMarker()],
context={
PLAN_CONTEXT_APPLICATION: self.application,
PLAN_CONTEXT_CONSENT_PERMISSIONS: [],
},
)
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
{},
)
self.assertEqual(response.status_code, 200)
self.assertStageResponse(response, component="xak-flow-redirect")

View File

@ -179,6 +179,32 @@ paths:
$ref: '#/components/schemas/ValidationError' $ref: '#/components/schemas/ValidationError'
'403': '403':
$ref: '#/components/schemas/GenericError' $ref: '#/components/schemas/GenericError'
/authenticators/admin/all/:
get:
operationId: authenticators_admin_all_list
description: Get all devices for current user
parameters:
- in: query
name: user
schema:
type: integer
tags:
- authenticators
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Device'
description: ''
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
/authenticators/admin/duo/: /authenticators/admin/duo/:
get: get:
operationId: authenticators_admin_duo_list operationId: authenticators_admin_duo_list
@ -407,6 +433,30 @@ paths:
$ref: '#/components/schemas/ValidationError' $ref: '#/components/schemas/ValidationError'
'403': '403':
$ref: '#/components/schemas/GenericError' $ref: '#/components/schemas/GenericError'
post:
operationId: authenticators_admin_sms_create
description: Viewset for sms authenticator devices (for admins)
tags:
- authenticators
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/SMSDeviceRequest'
required: true
security:
- authentik: []
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/SMSDevice'
description: ''
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
/authenticators/admin/sms/{id}/: /authenticators/admin/sms/{id}/:
get: get:
operationId: authenticators_admin_sms_retrieve operationId: authenticators_admin_sms_retrieve
@ -433,6 +483,88 @@ paths:
$ref: '#/components/schemas/ValidationError' $ref: '#/components/schemas/ValidationError'
'403': '403':
$ref: '#/components/schemas/GenericError' $ref: '#/components/schemas/GenericError'
put:
operationId: authenticators_admin_sms_update
description: Viewset for sms authenticator devices (for admins)
parameters:
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this SMS Device.
required: true
tags:
- authenticators
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/SMSDeviceRequest'
required: true
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/SMSDevice'
description: ''
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
patch:
operationId: authenticators_admin_sms_partial_update
description: Viewset for sms authenticator devices (for admins)
parameters:
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this SMS Device.
required: true
tags:
- authenticators
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PatchedSMSDeviceRequest'
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/SMSDevice'
description: ''
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
delete:
operationId: authenticators_admin_sms_destroy
description: Viewset for sms authenticator devices (for admins)
parameters:
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this SMS Device.
required: true
tags:
- authenticators
security:
- authentik: []
responses:
'204':
description: No response body
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
/authenticators/admin/static/: /authenticators/admin/static/:
get: get:
operationId: authenticators_admin_static_list operationId: authenticators_admin_static_list
@ -481,6 +613,30 @@ paths:
$ref: '#/components/schemas/ValidationError' $ref: '#/components/schemas/ValidationError'
'403': '403':
$ref: '#/components/schemas/GenericError' $ref: '#/components/schemas/GenericError'
post:
operationId: authenticators_admin_static_create
description: Viewset for static authenticator devices (for admins)
tags:
- authenticators
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/StaticDeviceRequest'
required: true
security:
- authentik: []
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/StaticDevice'
description: ''
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
/authenticators/admin/static/{id}/: /authenticators/admin/static/{id}/:
get: get:
operationId: authenticators_admin_static_retrieve operationId: authenticators_admin_static_retrieve
@ -507,6 +663,88 @@ paths:
$ref: '#/components/schemas/ValidationError' $ref: '#/components/schemas/ValidationError'
'403': '403':
$ref: '#/components/schemas/GenericError' $ref: '#/components/schemas/GenericError'
put:
operationId: authenticators_admin_static_update
description: Viewset for static authenticator devices (for admins)
parameters:
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this static device.
required: true
tags:
- authenticators
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/StaticDeviceRequest'
required: true
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/StaticDevice'
description: ''
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
patch:
operationId: authenticators_admin_static_partial_update
description: Viewset for static authenticator devices (for admins)
parameters:
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this static device.
required: true
tags:
- authenticators
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PatchedStaticDeviceRequest'
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/StaticDevice'
description: ''
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
delete:
operationId: authenticators_admin_static_destroy
description: Viewset for static authenticator devices (for admins)
parameters:
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this static device.
required: true
tags:
- authenticators
security:
- authentik: []
responses:
'204':
description: No response body
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
/authenticators/admin/totp/: /authenticators/admin/totp/:
get: get:
operationId: authenticators_admin_totp_list operationId: authenticators_admin_totp_list
@ -555,6 +793,30 @@ paths:
$ref: '#/components/schemas/ValidationError' $ref: '#/components/schemas/ValidationError'
'403': '403':
$ref: '#/components/schemas/GenericError' $ref: '#/components/schemas/GenericError'
post:
operationId: authenticators_admin_totp_create
description: Viewset for totp authenticator devices (for admins)
tags:
- authenticators
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/TOTPDeviceRequest'
required: true
security:
- authentik: []
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/TOTPDevice'
description: ''
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
/authenticators/admin/totp/{id}/: /authenticators/admin/totp/{id}/:
get: get:
operationId: authenticators_admin_totp_retrieve operationId: authenticators_admin_totp_retrieve
@ -581,6 +843,88 @@ paths:
$ref: '#/components/schemas/ValidationError' $ref: '#/components/schemas/ValidationError'
'403': '403':
$ref: '#/components/schemas/GenericError' $ref: '#/components/schemas/GenericError'
put:
operationId: authenticators_admin_totp_update
description: Viewset for totp authenticator devices (for admins)
parameters:
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this TOTP device.
required: true
tags:
- authenticators
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/TOTPDeviceRequest'
required: true
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/TOTPDevice'
description: ''
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
patch:
operationId: authenticators_admin_totp_partial_update
description: Viewset for totp authenticator devices (for admins)
parameters:
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this TOTP device.
required: true
tags:
- authenticators
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PatchedTOTPDeviceRequest'
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/TOTPDevice'
description: ''
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
delete:
operationId: authenticators_admin_totp_destroy
description: Viewset for totp authenticator devices (for admins)
parameters:
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this TOTP device.
required: true
tags:
- authenticators
security:
- authentik: []
responses:
'204':
description: No response body
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
/authenticators/admin/webauthn/: /authenticators/admin/webauthn/:
get: get:
operationId: authenticators_admin_webauthn_list operationId: authenticators_admin_webauthn_list
@ -629,6 +973,30 @@ paths:
$ref: '#/components/schemas/ValidationError' $ref: '#/components/schemas/ValidationError'
'403': '403':
$ref: '#/components/schemas/GenericError' $ref: '#/components/schemas/GenericError'
post:
operationId: authenticators_admin_webauthn_create
description: Viewset for WebAuthn authenticator devices (for admins)
tags:
- authenticators
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/WebAuthnDeviceRequest'
required: true
security:
- authentik: []
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/WebAuthnDevice'
description: ''
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
/authenticators/admin/webauthn/{id}/: /authenticators/admin/webauthn/{id}/:
get: get:
operationId: authenticators_admin_webauthn_retrieve operationId: authenticators_admin_webauthn_retrieve
@ -655,6 +1023,88 @@ paths:
$ref: '#/components/schemas/ValidationError' $ref: '#/components/schemas/ValidationError'
'403': '403':
$ref: '#/components/schemas/GenericError' $ref: '#/components/schemas/GenericError'
put:
operationId: authenticators_admin_webauthn_update
description: Viewset for WebAuthn authenticator devices (for admins)
parameters:
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this WebAuthn Device.
required: true
tags:
- authenticators
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/WebAuthnDeviceRequest'
required: true
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/WebAuthnDevice'
description: ''
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
patch:
operationId: authenticators_admin_webauthn_partial_update
description: Viewset for WebAuthn authenticator devices (for admins)
parameters:
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this WebAuthn Device.
required: true
tags:
- authenticators
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PatchedWebAuthnDeviceRequest'
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/WebAuthnDevice'
description: ''
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
delete:
operationId: authenticators_admin_webauthn_destroy
description: Viewset for WebAuthn authenticator devices (for admins)
parameters:
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this WebAuthn Device.
required: true
tags:
- authenticators
security:
- authentik: []
responses:
'204':
description: No response body
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
/authenticators/all/: /authenticators/all/:
get: get:
operationId: authenticators_all_list operationId: authenticators_all_list
@ -1601,6 +2051,22 @@ paths:
operationId: core_applications_list operationId: core_applications_list
description: Custom list method that checks Policy based access instead of guardian description: Custom list method that checks Policy based access instead of guardian
parameters: parameters:
- in: query
name: group
schema:
type: string
- in: query
name: meta_description
schema:
type: string
- in: query
name: meta_launch_url
schema:
type: string
- in: query
name: meta_publisher
schema:
type: string
- in: query - in: query
name: name name: name
schema: schema:
@ -5403,7 +5869,7 @@ paths:
/flows/instances/{slug}/export/: /flows/instances/{slug}/export/:
get: get:
operationId: flows_instances_export_retrieve operationId: flows_instances_export_retrieve
description: Export flow to .akflow file description: Export flow to .yaml file
parameters: parameters:
- in: path - in: path
name: slug name: slug
@ -5547,7 +6013,7 @@ paths:
/flows/instances/import_flow/: /flows/instances/import_flow/:
post: post:
operationId: flows_instances_import_flow_create operationId: flows_instances_import_flow_create
description: Import flow from .akflow file description: Import flow from .yaml file
tags: tags:
- flows - flows
requestBody: requestBody:
@ -5564,6 +6030,215 @@ paths:
description: Bad request description: Bad request
'403': '403':
$ref: '#/components/schemas/GenericError' $ref: '#/components/schemas/GenericError'
/managed/blueprints/:
get:
operationId: managed_blueprints_list
description: Blueprint instances
parameters:
- in: query
name: name
schema:
type: string
- name: ordering
required: false
in: query
description: Which field to use when ordering the results.
schema:
type: string
- name: page
required: false
in: query
description: A page number within the paginated result set.
schema:
type: integer
- name: page_size
required: false
in: query
description: Number of results to return per page.
schema:
type: integer
- in: query
name: path
schema:
type: string
- name: search
required: false
in: query
description: A search term.
schema:
type: string
tags:
- managed
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/PaginatedBlueprintInstanceList'
description: ''
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
post:
operationId: managed_blueprints_create
description: Blueprint instances
tags:
- managed
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/BlueprintInstanceRequest'
required: true
security:
- authentik: []
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/BlueprintInstance'
description: ''
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
/managed/blueprints/{instance_uuid}/:
get:
operationId: managed_blueprints_retrieve
description: Blueprint instances
parameters:
- in: path
name: instance_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this Blueprint Instance.
required: true
tags:
- managed
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/BlueprintInstance'
description: ''
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
put:
operationId: managed_blueprints_update
description: Blueprint instances
parameters:
- in: path
name: instance_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this Blueprint Instance.
required: true
tags:
- managed
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/BlueprintInstanceRequest'
required: true
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/BlueprintInstance'
description: ''
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
patch:
operationId: managed_blueprints_partial_update
description: Blueprint instances
parameters:
- in: path
name: instance_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this Blueprint Instance.
required: true
tags:
- managed
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PatchedBlueprintInstanceRequest'
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/BlueprintInstance'
description: ''
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
delete:
operationId: managed_blueprints_destroy
description: Blueprint instances
parameters:
- in: path
name: instance_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this Blueprint Instance.
required: true
tags:
- managed
security:
- authentik: []
responses:
'204':
description: No response body
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
/managed/blueprints/available/:
get:
operationId: managed_blueprints_available_list
description: Get blueprints
tags:
- managed
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
type: array
items:
type: string
description: ''
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
/oauth2/authorization_codes/: /oauth2/authorization_codes/:
get: get:
operationId: oauth2_authorization_codes_list operationId: oauth2_authorization_codes_list
@ -7679,12 +8354,12 @@ paths:
enum: enum:
- authentik.admin - authentik.admin
- authentik.api - authentik.api
- authentik.blueprints
- authentik.core - authentik.core
- authentik.crypto - authentik.crypto
- authentik.events - authentik.events
- authentik.flows - authentik.flows
- authentik.lib - authentik.lib
- authentik.managed
- authentik.outposts - authentik.outposts
- authentik.policies - authentik.policies
- authentik.policies.dummy - authentik.policies.dummy
@ -19141,7 +19816,7 @@ components:
- authentik.stages.user_logout - authentik.stages.user_logout
- authentik.stages.user_write - authentik.stages.user_write
- authentik.tenants - authentik.tenants
- authentik.managed - authentik.blueprints
- authentik.core - authentik.core
type: string type: string
AppleChallengeResponseRequest: AppleChallengeResponseRequest:
@ -20187,6 +20862,60 @@ components:
- POST - POST
- POST_AUTO - POST_AUTO
type: string type: string
BlueprintInstance:
type: object
description: Info about a single blueprint instance file
properties:
name:
type: string
path:
type: string
context:
type: object
additionalProperties: {}
last_applied:
type: string
format: date-time
readOnly: true
status:
$ref: '#/components/schemas/BlueprintInstanceStatusEnum'
enabled:
type: boolean
required:
- context
- last_applied
- name
- path
- status
BlueprintInstanceRequest:
type: object
description: Info about a single blueprint instance file
properties:
name:
type: string
minLength: 1
path:
type: string
minLength: 1
context:
type: object
additionalProperties: {}
status:
$ref: '#/components/schemas/BlueprintInstanceStatusEnum'
enabled:
type: boolean
required:
- context
- name
- path
- status
BlueprintInstanceStatusEnum:
enum:
- successful
- warning
- error
- unknown
type: string
Cache: Cache:
type: object type: object
description: Generic cache stats for an object description: Generic cache stats for an object
@ -20523,11 +21252,14 @@ components:
type: array type: array
items: items:
$ref: '#/components/schemas/Permission' $ref: '#/components/schemas/Permission'
token:
type: string
required: required:
- additional_permissions - additional_permissions
- pending_user - pending_user
- pending_user_avatar - pending_user_avatar
- permissions - permissions
- token
- type - type
ConsentChallengeResponseRequest: ConsentChallengeResponseRequest:
type: object type: object
@ -20537,6 +21269,11 @@ components:
type: string type: string
minLength: 1 minLength: 1
default: ak-stage-consent default: ak-stage-consent
token:
type: string
minLength: 1
required:
- token
ConsentStage: ConsentStage:
type: object type: object
description: ConsentStage Serializer description: ConsentStage Serializer
@ -20742,7 +21479,10 @@ components:
type: type:
type: string type: string
readOnly: true readOnly: true
confirmed:
type: boolean
required: required:
- confirmed
- meta_model_name - meta_model_name
- name - name
- pk - pk
@ -24096,6 +24836,41 @@ components:
required: required:
- pagination - pagination
- results - results
PaginatedBlueprintInstanceList:
type: object
properties:
pagination:
type: object
properties:
next:
type: number
previous:
type: number
count:
type: number
current:
type: number
total_pages:
type: number
start_index:
type: number
end_index:
type: number
required:
- next
- previous
- count
- current
- total_pages
- start_index
- end_index
results:
type: array
items:
$ref: '#/components/schemas/BlueprintInstance'
required:
- pagination
- results
PaginatedCaptchaStageList: PaginatedCaptchaStageList:
type: object type: object
properties: properties:
@ -27003,6 +27778,23 @@ components:
minLength: 1 minLength: 1
description: If any of the user's device has been used within this threshold, description: If any of the user's device has been used within this threshold,
this stage will be skipped this stage will be skipped
PatchedBlueprintInstanceRequest:
type: object
description: Info about a single blueprint instance file
properties:
name:
type: string
minLength: 1
path:
type: string
minLength: 1
context:
type: object
additionalProperties: {}
status:
$ref: '#/components/schemas/BlueprintInstanceStatusEnum'
enabled:
type: boolean
PatchedCaptchaStageRequest: PatchedCaptchaStageRequest:
type: object type: object
description: CaptchaStage Serializer description: CaptchaStage Serializer
@ -30822,13 +31614,6 @@ components:
maxLength: 16 maxLength: 16
required: required:
- token - token
StatusEnum:
enum:
- SUCCESSFUL
- WARNING
- ERROR
- UNKNOWN
type: string
SubModeEnum: SubModeEnum:
enum: enum:
- hashed_user_id - hashed_user_id
@ -30937,7 +31722,7 @@ components:
type: string type: string
format: date-time format: date-time
status: status:
$ref: '#/components/schemas/StatusEnum' $ref: '#/components/schemas/TaskStatusEnum'
messages: messages:
type: array type: array
items: {} items: {}
@ -30947,6 +31732,13 @@ components:
- task_description - task_description
- task_finish_timestamp - task_finish_timestamp
- task_name - task_name
TaskStatusEnum:
enum:
- SUCCESSFUL
- WARNING
- ERROR
- UNKNOWN
type: string
Tenant: Tenant:
type: object type: object
description: Tenant Serializer description: Tenant Serializer

14
web/package-lock.json generated
View File

@ -21,7 +21,7 @@
"@codemirror/legacy-modes": "^6.1.0", "@codemirror/legacy-modes": "^6.1.0",
"@formatjs/intl-listformat": "^7.0.3", "@formatjs/intl-listformat": "^7.0.3",
"@fortawesome/fontawesome-free": "^6.1.1", "@fortawesome/fontawesome-free": "^6.1.1",
"@goauthentik/api": "^2022.7.2-1657137907", "@goauthentik/api": "^2022.7.3-1659304078",
"@jackfranklin/rollup-plugin-markdown": "^0.4.0", "@jackfranklin/rollup-plugin-markdown": "^0.4.0",
"@lingui/cli": "^3.14.0", "@lingui/cli": "^3.14.0",
"@lingui/core": "^3.14.0", "@lingui/core": "^3.14.0",
@ -1922,9 +1922,9 @@
} }
}, },
"node_modules/@goauthentik/api": { "node_modules/@goauthentik/api": {
"version": "2022.7.2-1657137907", "version": "2022.7.3-1659304078",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2022.7.2-1657137907.tgz", "resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2022.7.3-1659304078.tgz",
"integrity": "sha512-dNyoxWOhLpBMFY3u5v0DiWnqzbEApru87Dkl09i9tgAq5TEjENZbE5xsZO8IpjoZJ8rb8uYqFXyBgVLK9mBvgw==" "integrity": "sha512-dWvWB7mlhEZtbFJPVsUCK8Pyxb2lTJhOFmG6oAPVFI0k+TN9HGjY+0FSD+wto+Y9fWq6rPo4lZLaPVJluaA0WA=="
}, },
"node_modules/@humanwhocodes/config-array": { "node_modules/@humanwhocodes/config-array": {
"version": "0.9.2", "version": "0.9.2",
@ -10384,9 +10384,9 @@
"integrity": "sha512-J/3yg2AIXc9wznaVqpHVX3Wa5jwKovVF0AMYSnbmcXTiL3PpRPfF58pzWucCwEiCJBp+hCNRLWClTomD8SseKg==" "integrity": "sha512-J/3yg2AIXc9wznaVqpHVX3Wa5jwKovVF0AMYSnbmcXTiL3PpRPfF58pzWucCwEiCJBp+hCNRLWClTomD8SseKg=="
}, },
"@goauthentik/api": { "@goauthentik/api": {
"version": "2022.7.2-1657137907", "version": "2022.7.3-1659304078",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2022.7.2-1657137907.tgz", "resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2022.7.3-1659304078.tgz",
"integrity": "sha512-dNyoxWOhLpBMFY3u5v0DiWnqzbEApru87Dkl09i9tgAq5TEjENZbE5xsZO8IpjoZJ8rb8uYqFXyBgVLK9mBvgw==" "integrity": "sha512-dWvWB7mlhEZtbFJPVsUCK8Pyxb2lTJhOFmG6oAPVFI0k+TN9HGjY+0FSD+wto+Y9fWq6rPo4lZLaPVJluaA0WA=="
}, },
"@humanwhocodes/config-array": { "@humanwhocodes/config-array": {
"version": "0.9.2", "version": "0.9.2",

View File

@ -64,7 +64,7 @@
"@codemirror/legacy-modes": "^6.1.0", "@codemirror/legacy-modes": "^6.1.0",
"@formatjs/intl-listformat": "^7.0.3", "@formatjs/intl-listformat": "^7.0.3",
"@fortawesome/fontawesome-free": "^6.1.1", "@fortawesome/fontawesome-free": "^6.1.1",
"@goauthentik/api": "^2022.7.2-1657137907", "@goauthentik/api": "^2022.7.3-1659304078",
"@jackfranklin/rollup-plugin-markdown": "^0.4.0", "@jackfranklin/rollup-plugin-markdown": "^0.4.0",
"@lingui/cli": "^3.14.0", "@lingui/cli": "^3.14.0",
"@lingui/core": "^3.14.0", "@lingui/core": "^3.14.0",

View File

@ -32,6 +32,7 @@ export class UserConsentList extends Table<UserConsent> {
return [ return [
new TableColumn(t`Application`, "application"), new TableColumn(t`Application`, "application"),
new TableColumn(t`Expires`, "expires"), new TableColumn(t`Expires`, "expires"),
new TableColumn(t`Permissions`, "permissions"),
]; ];
} }
@ -58,6 +59,10 @@ export class UserConsentList extends Table<UserConsent> {
} }
row(item: UserConsent): TemplateResult[] { row(item: UserConsent): TemplateResult[] {
return [html`${item.application.name}`, html`${item.expires?.toLocaleString()}`]; return [
html`${item.application.name}`,
html`${item.expires?.toLocaleString()}`,
html`${item.permissions || "-"}`,
];
} }
} }

View File

@ -23,17 +23,19 @@ export function readFileAsync(file: Blob) {
}); });
} }
export type KeyUnknown = {
[key: string]: unknown;
};
export class BaseStage<Tin, Tout> extends LitElement { export class BaseStage<Tin, Tout> extends LitElement {
host!: StageHost; host!: StageHost;
@property({ attribute: false }) @property({ attribute: false })
challenge!: Tin; challenge!: Tin;
async submitForm(e: Event): Promise<boolean> { async submitForm(e: Event, defaults?: KeyUnknown): Promise<boolean> {
e.preventDefault(); e.preventDefault();
const object: { const object: KeyUnknown = defaults || {};
[key: string]: unknown;
} = {};
const form = new FormData(this.shadowRoot?.querySelector("form") || undefined); const form = new FormData(this.shadowRoot?.querySelector("form") || undefined);
for await (const [key, value] of form.entries()) { for await (const [key, value] of form.entries()) {

View File

@ -107,7 +107,9 @@ export class ConsentStage extends BaseStage<ConsentChallenge, ConsentChallengeRe
<form <form
class="pf-c-form" class="pf-c-form"
@submit=${(e: Event) => { @submit=${(e: Event) => {
this.submitForm(e); this.submitForm(e, {
token: this.challenge.token,
});
}} }}
> >
<ak-form-static <ak-form-static

View File

@ -7,7 +7,7 @@ import { t } from "@lingui/macro";
import { customElement } from "lit/decorators.js"; import { customElement } from "lit/decorators.js";
import { SourcesApi, StatusEnum } from "@goauthentik/api"; import { SourcesApi, TaskStatusEnum } from "@goauthentik/api";
interface LDAPSyncStats { interface LDAPSyncStats {
healthy: number; healthy: number;
@ -50,7 +50,7 @@ export class LDAPSyncStatusChart extends AKChart<LDAPSyncStats> {
}); });
health.forEach((task) => { health.forEach((task) => {
if (task.status !== StatusEnum.Successful) { if (task.status !== TaskStatusEnum.Successful) {
sourceKey = "failed"; sourceKey = "failed";
} }
const now = new Date().getTime(); const now = new Date().getTime();

View File

@ -24,7 +24,7 @@ import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css"; import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { LDAPSource, SourcesApi, StatusEnum } from "@goauthentik/api"; import { LDAPSource, SourcesApi, TaskStatusEnum } from "@goauthentik/api";
@customElement("ak-source-ldap-view") @customElement("ak-source-ldap-view")
export class LDAPSourceViewPage extends LitElement { export class LDAPSourceViewPage extends LitElement {
@ -145,9 +145,9 @@ export class LDAPSourceViewPage extends LitElement {
return html`<ul class="pf-c-list"> return html`<ul class="pf-c-list">
${tasks.map((task) => { ${tasks.map((task) => {
let header = ""; let header = "";
if (task.status === StatusEnum.Warning) { if (task.status === TaskStatusEnum.Warning) {
header = t`Task finished with warnings`; header = t`Task finished with warnings`;
} else if (task.status === StatusEnum.Error) { } else if (task.status === TaskStatusEnum.Error) {
header = t`Task finished with errors`; header = t`Task finished with errors`;
} else { } else {
header = t`Last sync: ${task.taskFinishTimestamp.toLocaleString()}`; header = t`Last sync: ${task.taskFinishTimestamp.toLocaleString()}`;

View File

@ -14,7 +14,7 @@ import { customElement, property } from "lit/decorators.js";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
import { AdminApi, StatusEnum, Task } from "@goauthentik/api"; import { AdminApi, Task, TaskStatusEnum } from "@goauthentik/api";
@customElement("ak-system-task-list") @customElement("ak-system-task-list")
export class SystemTaskListPage extends TablePage<Task> { export class SystemTaskListPage extends TablePage<Task> {
@ -67,11 +67,11 @@ export class SystemTaskListPage extends TablePage<Task> {
taskStatus(task: Task): TemplateResult { taskStatus(task: Task): TemplateResult {
switch (task.status) { switch (task.status) {
case StatusEnum.Successful: case TaskStatusEnum.Successful:
return html`<ak-label color=${PFColor.Green}>${t`Successful`}</ak-label>`; return html`<ak-label color=${PFColor.Green}>${t`Successful`}</ak-label>`;
case StatusEnum.Warning: case TaskStatusEnum.Warning:
return html`<ak-label color=${PFColor.Orange}>${t`Warning`}</ak-label>`; return html`<ak-label color=${PFColor.Orange}>${t`Warning`}</ak-label>`;
case StatusEnum.Error: case TaskStatusEnum.Error:
return html`<ak-label color=${PFColor.Red}>${t`Error`}</ak-label>`; return html`<ak-label color=${PFColor.Red}>${t`Error`}</ak-label>`;
default: default:
return html`<ak-label color=${PFColor.Grey}>${t`Unknown`}</ak-label>`; return html`<ak-label color=${PFColor.Grey}>${t`Unknown`}</ak-label>`;