Compare commits

..

6 Commits

Author SHA1 Message Date
ad652bde38 more consistent nesting
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-08-24 20:26:40 +02:00
9e813bf404 refactor some more
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-08-24 18:09:41 +02:00
11e708a45a inconsistent naming
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-08-24 16:00:08 +02:00
1e6e4a0bbc refactor from self.executor.current_stage to make nesting easier
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-08-24 15:59:31 +02:00
2149e81d8f base
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-08-24 15:43:33 +02:00
98dc794597 unrelated
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-08-24 15:42:22 +02:00
78 changed files with 1051 additions and 1109 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 2024.8.0-rc1
current_version = 2024.6.4
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?

View File

@ -92,4 +92,4 @@ jobs:
run: make gen-client-ts
- name: test
working-directory: web/
run: npm run test || exit 0
run: npm run test

View File

@ -2,7 +2,7 @@
from os import environ
__version__ = "2024.8.0"
__version__ = "2024.6.4"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -69,8 +69,8 @@ class MessageStage(StageView):
def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""Show a pre-configured message after the flow is done"""
message = getattr(self.executor.current_stage, "message", "")
level = getattr(self.executor.current_stage, "level", messages.SUCCESS)
message = getattr(self.current_stage, "message", "")
level = getattr(self.current_stage, "level", messages.SUCCESS)
messages.add_message(
self.request,
level,
@ -486,9 +486,7 @@ class GroupUpdateStage(StageView):
def handle_groups(self) -> bool:
self.source: Source = self.executor.plan.context[PLAN_CONTEXT_SOURCE]
self.user: User = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
self.group_connection_type: GroupSourceConnection = (
self.executor.current_stage.group_connection_type
)
self.group_connection_type: GroupSourceConnection = self.current_stage.group_connection_type
raw_groups: dict[str, dict[str, Any | dict[str, Any]]] = self.executor.plan.context[
PLAN_CONTEXT_SOURCE_GROUPS

View File

@ -17,7 +17,7 @@ from authentik.flows.challenge import RedirectChallenge
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
from authentik.flows.stage import RedirectStage
from authentik.flows.stage import RedirectStageChallengeView
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.utils.time import timedelta_from_string
from authentik.lib.utils.urls import redirect_with_qs
@ -83,7 +83,7 @@ class RACInterface(InterfaceView):
return super().get_context_data(**kwargs)
class RACFinalStage(RedirectStage):
class RACFinalStage(RedirectStageChallengeView):
"""RAC Connection final stage, set the connection token in the stage"""
endpoint: Endpoint
@ -91,9 +91,9 @@ class RACFinalStage(RedirectStage):
application: Application
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
self.endpoint = self.executor.current_stage.endpoint
self.provider = self.executor.current_stage.provider
self.application = self.executor.current_stage.application
self.endpoint = self.current_stage.endpoint
self.provider = self.current_stage.provider
self.application = self.current_stage.application
# Check policies bound to endpoint directly
engine = PolicyEngine(self.endpoint, self.request.user, self.request)
engine.use_cache = False
@ -132,7 +132,7 @@ class RACFinalStage(RedirectStage):
flow=self.executor.plan.flow_pk,
endpoint=self.endpoint.name,
).from_http(self.request)
self.executor.current_stage.destination = self.request.build_absolute_uri(
self.current_stage.destination = self.request.build_absolute_uri(
reverse("authentik_providers_rac:if-rac", kwargs={"token": str(token.token)})
)
return super().get_challenge(*args, **kwargs)

View File

@ -21,16 +21,15 @@ from authentik.lib.utils.time import timedelta_from_string
PLAN_CONTEXT_RESUME_TOKEN = "resume_token" # nosec
class SourceStageView(ChallengeStageView):
class SourceStageView(ChallengeStageView[SourceStage]):
"""Suspend the current flow execution and send the user to a source,
after which this flow execution is resumed."""
login_button: UILoginButton
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
current_stage: SourceStage = self.executor.current_stage
source: Source = (
Source.objects.filter(pk=current_stage.source_id).select_subclasses().first()
Source.objects.filter(pk=self.current_stage.source_id).select_subclasses().first()
)
if not source:
self.logger.warning("Source does not exist")
@ -56,11 +55,10 @@ class SourceStageView(ChallengeStageView):
pending_user: User = self.get_pending_user()
if pending_user.is_anonymous or not pending_user.pk:
pending_user = get_anonymous_user()
current_stage: SourceStage = self.executor.current_stage
identifier = slugify(f"ak-source-stage-{current_stage.name}-{str(uuid4())}")
identifier = slugify(f"ak-source-stage-{self.current_stage.name}-{str(uuid4())}")
# Don't check for validity here, we only care if the token exists
tokens = FlowToken.objects.filter(identifier=identifier)
valid_delta = timedelta_from_string(current_stage.resume_timeout)
valid_delta = timedelta_from_string(self.current_stage.resume_timeout)
if not tokens.exists():
return FlowToken.objects.create(
expires=now() + valid_delta,

View File

@ -74,9 +74,9 @@ class FlowPlan:
def redirect(self, destination: str):
"""Insert a redirect stage as next stage"""
from authentik.flows.stage import RedirectStage
from authentik.flows.stage import RedirectStageChallengeView
self.insert_stage(in_memory_stage(RedirectStage, destination=destination))
self.insert_stage(in_memory_stage(RedirectStageChallengeView, destination=destination))
def next(self, http_request: HttpRequest | None) -> FlowStageBinding | None:
"""Return next pending stage from the bottom of the list"""

View File

@ -30,6 +30,7 @@ from authentik.lib.avatars import DEFAULT_AVATAR, get_avatar
from authentik.lib.utils.reflection import class_to_path
if TYPE_CHECKING:
from authentik.flows.models import Stage
from authentik.flows.views.executor import FlowExecutorView
PLAN_CONTEXT_PENDING_USER_IDENTIFIER = "pending_user_identifier"
@ -40,20 +41,21 @@ HIST_FLOWS_STAGE_TIME = Histogram(
)
class StageView(View):
class StageView[TStage: "Stage"](View):
"""Abstract Stage"""
executor: "FlowExecutorView"
current_stage: TStage
request: HttpRequest = None
logger: BoundLogger
def __init__(self, executor: "FlowExecutorView", **kwargs):
def __init__(self, executor: "FlowExecutorView", current_stage: TStage | None = None, **kwargs):
self.executor = executor
current_stage = getattr(self.executor, "current_stage", None)
self.current_stage = current_stage or executor.current_stage
self.logger = get_logger().bind(
stage=getattr(current_stage, "name", None),
stage=getattr(self.current_stage, "name", None),
stage_view=class_to_path(type(self)),
)
super().__init__(**kwargs)
@ -80,7 +82,7 @@ class StageView(View):
"""Cleanup session"""
class ChallengeStageView(StageView):
class ChallengeStageView[TStage: "Stage"](StageView[TStage]):
"""Stage view which response with a challenge"""
response_class = ChallengeResponse
@ -253,12 +255,12 @@ class AccessDeniedChallengeView(ChallengeStageView):
return self.executor.cancel()
class RedirectStage(ChallengeStageView):
class RedirectStageChallengeView(ChallengeStageView):
"""Redirect to any URL"""
def get_challenge(self, *args, **kwargs) -> RedirectChallenge:
destination = getattr(
self.executor.current_stage, "destination", reverse("authentik_core:root-redirect")
self.current_stage, "destination", reverse("authentik_core:root-redirect")
)
return RedirectChallenge(
data={

View File

@ -164,7 +164,7 @@ class SAMLProvider(Provider):
)
sign_assertion = models.BooleanField(default=True)
sign_response = models.BooleanField(default=True)
sign_response = models.BooleanField(default=False)
@property
def launch_url(self) -> str | None:

View File

@ -54,11 +54,7 @@ class TestServiceProviderMetadataParser(TestCase):
request = self.factory.get("/")
metadata = lxml_from_string(MetadataProcessor(provider, request).build_entity_descriptor())
schema = etree.XMLSchema(
etree.parse(
source="schemas/saml-schema-metadata-2.0.xsd", parser=etree.XMLParser()
) # nosec
)
schema = etree.XMLSchema(etree.parse("schemas/saml-schema-metadata-2.0.xsd")) # nosec
self.assertTrue(schema.validate(metadata))
def test_schema_want_authn_requests_signed(self):

View File

@ -47,9 +47,7 @@ class TestSchema(TestCase):
metadata = lxml_from_string(request)
schema = etree.XMLSchema(
etree.parse("schemas/saml-schema-protocol-2.0.xsd", parser=etree.XMLParser()) # nosec
)
schema = etree.XMLSchema(etree.parse("schemas/saml-schema-protocol-2.0.xsd")) # nosec
self.assertTrue(schema.validate(metadata))
def test_response_schema(self):
@ -70,7 +68,5 @@ class TestSchema(TestCase):
metadata = lxml_from_string(response)
schema = etree.XMLSchema(
etree.parse("schemas/saml-schema-protocol-2.0.xsd", parser=etree.XMLParser()) # nosec
)
schema = etree.XMLSchema(etree.parse("schemas/saml-schema-protocol-2.0.xsd")) # nosec
self.assertTrue(schema.validate(metadata))

View File

@ -30,9 +30,7 @@ class TestMetadataProcessor(TestCase):
xml = MetadataProcessor(self.source, request).build_entity_descriptor()
metadata = lxml_from_string(xml)
schema = etree.XMLSchema(
etree.parse("schemas/saml-schema-metadata-2.0.xsd", parser=etree.XMLParser()) # nosec
)
schema = etree.XMLSchema(etree.parse("schemas/saml-schema-metadata-2.0.xsd")) # nosec
self.assertTrue(schema.validate(metadata))
def test_metadata_consistent(self):

View File

@ -32,7 +32,7 @@ class AuthenticatorDuoChallengeResponse(ChallengeResponse):
component = CharField(default="ak-stage-authenticator-duo")
class AuthenticatorDuoStageView(ChallengeStageView):
class AuthenticatorDuoStageView(ChallengeStageView[AuthenticatorDuoStage]):
"""Duo stage"""
response_class = AuthenticatorDuoChallengeResponse
@ -40,9 +40,8 @@ class AuthenticatorDuoStageView(ChallengeStageView):
def duo_enroll(self):
"""Enroll User with Duo API and save results"""
user = self.get_pending_user()
stage: AuthenticatorDuoStage = self.executor.current_stage
try:
enroll = stage.auth_client().enroll(user.username)
enroll = self.current_stage.auth_client().enroll(user.username)
except RuntimeError as exc:
Event.new(
EventAction.CONFIGURATION_ERROR,
@ -54,7 +53,6 @@ class AuthenticatorDuoStageView(ChallengeStageView):
return enroll
def get_challenge(self, *args, **kwargs) -> Challenge:
stage: AuthenticatorDuoStage = self.executor.current_stage
if SESSION_KEY_DUO_ENROLL not in self.request.session:
self.duo_enroll()
enroll = self.request.session[SESSION_KEY_DUO_ENROLL]
@ -62,15 +60,14 @@ class AuthenticatorDuoStageView(ChallengeStageView):
data={
"activation_barcode": enroll["activation_barcode"],
"activation_code": enroll["activation_code"],
"stage_uuid": str(stage.stage_uuid),
"stage_uuid": str(self.current_stage.stage_uuid),
}
)
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
# Duo Challenge has already been validated
stage: AuthenticatorDuoStage = self.executor.current_stage
enroll = self.request.session.get(SESSION_KEY_DUO_ENROLL)
enroll_status = stage.auth_client().enroll_status(
enroll_status = self.current_stage.auth_client().enroll_status(
enroll["user_id"], enroll["activation_code"]
)
if enroll_status != "success":
@ -82,7 +79,7 @@ class AuthenticatorDuoStageView(ChallengeStageView):
name="Duo Authenticator",
user=self.get_pending_user(),
duo_user_id=enroll["user_id"],
stage=stage,
stage=self.current_stage,
last_t=now(),
)
else:

View File

@ -57,21 +57,20 @@ class AuthenticatorSMSChallengeResponse(ChallengeResponse):
return super().validate(attrs)
class AuthenticatorSMSStageView(ChallengeStageView):
class AuthenticatorSMSStageView(ChallengeStageView[AuthenticatorSMSStage]):
"""OTP sms Setup stage"""
response_class = AuthenticatorSMSChallengeResponse
def validate_and_send(self, phone_number: str):
"""Validate phone number and send message"""
stage: AuthenticatorSMSStage = self.executor.current_stage
hashed_number = hash_phone_number(phone_number)
query = Q(phone_number=hashed_number) | Q(phone_number=phone_number)
if SMSDevice.objects.filter(query, stage=stage.pk).exists():
if SMSDevice.objects.filter(query, stage=self.current_stage.pk).exists():
raise ValidationError(_("Invalid phone number"))
# No code yet, but we have a phone number, so send a verification message
device: SMSDevice = self.request.session[SESSION_KEY_SMS_DEVICE]
stage.send(device.token, device)
self.current_stage.send(device.token, device)
def _has_phone_number(self) -> str | None:
context = self.executor.plan.context
@ -101,10 +100,10 @@ class AuthenticatorSMSStageView(ChallengeStageView):
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
user = self.get_pending_user()
stage: AuthenticatorSMSStage = self.executor.current_stage
if SESSION_KEY_SMS_DEVICE not in self.request.session:
device = SMSDevice(user=user, confirmed=False, stage=stage, name="SMS Device")
device = SMSDevice(
user=user, confirmed=False, stage=self.current_stage, name="SMS Device"
)
device.generate_token(commit=False)
self.request.session[SESSION_KEY_SMS_DEVICE] = device
if phone_number := self._has_phone_number():
@ -130,8 +129,7 @@ class AuthenticatorSMSStageView(ChallengeStageView):
device: SMSDevice = self.request.session[SESSION_KEY_SMS_DEVICE]
if not device.confirmed:
return self.challenge_invalid(response)
stage: AuthenticatorSMSStage = self.executor.current_stage
if stage.verify_only:
if self.current_stage.verify_only:
self.logger.debug("Hashing number on device")
device.set_hashed_number()
device.save()

View File

@ -29,7 +29,7 @@ class AuthenticatorStaticChallengeResponse(ChallengeResponse):
component = CharField(default="ak-stage-authenticator-static")
class AuthenticatorStaticStageView(ChallengeStageView):
class AuthenticatorStaticStageView(ChallengeStageView[AuthenticatorStaticStage]):
"""Static OTP Setup stage"""
response_class = AuthenticatorStaticChallengeResponse
@ -48,14 +48,14 @@ class AuthenticatorStaticStageView(ChallengeStageView):
self.logger.debug("No pending user, continuing")
return self.executor.stage_ok()
stage: AuthenticatorStaticStage = self.executor.current_stage
if SESSION_STATIC_DEVICE not in self.request.session:
device = StaticDevice(user=user, confirmed=False, name="Static Token")
tokens = []
for _ in range(0, stage.token_count):
for _ in range(0, self.current_stage.token_count):
tokens.append(
StaticToken(device=device, token=generate_id(length=stage.token_length))
StaticToken(
device=device, token=generate_id(length=self.current_stage.token_length)
)
)
self.request.session[SESSION_STATIC_DEVICE] = device
self.request.session[SESSION_STATIC_TOKENS] = tokens

View File

@ -45,7 +45,7 @@ class AuthenticatorTOTPChallengeResponse(ChallengeResponse):
return code
class AuthenticatorTOTPStageView(ChallengeStageView):
class AuthenticatorTOTPStageView(ChallengeStageView[AuthenticatorTOTPStage]):
"""OTP totp Setup stage"""
response_class = AuthenticatorTOTPChallengeResponse
@ -71,11 +71,12 @@ class AuthenticatorTOTPStageView(ChallengeStageView):
self.logger.debug("No pending user, continuing")
return self.executor.stage_ok()
stage: AuthenticatorTOTPStage = self.executor.current_stage
if SESSION_TOTP_DEVICE not in self.request.session:
device = TOTPDevice(
user=user, confirmed=False, digits=stage.digits, name="TOTP Authenticator"
user=user,
confirmed=False,
digits=self.current_stage.digits,
name="TOTP Authenticator",
)
self.request.session[SESSION_TOTP_DEVICE] = device

View File

@ -151,7 +151,7 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
return attrs
class AuthenticatorValidateStageView(ChallengeStageView):
class AuthenticatorValidateStageView(ChallengeStageView[AuthenticatorValidateStage]):
"""Authenticator Validation"""
response_class = AuthenticatorValidationChallengeResponse
@ -177,16 +177,14 @@ class AuthenticatorValidateStageView(ChallengeStageView):
# since their challenges are device-independent
seen_classes = []
stage: AuthenticatorValidateStage = self.executor.current_stage
threshold = timedelta_from_string(stage.last_auth_threshold)
threshold = timedelta_from_string(self.current_stage.last_auth_threshold)
allowed_devices = []
has_webauthn_filters_set = stage.webauthn_allowed_device_types.exists()
has_webauthn_filters_set = self.current_stage.webauthn_allowed_device_types.exists()
for device in user_devices:
device_class = device.__class__.__name__.lower().replace("device", "")
if device_class not in stage.device_classes:
if device_class not in self.current_stage.device_classes:
self.logger.debug("device class not allowed", device_class=device_class)
continue
if isinstance(device, SMSDevice) and device.is_hashed:
@ -199,7 +197,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
and device.device_type
and has_webauthn_filters_set
):
if not stage.webauthn_allowed_device_types.filter(
if not self.current_stage.webauthn_allowed_device_types.filter(
pk=device.device_type.pk
).exists():
self.logger.debug(
@ -216,7 +214,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
data={
"device_class": device_class,
"device_uid": device.pk,
"challenge": get_challenge_for_device(self.request, stage, device),
"challenge": get_challenge_for_device(self.request, self.current_stage, device),
}
)
challenge.is_valid()
@ -235,7 +233,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
"device_uid": -1,
"challenge": get_webauthn_challenge_without_user(
self.request,
self.executor.current_stage,
self.current_stage,
),
}
)
@ -246,7 +244,6 @@ class AuthenticatorValidateStageView(ChallengeStageView):
"""Check if a user is set, and check if the user has any devices
if not, we can skip this entire stage"""
user = self.get_pending_user()
stage: AuthenticatorValidateStage = self.executor.current_stage
if user and not user.is_anonymous:
try:
challenges = self.get_device_challenges()
@ -257,7 +254,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
self.logger.debug("Refusing passwordless flow in non-authentication flow")
return self.executor.stage_ok()
# Passwordless auth, with just webauthn
if DeviceClasses.WEBAUTHN in stage.device_classes:
if DeviceClasses.WEBAUTHN in self.current_stage.device_classes:
self.logger.debug("Flow without user, getting generic webauthn challenge")
challenges = self.get_webauthn_challenge_without_user()
else:
@ -267,13 +264,13 @@ class AuthenticatorValidateStageView(ChallengeStageView):
# No allowed devices
if len(challenges) < 1:
if stage.not_configured_action == NotConfiguredAction.SKIP:
if self.current_stage.not_configured_action == NotConfiguredAction.SKIP:
self.logger.debug("Authenticator not configured, skipping stage")
return self.executor.stage_ok()
if stage.not_configured_action == NotConfiguredAction.DENY:
if self.current_stage.not_configured_action == NotConfiguredAction.DENY:
self.logger.debug("Authenticator not configured, denying")
return self.executor.stage_invalid(_("No (allowed) MFA authenticator configured."))
if stage.not_configured_action == NotConfiguredAction.CONFIGURE:
if self.current_stage.not_configured_action == NotConfiguredAction.CONFIGURE:
self.logger.debug("Authenticator not configured, forcing configure")
return self.prepare_stages(user)
return super().get(request, *args, **kwargs)
@ -282,8 +279,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
"""Check how the user can configure themselves. If no stages are set, return an error.
If a single stage is set, insert that stage directly. If multiple are selected, include
them in the challenge."""
stage: AuthenticatorValidateStage = self.executor.current_stage
if not stage.configuration_stages.exists():
if not self.current_stage.configuration_stages.exists():
Event.new(
EventAction.CONFIGURATION_ERROR,
message=(
@ -293,15 +289,19 @@ class AuthenticatorValidateStageView(ChallengeStageView):
stage=self,
).from_http(self.request).set_user(user).save()
return self.executor.stage_invalid()
if stage.configuration_stages.count() == 1:
next_stage = Stage.objects.get_subclass(pk=stage.configuration_stages.first().pk)
if self.current_stage.configuration_stages.count() == 1:
next_stage = Stage.objects.get_subclass(
pk=self.current_stage.configuration_stages.first().pk
)
self.logger.debug("Single stage configured, auto-selecting", stage=next_stage)
self.executor.plan.context[PLAN_CONTEXT_SELECTED_STAGE] = next_stage
# Because that normal execution only happens on post, we directly inject it here and
# return it
self.executor.plan.insert_stage(next_stage)
return self.executor.stage_ok()
stages = Stage.objects.filter(pk__in=stage.configuration_stages.all()).select_subclasses()
stages = Stage.objects.filter(
pk__in=self.current_stage.configuration_stages.all()
).select_subclasses()
self.executor.plan.context[PLAN_CONTEXT_STAGES] = stages
return super().get(self.request, *args, **kwargs)
@ -309,7 +309,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
res = super().post(request, *args, **kwargs)
if (
PLAN_CONTEXT_SELECTED_STAGE in self.executor.plan.context
and self.executor.current_stage.not_configured_action == NotConfiguredAction.CONFIGURE
and self.current_stage.not_configured_action == NotConfiguredAction.CONFIGURE
):
self.logger.debug("Got selected stage in context, running that")
stage_pk = self.executor.plan.context.get(PLAN_CONTEXT_SELECTED_STAGE)
@ -351,7 +351,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
def cookie_jwt_key(self) -> str:
"""Signing key for MFA Cookie for this stage"""
return sha256(
f"{get_unique_identifier()}:{self.executor.current_stage.pk.hex}".encode("ascii")
f"{get_unique_identifier()}:{self.current_stage.pk.hex}".encode("ascii")
).hexdigest()
def check_mfa_cookie(self, allowed_devices: list[Device]):
@ -362,12 +362,11 @@ class AuthenticatorValidateStageView(ChallengeStageView):
correct user and with an allowed class"""
if COOKIE_NAME_MFA not in self.request.COOKIES:
return
stage: AuthenticatorValidateStage = self.executor.current_stage
threshold = timedelta_from_string(stage.last_auth_threshold)
threshold = timedelta_from_string(self.current_stage.last_auth_threshold)
latest_allowed = datetime.now() + threshold
try:
payload = decode(self.request.COOKIES[COOKIE_NAME_MFA], self.cookie_jwt_key, ["HS256"])
if payload["stage"] != stage.pk.hex:
if payload["stage"] != self.current_stage.pk.hex:
self.logger.warning("Invalid stage PK")
return
if datetime.fromtimestamp(payload["exp"]) > latest_allowed:
@ -385,15 +384,14 @@ class AuthenticatorValidateStageView(ChallengeStageView):
"""Set an MFA cookie to allow users to skip MFA validation in this context (browser)
The cookie is JWT which is signed with a hash of the secret key and the UID of the stage"""
stage: AuthenticatorValidateStage = self.executor.current_stage
delta = timedelta_from_string(stage.last_auth_threshold)
delta = timedelta_from_string(self.current_stage.last_auth_threshold)
if delta.total_seconds() < 1:
self.logger.info("Not setting MFA cookie since threshold is not set.")
return self.executor.stage_ok()
expiry = datetime.now() + delta
cookie_payload = {
"device": device.pk,
"stage": stage.pk.hex,
"stage": self.current_stage.pk.hex,
"exp": expiry.timestamp(),
}
response = self.executor.stage_ok()

View File

@ -108,7 +108,7 @@ class AuthenticatorWebAuthnChallengeResponse(ChallengeResponse):
return registration
class AuthenticatorWebAuthnStageView(ChallengeStageView):
class AuthenticatorWebAuthnStageView(ChallengeStageView[AuthenticatorWebAuthnStage]):
"""WebAuthn stage"""
response_class = AuthenticatorWebAuthnChallengeResponse
@ -116,12 +116,11 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
def get_challenge(self, *args, **kwargs) -> Challenge:
# clear session variables prior to starting a new registration
self.request.session.pop(SESSION_KEY_WEBAUTHN_CHALLENGE, None)
stage: AuthenticatorWebAuthnStage = self.executor.current_stage
user = self.get_pending_user()
# library accepts none so we store null in the database, but if there is a value
# set, cast it to string to ensure it's not a django class
authenticator_attachment = stage.authenticator_attachment
authenticator_attachment = self.current_stage.authenticator_attachment
if authenticator_attachment:
authenticator_attachment = AuthenticatorAttachment(str(authenticator_attachment))
@ -132,8 +131,12 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
user_name=user.username,
user_display_name=user.name,
authenticator_selection=AuthenticatorSelectionCriteria(
resident_key=ResidentKeyRequirement(str(stage.resident_key_requirement)),
user_verification=UserVerificationRequirement(str(stage.user_verification)),
resident_key=ResidentKeyRequirement(
str(self.current_stage.resident_key_requirement)
),
user_verification=UserVerificationRequirement(
str(self.current_stage.user_verification)
),
authenticator_attachment=authenticator_attachment,
),
attestation=AttestationConveyancePreference.DIRECT,

View File

@ -70,7 +70,7 @@ class CaptchaChallengeResponse(ChallengeResponse):
return data
class CaptchaStageView(ChallengeStageView):
class CaptchaStageView(ChallengeStageView[CaptchaChallenge]):
"""Simple captcha checker, logic is handled in django-captcha module"""
response_class = CaptchaChallengeResponse
@ -78,8 +78,8 @@ class CaptchaStageView(ChallengeStageView):
def get_challenge(self, *args, **kwargs) -> Challenge:
return CaptchaChallenge(
data={
"js_url": self.executor.current_stage.js_url,
"site_key": self.executor.current_stage.public_key,
"js_url": self.current_stage.js_url,
"site_key": self.current_stage.public_key,
}
)
@ -87,6 +87,6 @@ class CaptchaStageView(ChallengeStageView):
response = response.validated_data["token"]
self.executor.plan.context[PLAN_CONTEXT_CAPTCHA] = {
"response": response,
"stage": self.executor.current_stage,
"stage": self.current_stage,
}
return self.executor.stage_ok()

View File

@ -48,7 +48,7 @@ class ConsentChallengeResponse(ChallengeResponse):
token = CharField(required=True)
class ConsentStageView(ChallengeStageView):
class ConsentStageView(ChallengeStageView[ConsentStage]):
"""Simple consent checker."""
response_class = ConsentChallengeResponse
@ -72,14 +72,13 @@ class ConsentStageView(ChallengeStageView):
"""Check if the current request should require a prompt for non consent reasons,
i.e. this stage injected from another stage, mode is always requireed or no application
is set."""
current_stage: ConsentStage = self.executor.current_stage
# 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
# prompt
if not isinstance(current_stage, ConsentStage):
if not isinstance(self.current_stage, ConsentStage):
return True
# For always require, we always return the challenge
if current_stage.mode == ConsentMode.ALWAYS_REQUIRE:
if self.current_stage.mode == ConsentMode.ALWAYS_REQUIRE:
return True
# at this point we need to check consent from database
if PLAN_CONTEXT_APPLICATION not in self.executor.plan.context:
@ -125,7 +124,6 @@ class ConsentStageView(ChallengeStageView):
return self.get(self.request)
if self.should_always_prompt():
return self.executor.stage_ok()
current_stage: ConsentStage = self.executor.current_stage
application = self.executor.plan.context[PLAN_CONTEXT_APPLICATION]
permissions = self.executor.plan.context.get(
PLAN_CONTEXT_CONSENT_PERMISSIONS, []
@ -139,9 +137,9 @@ class ConsentStageView(ChallengeStageView):
)
consent: UserConsent = self.executor.plan.context[PLAN_CONTEXT_CONSENT]
consent.permissions = permissions_string
if current_stage.mode == ConsentMode.PERMANENT:
if self.current_stage.mode == ConsentMode.PERMANENT:
consent.expiring = False
if current_stage.mode == ConsentMode.EXPIRING:
consent.expires = now() + timedelta_from_string(current_stage.consent_expire_in)
if self.current_stage.mode == ConsentMode.EXPIRING:
consent.expires = now() + timedelta_from_string(self.current_stage.consent_expire_in)
consent.save()
return self.executor.stage_ok()

View File

@ -6,11 +6,10 @@ from authentik.flows.stage import StageView
from authentik.stages.deny.models import DenyStage
class DenyStageView(StageView):
class DenyStageView(StageView[DenyStage]):
"""Cancels the current flow"""
def dispatch(self, request: HttpRequest) -> HttpResponse:
"""Cancels the current flow"""
stage: DenyStage = self.executor.current_stage
message = self.executor.plan.context.get("deny_message", stage.deny_message)
message = self.executor.plan.context.get("deny_message", self.current_stage.deny_message)
return self.executor.stage_invalid(message)

View File

@ -30,11 +30,11 @@ class DummyStageView(ChallengeStageView):
return self.executor.stage_ok()
def get_challenge(self, *args, **kwargs) -> Challenge:
if self.executor.current_stage.throw_error:
if self.current_stage.throw_error:
raise SentryIgnoredException("Test error")
return DummyChallenge(
data={
"title": self.executor.current_stage.name,
"name": self.executor.current_stage.name,
"title": self.current_stage.name,
"name": self.current_stage.name,
}
)

View File

@ -46,7 +46,7 @@ class EmailChallengeResponse(ChallengeResponse):
raise ValidationError(detail="email-sent", code="email-sent")
class EmailStageView(ChallengeStageView):
class EmailStageView(ChallengeStageView[EmailStage]):
"""Email stage which sends Email for verification"""
response_class = EmailChallengeResponse
@ -72,11 +72,10 @@ class EmailStageView(ChallengeStageView):
def get_token(self) -> FlowToken:
"""Get token"""
pending_user = self.get_pending_user()
current_stage: EmailStage = self.executor.current_stage
valid_delta = timedelta(
minutes=current_stage.token_expiry + 1
minutes=self.current_stage.token_expiry + 1
) # + 1 because django timesince always rounds down
identifier = slugify(f"ak-email-stage-{current_stage.name}-{str(uuid4())}")
identifier = slugify(f"ak-email-stage-{self.current_stage.name}-{str(uuid4())}")
# Don't check for validity here, we only care if the token exists
tokens = FlowToken.objects.filter(identifier=identifier)
if not tokens.exists():
@ -105,15 +104,14 @@ class EmailStageView(ChallengeStageView):
email = self.executor.plan.context.get(PLAN_CONTEXT_EMAIL_OVERRIDE, None)
if not email:
email = pending_user.email
current_stage: EmailStage = self.executor.current_stage
token = self.get_token()
# Send mail to user
try:
message = TemplateEmailMessage(
subject=_(current_stage.subject),
subject=_(self.current_stage.subject),
to=[(pending_user.name, email)],
language=pending_user.locale(self.request),
template_name=current_stage.template,
template_name=self.current_stage.template,
template_context={
"url": self.get_full_url(**{QS_KEY_TOKEN: token.key}),
"user": pending_user,
@ -121,26 +119,28 @@ class EmailStageView(ChallengeStageView):
"token": token.key,
},
)
send_mails(current_stage, message)
send_mails(self.current_stage, message)
except TemplateSyntaxError as exc:
Event.new(
EventAction.CONFIGURATION_ERROR,
message=_("Exception occurred while rendering E-mail template"),
error=exception_to_string(exc),
template=current_stage.template,
template=self.current_stage.template,
).from_http(self.request)
raise StageInvalidException from exc
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
# Check if the user came back from the email link to verify
restore_token: FlowToken = self.executor.plan.context.get(PLAN_CONTEXT_IS_RESTORED, None)
restore_token: FlowToken | None = self.executor.plan.context.get(
PLAN_CONTEXT_IS_RESTORED, None
)
user = self.get_pending_user()
if restore_token:
if restore_token.user != user:
self.logger.warning("Flow token for non-matching user, denying request")
return self.executor.stage_invalid()
messages.success(request, _("Successfully verified Email."))
if self.executor.current_stage.activate_user_on_success:
if self.current_stage.activate_user_on_success:
user.is_active = True
user.save()
return self.executor.stage_ok()

View File

@ -27,6 +27,7 @@ class IdentificationStageSerializer(StageSerializer):
fields = StageSerializer.Meta.fields + [
"user_fields",
"password_stage",
"captcha_stage",
"case_insensitive_matching",
"show_matched_user",
"enrollment_flow",

View File

@ -0,0 +1,26 @@
# Generated by Django 5.0.8 on 2024-08-24 12:58
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_stages_captcha", "0003_captchastage_error_on_invalid_score_and_more"),
("authentik_stages_identification", "0014_identificationstage_pretend"),
]
operations = [
migrations.AddField(
model_name="identificationstage",
name="captcha_stage",
field=models.ForeignKey(
default=None,
help_text="When set, the captcha element is shown on the identification stage.",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="authentik_stages_captcha.captchastage",
),
),
]

View File

@ -8,6 +8,7 @@ from rest_framework.serializers import BaseSerializer
from authentik.core.models import Source
from authentik.flows.models import Flow, Stage
from authentik.stages.captcha.models import CaptchaStage
from authentik.stages.password.models import PasswordStage
@ -42,6 +43,15 @@ class IdentificationStage(Stage):
),
),
)
captcha_stage = models.ForeignKey(
CaptchaStage,
null=True,
default=None,
on_delete=models.SET_NULL,
help_text=_(
("When set, the captcha element is shown on the identification stage."),
),
)
case_insensitive_matching = models.BooleanField(
default=True,

View File

@ -30,9 +30,14 @@ from authentik.lib.utils.urls import reverse_with_qs
from authentik.root.middleware import ClientIPMiddleware
from authentik.sources.oauth.types.apple import AppleLoginChallenge
from authentik.sources.plex.models import PlexAuthenticationChallenge
from authentik.stages.captcha.stage import (
CaptchaChallenge,
CaptchaChallengeResponse,
CaptchaStageView,
)
from authentik.stages.identification.models import IdentificationStage
from authentik.stages.identification.signals import identification_failed
from authentik.stages.password.stage import authenticate
from authentik.stages.password.stage import PasswordChallenge, PasswordStageView, authenticate
@extend_schema_field(
@ -63,8 +68,8 @@ class IdentificationChallenge(Challenge):
"""Identification challenges with all UI elements"""
user_fields = ListField(child=CharField(), allow_empty=True, allow_null=True)
password_fields = BooleanField()
allow_show_password = BooleanField(default=False)
password_stage = PasswordChallenge(required=False)
captcha_stage = CaptchaChallenge(required=False)
application_pre = CharField(required=False)
flow_designation = ChoiceField(FlowDesignation.choices)
@ -84,6 +89,7 @@ class IdentificationChallengeResponse(ChallengeResponse):
uid_field = CharField()
password = CharField(required=False, allow_blank=True, allow_null=True)
component = CharField(default="ak-stage-identification")
captcha = CaptchaChallengeResponse(required=False)
pre_user: User | None = None
@ -128,49 +134,50 @@ class IdentificationChallengeResponse(ChallengeResponse):
return attrs
raise ValidationError("Failed to authenticate.")
self.pre_user = pre_user
if not current_stage.password_stage:
# No password stage select, don't validate the password
return attrs
password = attrs.get("password", None)
if not password:
self.stage.logger.warning("Password not set for ident+auth attempt")
try:
with start_span(
op="authentik.stages.identification.authenticate",
description="User authenticate call (combo stage)",
):
user = authenticate(
self.stage.request,
current_stage.password_stage.backends,
current_stage,
username=self.pre_user.username,
password=password,
)
if not user:
raise ValidationError("Failed to authenticate.")
self.pre_user = user
except PermissionDenied as exc:
raise ValidationError(str(exc)) from exc
if current_stage.password_stage:
password = attrs.get("password", None)
if not password:
self.stage.logger.warning("Password not set for ident+auth attempt")
try:
with start_span(
op="authentik.stages.identification.authenticate",
description="User authenticate call (combo stage)",
):
user = authenticate(
self.stage.request,
current_stage.password_stage.backends,
current_stage,
username=self.pre_user.username,
password=password,
)
if not user:
raise ValidationError("Failed to authenticate.")
self.pre_user = user
except PermissionDenied as exc:
raise ValidationError(str(exc)) from exc
print(attrs)
# if current_stage.captcha_stage:
# captcha = CaptchaStageView(self.stage.executor)
# captcha.stage = current_stage.captcha_stage
# captcha.challenge_valid(attrs.get("captcha"))
return attrs
class IdentificationStageView(ChallengeStageView):
class IdentificationStageView(ChallengeStageView[IdentificationStage]):
"""Form to identify the user"""
response_class = IdentificationChallengeResponse
def get_user(self, uid_value: str) -> User | None:
"""Find user instance. Returns None if no user was found."""
current_stage: IdentificationStage = self.executor.current_stage
query = Q()
for search_field in current_stage.user_fields:
for search_field in self.current_stage.user_fields:
model_field = {
"email": "email",
"username": "username",
"upn": "attributes__upn",
}[search_field]
if current_stage.case_insensitive_matching:
if self.current_stage.case_insensitive_matching:
model_field += "__iexact"
else:
model_field += "__exact"
@ -191,16 +198,12 @@ class IdentificationStageView(ChallengeStageView):
return _("Continue")
def get_challenge(self) -> Challenge:
current_stage: IdentificationStage = self.executor.current_stage
challenge = IdentificationChallenge(
data={
"component": "ak-stage-identification",
"primary_action": self.get_primary_action(),
"user_fields": current_stage.user_fields,
"password_fields": bool(current_stage.password_stage),
"allow_show_password": bool(current_stage.password_stage)
and current_stage.password_stage.allow_show_password,
"show_source_labels": current_stage.show_source_labels,
"user_fields": self.current_stage.user_fields,
"show_source_labels": self.current_stage.show_source_labels,
"flow_designation": self.executor.flow.designation,
}
)
@ -212,29 +215,39 @@ class IdentificationStageView(ChallengeStageView):
).name
get_qs = self.request.session.get(SESSION_KEY_GET, self.request.GET)
# Check for related enrollment and recovery flow, add URL to view
if current_stage.enrollment_flow:
if self.current_stage.enrollment_flow:
challenge.initial_data["enroll_url"] = reverse_with_qs(
"authentik_core:if-flow",
query=get_qs,
kwargs={"flow_slug": current_stage.enrollment_flow.slug},
kwargs={"flow_slug": self.current_stage.enrollment_flow.slug},
)
if current_stage.recovery_flow:
if self.current_stage.recovery_flow:
challenge.initial_data["recovery_url"] = reverse_with_qs(
"authentik_core:if-flow",
query=get_qs,
kwargs={"flow_slug": current_stage.recovery_flow.slug},
kwargs={"flow_slug": self.current_stage.recovery_flow.slug},
)
if current_stage.passwordless_flow:
if self.current_stage.passwordless_flow:
challenge.initial_data["passwordless_url"] = reverse_with_qs(
"authentik_core:if-flow",
query=get_qs,
kwargs={"flow_slug": current_stage.passwordless_flow.slug},
kwargs={"flow_slug": self.current_stage.passwordless_flow.slug},
)
if self.current_stage.password_stage:
password = PasswordStageView(self.executor, self.current_stage.captcha_stage)
password_challenge = password.get_challenge()
password_challenge.is_valid()
challenge.initial_data["password_stage"] = password_challenge.data
if self.current_stage.captcha_stage:
captcha = CaptchaStageView(self.executor, self.current_stage.captcha_stage)
captcha_challenge = captcha.get_challenge()
captcha_challenge.is_valid()
challenge.initial_data["captcha_stage"] = captcha_challenge.data
# Check all enabled source, add them if they have a UI Login button.
ui_sources = []
sources: list[Source] = (
current_stage.sources.filter(enabled=True).order_by("name").select_subclasses()
self.current_stage.sources.filter(enabled=True).order_by("name").select_subclasses()
)
for source in sources:
ui_login_button = source.ui_login_button(self.request)
@ -249,8 +262,7 @@ class IdentificationStageView(ChallengeStageView):
def challenge_valid(self, response: IdentificationChallengeResponse) -> HttpResponse:
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = response.pre_user
current_stage: IdentificationStage = self.executor.current_stage
if not current_stage.show_matched_user:
if not self.current_stage.show_matched_user:
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER_IDENTIFIER] = (
response.validated_data.get("uid_field")
)

View File

@ -17,7 +17,7 @@ INVITATION_IN_EFFECT = "invitation_in_effect"
INVITATION = "invitation"
class InvitationStageView(StageView):
class InvitationStageView(StageView[InvitationStage]):
"""Finalise Authentication flow by logging the user in"""
def get_token(self) -> str | None:
@ -52,11 +52,10 @@ class InvitationStageView(StageView):
def dispatch(self, request: HttpRequest) -> HttpResponse:
"""Apply data to the current flow based on a URL"""
stage: InvitationStage = self.executor.current_stage
invite = self.get_invite()
if not invite:
if stage.continue_flow_without_invitation:
if self.current_stage.continue_flow_without_invitation:
return self.executor.stage_ok()
return self.executor.stage_invalid(_("Invalid invite/invite not found"))

View File

@ -130,7 +130,7 @@ class PasswordChallengeResponse(ChallengeResponse):
return password
class PasswordStageView(ChallengeStageView):
class PasswordStageView(ChallengeStageView[PasswordStage]):
"""Authentication stage which authenticates against django's AuthBackend"""
response_class = PasswordChallengeResponse
@ -138,7 +138,7 @@ class PasswordStageView(ChallengeStageView):
def get_challenge(self) -> Challenge:
challenge = PasswordChallenge(
data={
"allow_show_password": self.executor.current_stage.allow_show_password,
"allow_show_password": self.current_stage.allow_show_password,
}
)
recovery_flow = Flow.objects.filter(designation=FlowDesignation.RECOVERY)
@ -154,10 +154,9 @@ class PasswordStageView(ChallengeStageView):
if SESSION_KEY_INVALID_TRIES not in self.request.session:
self.request.session[SESSION_KEY_INVALID_TRIES] = 0
self.request.session[SESSION_KEY_INVALID_TRIES] += 1
current_stage: PasswordStage = self.executor.current_stage
if (
self.request.session[SESSION_KEY_INVALID_TRIES]
>= current_stage.failed_attempts_before_cancel
>= self.current_stage.failed_attempts_before_cancel
):
self.logger.debug("User has exceeded maximum tries")
del self.request.session[SESSION_KEY_INVALID_TRIES]

View File

@ -222,7 +222,7 @@ class PromptStageView(ChallengeStageView):
return serializers
def get_challenge(self, *args, **kwargs) -> Challenge:
fields: list[Prompt] = list(self.executor.current_stage.fields.all().order_by("order"))
fields: list[Prompt] = list(self.current_stage.fields.all().order_by("order"))
context_prompt = self.executor.plan.context.get(PLAN_CONTEXT_PROMPT, {})
serializers = self.get_prompt_challenge_fields(fields, context_prompt)
challenge = PromptChallenge(
@ -239,7 +239,7 @@ class PromptStageView(ChallengeStageView):
instance=None,
data=data,
request=self.request,
stage_instance=self.executor.current_stage,
stage_instance=self.current_stage,
stage=self,
plan=self.executor.plan,
user=self.get_pending_user(),

View File

@ -7,9 +7,10 @@ from django.utils.translation import gettext as _
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import StageView
from authentik.stages.user_delete.models import UserDeleteStage
class UserDeleteStageView(StageView):
class UserDeleteStageView(StageView[UserDeleteStage]):
"""Finalise unenrollment flow by deleting the user object."""
def dispatch(self, request: HttpRequest) -> HttpResponse:

View File

@ -39,7 +39,7 @@ class UserLoginChallengeResponse(ChallengeResponse):
remember_me = BooleanField(required=True)
class UserLoginStageView(ChallengeStageView):
class UserLoginStageView(ChallengeStageView[UserLoginStage]):
"""Finalise Authentication flow by logging the user in"""
response_class = UserLoginChallengeResponse
@ -49,8 +49,7 @@ class UserLoginStageView(ChallengeStageView):
def dispatch(self, request: HttpRequest) -> HttpResponse:
"""Check for remember_me, and do login"""
stage: UserLoginStage = self.executor.current_stage
if timedelta_from_string(stage.remember_me_offset).total_seconds() > 0:
if timedelta_from_string(self.current_stage.remember_me_offset).total_seconds() > 0:
return super().dispatch(request)
return self.do_login(request)
@ -59,9 +58,9 @@ class UserLoginStageView(ChallengeStageView):
def set_session_duration(self, remember: bool) -> timedelta:
"""Update the sessions' expiry"""
delta = timedelta_from_string(self.executor.current_stage.session_duration)
delta = timedelta_from_string(self.current_stage.session_duration)
if remember:
offset = timedelta_from_string(self.executor.current_stage.remember_me_offset)
offset = timedelta_from_string(self.current_stage.remember_me_offset)
delta = delta + offset
if delta.total_seconds() == 0:
self.request.session.set_expiry(0)
@ -71,11 +70,9 @@ class UserLoginStageView(ChallengeStageView):
def set_session_ip(self):
"""Set the sessions' last IP and session bindings"""
stage: UserLoginStage = self.executor.current_stage
self.request.session[SESSION_KEY_LAST_IP] = ClientIPMiddleware.get_client_ip(self.request)
self.request.session[SESSION_KEY_BINDING_NET] = stage.network_binding
self.request.session[SESSION_KEY_BINDING_GEO] = stage.geoip_binding
self.request.session[SESSION_KEY_BINDING_NET] = self.current_stage.network_binding
self.request.session[SESSION_KEY_BINDING_GEO] = self.current_stage.geoip_binding
def do_login(self, request: HttpRequest, remember: bool = False) -> HttpResponse:
"""Attach the currently pending user to the current session"""
@ -111,7 +108,7 @@ class UserLoginStageView(ChallengeStageView):
# as sources show their own success messages
if not self.executor.plan.context.get(PLAN_CONTEXT_SOURCE, None):
messages.success(self.request, _("Successfully logged in!"))
if self.executor.current_stage.terminate_other_sessions:
if self.current_stage.terminate_other_sessions:
AuthenticatedSession.objects.filter(
user=user,
).exclude(session_key=self.request.session.session_key).delete()

View File

@ -4,9 +4,10 @@ from django.contrib.auth import logout
from django.http import HttpRequest, HttpResponse
from authentik.flows.stage import StageView
from authentik.stages.user_logout.models import UserLogoutStage
class UserLogoutStageView(StageView):
class UserLogoutStageView(StageView[UserLogoutStage]):
"""Finalise Authentication flow by logging the user in"""
def dispatch(self, request: HttpRequest) -> HttpResponse:

View File

@ -55,7 +55,7 @@ class UserWriteStageView(StageView):
"""Ensure a user exists"""
user_created = False
path = self.executor.plan.context.get(
PLAN_CONTEXT_USER_PATH, self.executor.current_stage.user_path_template
PLAN_CONTEXT_USER_PATH, self.current_stage.user_path_template
)
if path == "":
path = User.default_path()
@ -64,11 +64,11 @@ class UserWriteStageView(StageView):
user_type = UserTypes(
self.executor.plan.context.get(
PLAN_CONTEXT_USER_TYPE,
self.executor.current_stage.user_type,
self.current_stage.user_type,
)
)
except ValueError:
user_type = self.executor.current_stage.user_type
user_type = self.current_stage.user_type
if user_type == UserTypes.INTERNAL_SERVICE_ACCOUNT:
user_type = UserTypes.SERVICE_ACCOUNT
@ -76,12 +76,12 @@ class UserWriteStageView(StageView):
self.executor.plan.context.setdefault(PLAN_CONTEXT_PENDING_USER, self.request.user)
if (
PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context
or self.executor.current_stage.user_creation_mode == UserCreationMode.ALWAYS_CREATE
or self.current_stage.user_creation_mode == UserCreationMode.ALWAYS_CREATE
):
if self.executor.current_stage.user_creation_mode == UserCreationMode.NEVER_CREATE:
if self.current_stage.user_creation_mode == UserCreationMode.NEVER_CREATE:
return None, False
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = User(
is_active=not self.executor.current_stage.create_users_as_inactive,
is_active=not self.current_stage.create_users_as_inactive,
path=path,
type=user_type,
)
@ -180,8 +180,8 @@ class UserWriteStageView(StageView):
try:
with transaction.atomic():
user.save()
if self.executor.current_stage.create_users_group:
user.ak_groups.add(self.executor.current_stage.create_users_group)
if self.current_stage.create_users_group:
user.ak_groups.add(self.current_stage.create_users_group)
if PLAN_CONTEXT_GROUPS in self.executor.plan.context:
user.ak_groups.add(*self.executor.plan.context[PLAN_CONTEXT_GROUPS])
except (IntegrityError, ValueError, TypeError, InternalError) as exc:

View File

@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://goauthentik.io/blueprints/schema.json",
"type": "object",
"title": "authentik 2024.8.0 Blueprint schema",
"title": "authentik 2024.6.4 Blueprint schema",
"required": [
"version",
"entries"
@ -10091,6 +10091,11 @@
"title": "Password stage",
"description": "When set, shows a password field, instead of showing the password field as separate step."
},
"captcha_stage": {
"type": "integer",
"title": "Captcha stage",
"description": "When set, the captcha element is shown on the identification stage."
},
"case_insensitive_matching": {
"type": "boolean",
"title": "Case insensitive matching",

View File

@ -31,7 +31,7 @@ services:
volumes:
- redis:/data
server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.8.0}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.4}
restart: unless-stopped
command: server
environment:
@ -52,7 +52,7 @@ services:
- postgresql
- redis
worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.8.0}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.4}
restart: unless-stopped
command: worker
environment:

6
go.mod
View File

@ -18,18 +18,18 @@ require (
github.com/gorilla/securecookie v1.1.2
github.com/gorilla/sessions v1.4.0
github.com/gorilla/websocket v1.5.3
github.com/jellydator/ttlcache/v3 v3.2.1
github.com/jellydator/ttlcache/v3 v3.2.0
github.com/mitchellh/mapstructure v1.5.0
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
github.com/pires/go-proxyproto v0.7.0
github.com/prometheus/client_golang v1.20.2
github.com/prometheus/client_golang v1.20.1
github.com/redis/go-redis/v9 v9.6.1
github.com/sethvargo/go-envconfig v1.1.0
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.8.1
github.com/stretchr/testify v1.9.0
github.com/wwt/guac v1.3.2
goauthentik.io/api/v3 v3.2024064.1
goauthentik.io/api/v3 v3.2024063.13
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.22.0
golang.org/x/sync v0.8.0

16
go.sum
View File

@ -200,8 +200,8 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jellydator/ttlcache/v3 v3.2.1 h1:eS8ljnYY7BllYGkXw/TfczWZrXUu/CH7SIkC6ugn9Js=
github.com/jellydator/ttlcache/v3 v3.2.1/go.mod h1:bj2/e0l4jRnQdrnSTaGTsh4GSXvMjQcy41i7th0GVGw=
github.com/jellydator/ttlcache/v3 v3.2.0 h1:6lqVJ8X3ZaUwvzENqPAobDsXNExfUJd61u++uW8a3LE=
github.com/jellydator/ttlcache/v3 v3.2.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
@ -239,8 +239,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.20.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg=
github.com/prometheus/client_golang v1.20.2/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_golang v1.20.1 h1:IMJXHOD6eARkQpxo8KkhgEVFlBNm+nkrFUyGlIu7Na8=
github.com/prometheus/client_golang v1.20.1/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
@ -297,10 +297,10 @@ go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucg
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
goauthentik.io/api/v3 v3.2024064.1 h1:vxquklgDGD+nGFhWRAsQ7ezQKg17MRq6bzEk25fbsb4=
goauthentik.io/api/v3 v3.2024064.1/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
goauthentik.io/api/v3 v3.2024063.13 h1:zWFlrr+8NOaQOCPSRV1FhbDJ58+BPa9BqjNvl4T//s8=
goauthentik.io/api/v3 v3.2024063.13/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=

View File

@ -29,4 +29,4 @@ func UserAgent() string {
return fmt.Sprintf("authentik@%s", FullVersion())
}
const VERSION = "2024.8.0"
const VERSION = "2024.6.4"

View File

@ -1,5 +1,5 @@
{
"name": "@goauthentik/authentik",
"version": "2024.8.0",
"version": "2024.6.4",
"private": true
}

8
poetry.lock generated
View File

@ -1312,17 +1312,17 @@ django = ">=3"
[[package]]
name = "django-pglock"
version = "1.6.0"
version = "1.5.1"
description = "Postgres locking routines and lock table access."
optional = false
python-versions = "<4,>=3.8.0"
files = [
{file = "django_pglock-1.6.0-py3-none-any.whl", hash = "sha256:41c98d0bd3738d11e6eaefcc3e5146028f118a593ac58c13d663b751170f01de"},
{file = "django_pglock-1.6.0.tar.gz", hash = "sha256:724450ecc9886f39af599c477d84ad086545a5373215ef7a670cd25faca25a61"},
{file = "django_pglock-1.5.1-py3-none-any.whl", hash = "sha256:d3b977922abbaffd43968714b69cdab7453866adf2b0695fb497491748d7bc67"},
{file = "django_pglock-1.5.1.tar.gz", hash = "sha256:291903d5d877b68558003e1d64d764ebd5590344ba3b7aa1d5127df5947869b1"},
]
[package.dependencies]
django = ">=4"
django = ">=3"
django-pgactivity = ">=1.2,<2"
[[package]]

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "authentik"
version = "2024.8.0"
version = "2024.6.4"
description = ""
authors = ["authentik Team <hello@goauthentik.io>"]

View File

@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: authentik
version: 2024.8.0
version: 2024.6.4
description: Making authentication simple.
contact:
email: hello@goauthentik.io
@ -40457,11 +40457,10 @@ components:
items:
type: string
nullable: true
password_fields:
type: boolean
allow_show_password:
type: boolean
default: false
password_stage:
$ref: '#/components/schemas/PasswordChallenge'
captcha_stage:
$ref: '#/components/schemas/CaptchaChallenge'
application_pre:
type: string
flow_designation:
@ -40482,7 +40481,6 @@ components:
type: boolean
required:
- flow_designation
- password_fields
- primary_action
- show_source_labels
- user_fields
@ -40500,6 +40498,8 @@ components:
password:
type: string
nullable: true
captcha:
$ref: '#/components/schemas/CaptchaChallengeResponseRequest'
required:
- uid_field
IdentificationStage:
@ -40545,6 +40545,12 @@ components:
nullable: true
description: When set, shows a password field, instead of showing the password
field as separate step.
captcha_stage:
type: string
format: uuid
nullable: true
description: When set, the captcha element is shown on the identification
stage.
case_insensitive_matching:
type: boolean
description: When enabled, user fields are matched regardless of their casing.
@ -40613,6 +40619,12 @@ components:
nullable: true
description: When set, shows a password field, instead of showing the password
field as separate step.
captcha_stage:
type: string
format: uuid
nullable: true
description: When set, the captcha element is shown on the identification
stage.
case_insensitive_matching:
type: boolean
description: When enabled, user fields are matched regardless of their casing.
@ -45745,6 +45757,12 @@ components:
nullable: true
description: When set, shows a password field, instead of showing the password
field as separate step.
captcha_stage:
type: string
format: uuid
nullable: true
description: When set, the captcha element is shown on the identification
stage.
case_insensitive_matching:
type: boolean
description: When enabled, user fields are matched regardless of their casing.

904
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -32,7 +32,7 @@
"guacamole-common-js": "^1.5.0",
"lit": "^3.2.0",
"md-front-matter": "^1.0.4",
"mermaid": "^11.0.2",
"mermaid": "^10.9.1",
"rapidoc": "^9.3.4",
"showdown": "^2.1.0",
"style-mod": "^4.1.2",
@ -51,7 +51,7 @@
"@babel/preset-typescript": "^7.24.7",
"@changesets/cli": "^2.27.5",
"@custom-elements-manifest/analyzer": "^0.10.2",
"@eslint/js": "^9.9.1",
"@eslint/js": "^9.9.0",
"@genesiscommunitysuccess/custom-elements-lsp": "^5.0.3",
"@hcaptcha/types": "^1.0.4",
"@jeysal/storybook-addon-css-user-preferences": "^0.2.0",
@ -101,10 +101,10 @@
"rollup-plugin-postcss-lit": "^2.1.0",
"storybook": "^8.1.11",
"storybook-addon-mock": "^5.0.0",
"syncpack": "^13.0.0",
"syncpack": "^12.3.3",
"ts-lit-plugin": "^2.0.2",
"ts-node": "^10.9.2",
"tslib": "^2.7.0",
"tslib": "^2.6.3",
"turnstile-types": "^1.2.2",
"typescript": "^5.5.4",
"typescript-eslint": "^8.2.0",

View File

@ -14,7 +14,7 @@
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-swc": "^0.3.1",
"@swc/cli": "^0.4.0",
"@swc/core": "^1.7.18",
"@swc/core": "^1.7.14",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/jquery": "^3.5.30",
"lockfile-lint": "^4.14.0",
@ -25,7 +25,7 @@
},
"license": "MIT",
"optionalDependencies": {
"@swc/core": "^1.7.18",
"@swc/core": "^1.7.14",
"@swc/core-darwin-arm64": "^1.6.13",
"@swc/core-darwin-x64": "^1.6.13",
"@swc/core-linux-arm-gnueabihf": "^1.6.13",

View File

@ -9,7 +9,7 @@
"version": "0.0.0",
"license": "MIT",
"dependencies": {
"@goauthentik/api": "^2024.6.3-1724414734",
"@goauthentik/api": "^2024.6.3-1724337552",
"base64-js": "^1.5.1",
"bootstrap": "^4.6.1",
"formdata-polyfill": "^4.0.10",
@ -21,16 +21,16 @@
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-swc": "^0.3.1",
"@swc/cli": "^0.4.0",
"@swc/core": "^1.7.18",
"@swc/core": "^1.7.14",
"@types/jquery": "^3.5.30",
"rollup": "^4.21.0",
"rollup-plugin-copy": "^3.5.0"
}
},
"node_modules/@goauthentik/api": {
"version": "2024.6.3-1724414734",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.6.3-1724414734.tgz",
"integrity": "sha512-2fLKwOh2Znc/unD8Q2U4G0g5QFM4jVqC95e5VRWWVnzp3xB7JWfEDBcRdwyv5PxCdmjBUkvbiul0kiuRwqBf4w=="
"version": "2024.6.3-1724337552",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.6.3-1724337552.tgz",
"integrity": "sha512-siu5qJqUt13iUPsLI0RfieVkDU8IMhuP2i5C/RRqY6oek0z+srSom9UTBAh6n6a2pTTNQO3clE2zxvAIJPahVg=="
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
@ -491,9 +491,9 @@
}
},
"node_modules/@swc/core": {
"version": "1.7.18",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.18.tgz",
"integrity": "sha512-qL9v5N5S38ijmqiQRvCFUUx2vmxWT/JJ2rswElnyaHkOHuVoAFhBB90Ywj4RKjh3R0zOjhEcemENTyF3q3G6WQ==",
"version": "1.7.14",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.14.tgz",
"integrity": "sha512-9aeXeifnyuvc2pcuuhPQgVUwdpGEzZ+9nJu0W8/hNl/aESFsJGR5i9uQJRGu0atoNr01gK092fvmqMmQAPcKow==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
@ -508,16 +508,16 @@
"url": "https://opencollective.com/swc"
},
"optionalDependencies": {
"@swc/core-darwin-arm64": "1.7.18",
"@swc/core-darwin-x64": "1.7.18",
"@swc/core-linux-arm-gnueabihf": "1.7.18",
"@swc/core-linux-arm64-gnu": "1.7.18",
"@swc/core-linux-arm64-musl": "1.7.18",
"@swc/core-linux-x64-gnu": "1.7.18",
"@swc/core-linux-x64-musl": "1.7.18",
"@swc/core-win32-arm64-msvc": "1.7.18",
"@swc/core-win32-ia32-msvc": "1.7.18",
"@swc/core-win32-x64-msvc": "1.7.18"
"@swc/core-darwin-arm64": "1.7.14",
"@swc/core-darwin-x64": "1.7.14",
"@swc/core-linux-arm-gnueabihf": "1.7.14",
"@swc/core-linux-arm64-gnu": "1.7.14",
"@swc/core-linux-arm64-musl": "1.7.14",
"@swc/core-linux-x64-gnu": "1.7.14",
"@swc/core-linux-x64-musl": "1.7.14",
"@swc/core-win32-arm64-msvc": "1.7.14",
"@swc/core-win32-ia32-msvc": "1.7.14",
"@swc/core-win32-x64-msvc": "1.7.14"
},
"peerDependencies": {
"@swc/helpers": "*"
@ -529,9 +529,9 @@
}
},
"node_modules/@swc/core-darwin-arm64": {
"version": "1.7.18",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.18.tgz",
"integrity": "sha512-MwLc5U+VGPMZm8MjlFBjEB2wyT1EK0NNJ3tn+ps9fmxdFP+PL8EpMiY1O1F2t1ydy2OzBtZz81sycjM9RieFBg==",
"version": "1.7.14",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.14.tgz",
"integrity": "sha512-V0OUXjOH+hdGxDYG8NkQzy25mKOpcNKFpqtZEzLe5V/CpLJPnpg1+pMz70m14s9ZFda9OxsjlvPbg1FLUwhgIQ==",
"cpu": [
"arm64"
],
@ -545,9 +545,9 @@
}
},
"node_modules/@swc/core-darwin-x64": {
"version": "1.7.18",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.18.tgz",
"integrity": "sha512-IkukOQUw7/14VkHp446OkYGCZEHqZg9pTmTdBawlUyz2JwZMSn2VodCl7aFSdGCsU4Cwni8zKA8CCgkCCAELhw==",
"version": "1.7.14",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.14.tgz",
"integrity": "sha512-9iFvUnxG6FC3An5ogp5jbBfQuUmTTwy8KMB+ZddUoPB3NR1eV+Y9vOh/tfWcenSJbgOKDLgYC5D/b1mHAprsrQ==",
"cpu": [
"x64"
],
@ -561,9 +561,9 @@
}
},
"node_modules/@swc/core-linux-arm-gnueabihf": {
"version": "1.7.18",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.18.tgz",
"integrity": "sha512-ATnb6jJaBeXCqrTUawWdoOy7eP9SCI7UMcfXlYIMxX4otKKspLPAEuGA5RaNxlCcj9ObyO0J3YGbtZ6hhD2pjg==",
"version": "1.7.14",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.14.tgz",
"integrity": "sha512-zGJsef9qPivKSH8Vv4F/HiBXBTHZ5Hs3ZjVGo/UIdWPJF8fTL9OVADiRrl34Q7zOZEtGXRwEKLUW1SCQcbDvZA==",
"cpu": [
"arm"
],
@ -577,9 +577,9 @@
}
},
"node_modules/@swc/core-linux-arm64-gnu": {
"version": "1.7.18",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.18.tgz",
"integrity": "sha512-poHtH7zL7lEp9K2inY90lGHJABWxURAOgWNeZqrcR5+jwIe7q5KBisysH09Zf/JNF9+6iNns+U0xgWTNJzBuGA==",
"version": "1.7.14",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.14.tgz",
"integrity": "sha512-AxV3MPsoI7i4B8FXOew3dx3N8y00YoJYvIPfxelw07RegeCEH3aHp2U2DtgbP/NV1ugZMx0TL2Z2DEvocmA51g==",
"cpu": [
"arm64"
],
@ -593,9 +593,9 @@
}
},
"node_modules/@swc/core-linux-arm64-musl": {
"version": "1.7.18",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.18.tgz",
"integrity": "sha512-qnNI1WmcOV7Wz1ZDyK6WrOlzLvJ01rnni8ec950mMHWkLRMP53QvCvhF3S+7gFplWBwWJTOOPPUqJp/PlSxWyQ==",
"version": "1.7.14",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.14.tgz",
"integrity": "sha512-JDLdNjUj3zPehd4+DrQD8Ltb3B5lD8D05IwePyDWw+uR/YPc7w/TX1FUVci5h3giJnlMCJRvi1IQYV7K1n7KtQ==",
"cpu": [
"arm64"
],
@ -609,9 +609,9 @@
}
},
"node_modules/@swc/core-linux-x64-gnu": {
"version": "1.7.18",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.18.tgz",
"integrity": "sha512-x9SCqCLzwtlqtD5At3I1a7Gco+EuXnzrJGoucmkpeQohshHuwa+cskqsXO6u1Dz0jXJEuHbBZB9va1wYYfjgFg==",
"version": "1.7.14",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.14.tgz",
"integrity": "sha512-Siy5OvPCLLWmMdx4msnEs8HvEVUEigSn0+3pbLjv78iwzXd0qSBNHUPZyC1xeurVaUbpNDxZTpPRIwpqNE2+Og==",
"cpu": [
"x64"
],
@ -625,9 +625,9 @@
}
},
"node_modules/@swc/core-linux-x64-musl": {
"version": "1.7.18",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.18.tgz",
"integrity": "sha512-qtj8iOpMMgKjzxTv+islmEY0JBsbd93nka0gzcTTmGZxKtL5jSUsYQvkxwNPZr5M9NU1fgaR3n1vE6lFmtY0IQ==",
"version": "1.7.14",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.14.tgz",
"integrity": "sha512-FtEGm9mwtRYQNK43WMtUIadxHs/ja2rnDurB99os0ZoFTGG2IHuht2zD97W0wB8JbqEabT1XwSG9Y5wmN+ciEQ==",
"cpu": [
"x64"
],
@ -641,9 +641,9 @@
}
},
"node_modules/@swc/core-win32-arm64-msvc": {
"version": "1.7.18",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.18.tgz",
"integrity": "sha512-ltX/Ol9+Qu4SXmISCeuwVgAjSa8nzHTymknpozzVMgjXUoZMoz6lcynfKL1nCh5XLgqh0XNHUKLti5YFF8LrrA==",
"version": "1.7.14",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.14.tgz",
"integrity": "sha512-Jp8KDlfq7Ntt2/BXr0y344cYgB1zf0DaLzDZ1ZJR6rYlAzWYSccLYcxHa97VGnsYhhPspMpmCvHid97oe2hl4A==",
"cpu": [
"arm64"
],
@ -657,9 +657,9 @@
}
},
"node_modules/@swc/core-win32-ia32-msvc": {
"version": "1.7.18",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.18.tgz",
"integrity": "sha512-RgTcFP3wgyxnQbTCJrlgBJmgpeTXo8t807GU9GxApAXfpLZJ3swJ2GgFUmIJVdLWyffSHF5BEkF3FmF6mtH5AQ==",
"version": "1.7.14",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.14.tgz",
"integrity": "sha512-I+cFsXF0OU0J9J4zdWiQKKLURO5dvCujH9Jr8N0cErdy54l9d4gfIxdctfTF+7FyXtWKLTCkp+oby9BQhkFGWA==",
"cpu": [
"ia32"
],
@ -673,9 +673,9 @@
}
},
"node_modules/@swc/core-win32-x64-msvc": {
"version": "1.7.18",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.18.tgz",
"integrity": "sha512-XbZ0wAgzR757+DhQcnv60Y/bK9yuWPhDNRQVFFQVRsowvK3+c6EblyfUSytIidpXgyYFzlprq/9A9ZlO/wvDWw==",
"version": "1.7.14",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.14.tgz",
"integrity": "sha512-NNrprQCK6d28mG436jVo2TD+vACHseUECacEBGZ9Ef0qfOIWS1XIt2MisQKG0Oea2VvLFl6tF/V4Lnx/H0Sn3Q==",
"cpu": [
"x64"
],

View File

@ -4,7 +4,7 @@
"private": true,
"license": "MIT",
"dependencies": {
"@goauthentik/api": "^2024.6.3-1724414734",
"@goauthentik/api": "^2024.6.3-1724337552",
"base64-js": "^1.5.1",
"bootstrap": "^4.6.1",
"formdata-polyfill": "^4.0.10",
@ -20,7 +20,7 @@
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-swc": "^0.3.1",
"@swc/cli": "^0.4.0",
"@swc/core": "^1.7.18",
"@swc/core": "^1.7.14",
"@types/jquery": "^3.5.30",
"rollup": "^4.21.0",
"rollup-plugin-copy": "^3.5.0"

View File

@ -21,6 +21,7 @@ import {
SourcesApi,
Stage,
StagesApi,
StagesCaptchaListRequest,
StagesPasswordListRequest,
UserFieldsEnum,
} from "@goauthentik/api";
@ -160,6 +161,37 @@ export class IdentificationStageForm extends BaseStageForm<IdentificationStage>
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Captcha stage")} name="captchaStage">
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Stage[]> => {
const args: StagesCaptchaListRequest = {
ordering: "name",
};
if (query !== undefined) {
args.search = query;
}
const stages = await new StagesApi(
DEFAULT_CONFIG,
).stagesCaptchaList(args);
return stages.results;
}}
.groupBy=${(items: Stage[]) => {
return groupBy(items, (stage) => stage.verboseNamePlural);
}}
.renderElement=${(stage: Stage): string => {
return stage.name;
}}
.value=${(stage: Stage | undefined): string | undefined => {
return stage?.pk;
}}
.selected=${(stage: Stage): boolean => {
return stage.pk === this.instance?.captchaStage;
}}
?blankable=${true}
>
</ak-search-select>
<p class="pf-c-form__helper-text">${msg("TODO.")}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="caseInsensitiveMatching">
<label class="pf-c-switch">
<input

View File

@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
export const ERROR_CLASS = "pf-m-danger";
export const PROGRESS_CLASS = "pf-m-in-progress";
export const CURRENT_CLASS = "pf-m-current";
export const VERSION = "2024.8.0";
export const VERSION = "2024.6.4";
export const TITLE_DEFAULT = "authentik";
export const ROUTE_SEPARATOR = ";";

View File

@ -41,7 +41,7 @@ export class Diagram extends AKElement {
// The type definition for this says number
// but the example use strings
// and numbers don't work
logLevel: "fatal",
logLevel: "fatal" as unknown as number,
startOnLoad: false,
flowchart: {
curve: "linear",

View File

@ -5,7 +5,6 @@ import { BaseStage } from "@goauthentik/flow/stages/base";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, html, nothing } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
@ -33,17 +32,7 @@ export class AccessDeniedStage extends BaseStage<
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form">
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}
>
<div slot="link">
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
>${msg("Not you?")}</a
>
</div>
</ak-form-static>
${this.renderUserInfo()}
<ak-empty-state icon="fa-times" header=${msg("Request has been denied.")}>
${this.challenge.errorMessage
? html`

View File

@ -7,7 +7,6 @@ import { BaseStage } from "@goauthentik/flow/stages/base";
import { msg } from "@lit/localize";
import { CSSResult, PropertyValues, TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
@ -77,17 +76,7 @@ export class AuthenticatorDuoStage extends BaseStage<
this.submitForm(e);
}}
>
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}
>
<div slot="link">
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
>${msg("Not you?")}</a
>
</div>
</ak-form-static>
${this.renderUserInfo()}
<img
src=${this.challenge.activationBarcode}
alt=${msg("Duo activation QR code")}

View File

@ -41,17 +41,7 @@ export class AuthenticatorSMSStage extends BaseStage<
this.submitForm(e);
}}
>
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}
>
<div slot="link">
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
>${msg("Not you?")}</a
>
</div>
</ak-form-static>
${this.renderUserInfo()}
<ak-form-element
label="${msg("Phone number")}"
required

View File

@ -6,7 +6,6 @@ import { BaseStage } from "@goauthentik/flow/stages/base";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
@ -66,17 +65,7 @@ export class AuthenticatorStaticStage extends BaseStage<
this.submitForm(e);
}}
>
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}
>
<div slot="link">
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
>${msg("Not you?")}</a
>
</div>
</ak-form-static>
${this.renderUserInfo()}
<ak-form-element label="" class="pf-c-form__group">
<ul>
${this.challenge.codes.map((token) => {

View File

@ -9,7 +9,6 @@ import "webcomponent-qr-code";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
@ -60,17 +59,7 @@ export class AuthenticatorTOTPStage extends BaseStage<
this.submitForm(e);
}}
>
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}
>
<div slot="link">
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
>${msg("Not you?")}</a
>
</div>
</ak-form-static>
${this.renderUserInfo()}
<input type="hidden" name="otp_uri" value=${this.challenge.configUrl} />
<ak-form-element>
<div class="qr-container">

View File

@ -10,7 +10,6 @@ import { BaseStage } from "@goauthentik/flow/stages/base";
import { msg, str } from "@lit/localize";
import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
@ -133,17 +132,7 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form">
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}
>
<div slot="link">
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
>${msg("Not you?")}</a
>
</div>
</ak-form-static>
${this.renderUserInfo()}
<ak-empty-state
?loading="${this.registerRunning}"
header=${this.registerRunning

View File

@ -7,8 +7,7 @@ import type { TurnstileObject } from "turnstile-types";
import { msg } from "@lit/localize";
import { CSSResult, PropertyValues, TemplateResult, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { customElement, property, state } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
@ -45,6 +44,9 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
@state()
scriptElement?: HTMLScriptElement;
@property({ type: Boolean })
embedded = false;
constructor() {
super();
this.captchaContainer = document.createElement("div");
@ -161,6 +163,9 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
}
render(): TemplateResult {
if (this.embedded) {
return this.renderBody();
}
if (!this.challenge) {
return html`<ak-empty-state loading> </ak-empty-state>`;
}
@ -168,18 +173,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form">
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}
>
<div slot="link">
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
>${msg("Not you?")}</a
>
</div>
</ak-form-static>
${this.renderUserInfo()}
${this.renderBody()}
</form>
</div>

View File

@ -5,7 +5,6 @@ import { BaseStage } from "@goauthentik/flow/stages/base";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, html, nothing } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
@ -109,17 +108,7 @@ export class ConsentStage extends BaseStage<ConsentChallenge, ConsentChallengeRe
});
}}
>
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}
>
<div slot="link">
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
>${msg("Not you?")}</a
>
</div>
</ak-form-static>
${this.renderUserInfo()}
${this.challenge.additionalPermissions.length > 0
? this.renderAdditional()
: this.renderNoPrevious()}

View File

@ -4,6 +4,7 @@ import "@goauthentik/elements/EmptyState";
import "@goauthentik/elements/forms/FormElement";
import "@goauthentik/flow/components/ak-flow-password-input.js";
import { BaseStage } from "@goauthentik/flow/stages/base";
import "@goauthentik/flow/stages/captcha/CaptchaStage";
import { msg, str } from "@lit/localize";
import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit";
@ -123,7 +124,7 @@ export class IdentificationStage extends BaseStage<
this.form.appendChild(username);
}
// Only add the password field when we don't already show a password field
if (!compatMode && !this.challenge.passwordFields) {
if (!compatMode && !this.challenge.passwordStage) {
const password = document.createElement("input");
password.setAttribute("type", "password");
password.setAttribute("name", "password");
@ -260,7 +261,7 @@ export class IdentificationStage extends BaseStage<
required
/>
</ak-form-element>
${this.challenge.passwordFields
${this.challenge.passwordStage
? html`
<ak-flow-input-password
label=${msg("Password")}
@ -268,12 +269,20 @@ export class IdentificationStage extends BaseStage<
required
class="pf-c-form__group"
.errors=${(this.challenge?.responseErrors || {})["password"]}
?allow-show-password=${this.challenge.allowShowPassword}
?allow-show-password=${this.challenge.passwordStage.allowShowPassword}
prefill=${PasswordManagerPrefill["password"] ?? ""}
></ak-flow-input-password>
`
: nothing}
${this.renderNonFieldErrors()}
${this.challenge.captchaStage
? html`
<ak-stage-captcha
.challenge=${this.challenge.captchaStage}
embedded
></ak-stage-captcha>
`
: nothing}
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
${this.challenge.primaryAction}
@ -284,6 +293,13 @@ export class IdentificationStage extends BaseStage<
: nothing}`;
}
submitForm(
e: Event,
defaults?: IdentificationChallengeResponseRequest | undefined,
): Promise<boolean> {
return super.submitForm(e, defaults);
}
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state loading> </ak-empty-state>`;

View File

@ -8,7 +8,6 @@ import { PasswordManagerPrefill } from "@goauthentik/flow/stages/identification/
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
@ -45,17 +44,7 @@ export class PasswordStage extends BaseStage<PasswordChallenge, PasswordChalleng
this.submitForm(e);
}}
>
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}
>
<div slot="link">
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
>${msg("Not you?")}</a
>
</div>
</ak-form-static>
${this.renderUserInfo()}
<input
name="username"
autocomplete="username"

View File

@ -6,7 +6,6 @@ import { BaseStage } from "@goauthentik/flow/stages/base";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
@ -36,17 +35,7 @@ export class PasswordStage extends BaseStage<
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form">
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}
>
<div slot="link">
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
>${msg("Not you?")}</a
>
</div>
</ak-form-static>
${this.renderUserInfo()}
<div class="pf-c-form__group">
<h3 id="header-text" class="pf-c-title pf-m-xl pf-u-mb-xl">
${msg("Stay signed in?")}

View File

@ -6860,36 +6860,6 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="sa65f772cfc5aa67e">
<source>Selected Transports</source>
</trans-unit>
<trans-unit id="sd923f95605fed7b2">
<source>Expired</source>
</trans-unit>
<trans-unit id="s86959994f28077dc">
<source>Expiring soon</source>
</trans-unit>
<trans-unit id="sc60a5fba70bf5a53">
<source>Unlicensed</source>
</trans-unit>
<trans-unit id="s4f9880ce82953741">
<source>Read Only</source>
</trans-unit>
<trans-unit id="s4220dda46d622211">
<source>Valid</source>
</trans-unit>
<trans-unit id="sdc94711a2eb66a45">
<source>Current license status</source>
</trans-unit>
<trans-unit id="se10fb73c1f1039c5">
<source>Overall license status</source>
</trans-unit>
<trans-unit id="sc4804358f202968c">
<source>Internal user usage</source>
</trans-unit>
<trans-unit id="s087d6f07b52b30ec">
<source><x id="0" equiv-text="${internalUserPercentage &lt; Infinity ? internalUserPercentage : &quot;∞&quot;}"/>%</source>
</trans-unit>
<trans-unit id="sae72e1569d34fb02">
<source>External user usage</source>
</trans-unit>
</body>
</file>

View File

@ -7125,36 +7125,6 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="sa65f772cfc5aa67e">
<source>Selected Transports</source>
</trans-unit>
<trans-unit id="sd923f95605fed7b2">
<source>Expired</source>
</trans-unit>
<trans-unit id="s86959994f28077dc">
<source>Expiring soon</source>
</trans-unit>
<trans-unit id="sc60a5fba70bf5a53">
<source>Unlicensed</source>
</trans-unit>
<trans-unit id="s4f9880ce82953741">
<source>Read Only</source>
</trans-unit>
<trans-unit id="s4220dda46d622211">
<source>Valid</source>
</trans-unit>
<trans-unit id="sdc94711a2eb66a45">
<source>Current license status</source>
</trans-unit>
<trans-unit id="se10fb73c1f1039c5">
<source>Overall license status</source>
</trans-unit>
<trans-unit id="sc4804358f202968c">
<source>Internal user usage</source>
</trans-unit>
<trans-unit id="s087d6f07b52b30ec">
<source><x id="0" equiv-text="${internalUserPercentage &lt; Infinity ? internalUserPercentage : &quot;∞&quot;}"/>%</source>
</trans-unit>
<trans-unit id="sae72e1569d34fb02">
<source>External user usage</source>
</trans-unit>
</body>
</file>

View File

@ -6777,36 +6777,6 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="sa65f772cfc5aa67e">
<source>Selected Transports</source>
</trans-unit>
<trans-unit id="sd923f95605fed7b2">
<source>Expired</source>
</trans-unit>
<trans-unit id="s86959994f28077dc">
<source>Expiring soon</source>
</trans-unit>
<trans-unit id="sc60a5fba70bf5a53">
<source>Unlicensed</source>
</trans-unit>
<trans-unit id="s4f9880ce82953741">
<source>Read Only</source>
</trans-unit>
<trans-unit id="s4220dda46d622211">
<source>Valid</source>
</trans-unit>
<trans-unit id="sdc94711a2eb66a45">
<source>Current license status</source>
</trans-unit>
<trans-unit id="se10fb73c1f1039c5">
<source>Overall license status</source>
</trans-unit>
<trans-unit id="sc4804358f202968c">
<source>Internal user usage</source>
</trans-unit>
<trans-unit id="s087d6f07b52b30ec">
<source><x id="0" equiv-text="${internalUserPercentage &lt; Infinity ? internalUserPercentage : &quot;∞&quot;}"/>%</source>
</trans-unit>
<trans-unit id="sae72e1569d34fb02">
<source>External user usage</source>
</trans-unit>
</body>
</file>

View File

@ -9022,36 +9022,6 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
</trans-unit>
<trans-unit id="sa65f772cfc5aa67e">
<source>Selected Transports</source>
</trans-unit>
<trans-unit id="sd923f95605fed7b2">
<source>Expired</source>
</trans-unit>
<trans-unit id="s86959994f28077dc">
<source>Expiring soon</source>
</trans-unit>
<trans-unit id="sc60a5fba70bf5a53">
<source>Unlicensed</source>
</trans-unit>
<trans-unit id="s4f9880ce82953741">
<source>Read Only</source>
</trans-unit>
<trans-unit id="s4220dda46d622211">
<source>Valid</source>
</trans-unit>
<trans-unit id="sdc94711a2eb66a45">
<source>Current license status</source>
</trans-unit>
<trans-unit id="se10fb73c1f1039c5">
<source>Overall license status</source>
</trans-unit>
<trans-unit id="sc4804358f202968c">
<source>Internal user usage</source>
</trans-unit>
<trans-unit id="s087d6f07b52b30ec">
<source><x id="0" equiv-text="${internalUserPercentage &lt; Infinity ? internalUserPercentage : &quot;∞&quot;}"/>%</source>
</trans-unit>
<trans-unit id="sae72e1569d34fb02">
<source>External user usage</source>
</trans-unit>
</body>
</file>

View File

@ -8696,36 +8696,6 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="sa65f772cfc5aa67e">
<source>Selected Transports</source>
</trans-unit>
<trans-unit id="sd923f95605fed7b2">
<source>Expired</source>
</trans-unit>
<trans-unit id="s86959994f28077dc">
<source>Expiring soon</source>
</trans-unit>
<trans-unit id="sc60a5fba70bf5a53">
<source>Unlicensed</source>
</trans-unit>
<trans-unit id="s4f9880ce82953741">
<source>Read Only</source>
</trans-unit>
<trans-unit id="s4220dda46d622211">
<source>Valid</source>
</trans-unit>
<trans-unit id="sdc94711a2eb66a45">
<source>Current license status</source>
</trans-unit>
<trans-unit id="se10fb73c1f1039c5">
<source>Overall license status</source>
</trans-unit>
<trans-unit id="sc4804358f202968c">
<source>Internal user usage</source>
</trans-unit>
<trans-unit id="s087d6f07b52b30ec">
<source><x id="0" equiv-text="${internalUserPercentage &lt; Infinity ? internalUserPercentage : &quot;∞&quot;}"/>%</source>
</trans-unit>
<trans-unit id="sae72e1569d34fb02">
<source>External user usage</source>
</trans-unit>
</body>
</file>

View File

@ -8541,36 +8541,6 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
</trans-unit>
<trans-unit id="sa65f772cfc5aa67e">
<source>Selected Transports</source>
</trans-unit>
<trans-unit id="sd923f95605fed7b2">
<source>Expired</source>
</trans-unit>
<trans-unit id="s86959994f28077dc">
<source>Expiring soon</source>
</trans-unit>
<trans-unit id="sc60a5fba70bf5a53">
<source>Unlicensed</source>
</trans-unit>
<trans-unit id="s4f9880ce82953741">
<source>Read Only</source>
</trans-unit>
<trans-unit id="s4220dda46d622211">
<source>Valid</source>
</trans-unit>
<trans-unit id="sdc94711a2eb66a45">
<source>Current license status</source>
</trans-unit>
<trans-unit id="se10fb73c1f1039c5">
<source>Overall license status</source>
</trans-unit>
<trans-unit id="sc4804358f202968c">
<source>Internal user usage</source>
</trans-unit>
<trans-unit id="s087d6f07b52b30ec">
<source><x id="0" equiv-text="${internalUserPercentage &lt; Infinity ? internalUserPercentage : &quot;∞&quot;}"/>%</source>
</trans-unit>
<trans-unit id="sae72e1569d34fb02">
<source>External user usage</source>
</trans-unit>
</body>
</file>

View File

@ -8961,36 +8961,6 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
</trans-unit>
<trans-unit id="sa65f772cfc5aa67e">
<source>Selected Transports</source>
</trans-unit>
<trans-unit id="sd923f95605fed7b2">
<source>Expired</source>
</trans-unit>
<trans-unit id="s86959994f28077dc">
<source>Expiring soon</source>
</trans-unit>
<trans-unit id="sc60a5fba70bf5a53">
<source>Unlicensed</source>
</trans-unit>
<trans-unit id="s4f9880ce82953741">
<source>Read Only</source>
</trans-unit>
<trans-unit id="s4220dda46d622211">
<source>Valid</source>
</trans-unit>
<trans-unit id="sdc94711a2eb66a45">
<source>Current license status</source>
</trans-unit>
<trans-unit id="se10fb73c1f1039c5">
<source>Overall license status</source>
</trans-unit>
<trans-unit id="sc4804358f202968c">
<source>Internal user usage</source>
</trans-unit>
<trans-unit id="s087d6f07b52b30ec">
<source><x id="0" equiv-text="${internalUserPercentage &lt; Infinity ? internalUserPercentage : &quot;∞&quot;}"/>%</source>
</trans-unit>
<trans-unit id="sae72e1569d34fb02">
<source>External user usage</source>
</trans-unit>
</body>
</file>

View File

@ -8924,34 +8924,4 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="sa65f772cfc5aa67e">
<source>Selected Transports</source>
</trans-unit>
<trans-unit id="sd923f95605fed7b2">
<source>Expired</source>
</trans-unit>
<trans-unit id="s86959994f28077dc">
<source>Expiring soon</source>
</trans-unit>
<trans-unit id="sc60a5fba70bf5a53">
<source>Unlicensed</source>
</trans-unit>
<trans-unit id="s4f9880ce82953741">
<source>Read Only</source>
</trans-unit>
<trans-unit id="s4220dda46d622211">
<source>Valid</source>
</trans-unit>
<trans-unit id="sdc94711a2eb66a45">
<source>Current license status</source>
</trans-unit>
<trans-unit id="se10fb73c1f1039c5">
<source>Overall license status</source>
</trans-unit>
<trans-unit id="sc4804358f202968c">
<source>Internal user usage</source>
</trans-unit>
<trans-unit id="s087d6f07b52b30ec">
<source><x id="0" equiv-text="${internalUserPercentage &lt; Infinity ? internalUserPercentage : &quot;∞&quot;}"/>%</source>
</trans-unit>
<trans-unit id="sae72e1569d34fb02">
<source>External user usage</source>
</trans-unit>
</body></file></xliff>

View File

@ -9025,36 +9025,6 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="sa65f772cfc5aa67e">
<source>Selected Transports</source>
</trans-unit>
<trans-unit id="sd923f95605fed7b2">
<source>Expired</source>
</trans-unit>
<trans-unit id="s86959994f28077dc">
<source>Expiring soon</source>
</trans-unit>
<trans-unit id="sc60a5fba70bf5a53">
<source>Unlicensed</source>
</trans-unit>
<trans-unit id="s4f9880ce82953741">
<source>Read Only</source>
</trans-unit>
<trans-unit id="s4220dda46d622211">
<source>Valid</source>
</trans-unit>
<trans-unit id="sdc94711a2eb66a45">
<source>Current license status</source>
</trans-unit>
<trans-unit id="se10fb73c1f1039c5">
<source>Overall license status</source>
</trans-unit>
<trans-unit id="sc4804358f202968c">
<source>Internal user usage</source>
</trans-unit>
<trans-unit id="s087d6f07b52b30ec">
<source><x id="0" equiv-text="${internalUserPercentage &lt; Infinity ? internalUserPercentage : &quot;∞&quot;}"/>%</source>
</trans-unit>
<trans-unit id="sae72e1569d34fb02">
<source>External user usage</source>
</trans-unit>
</body>
</file>

View File

@ -6770,36 +6770,6 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="sa65f772cfc5aa67e">
<source>Selected Transports</source>
</trans-unit>
<trans-unit id="sd923f95605fed7b2">
<source>Expired</source>
</trans-unit>
<trans-unit id="s86959994f28077dc">
<source>Expiring soon</source>
</trans-unit>
<trans-unit id="sc60a5fba70bf5a53">
<source>Unlicensed</source>
</trans-unit>
<trans-unit id="s4f9880ce82953741">
<source>Read Only</source>
</trans-unit>
<trans-unit id="s4220dda46d622211">
<source>Valid</source>
</trans-unit>
<trans-unit id="sdc94711a2eb66a45">
<source>Current license status</source>
</trans-unit>
<trans-unit id="se10fb73c1f1039c5">
<source>Overall license status</source>
</trans-unit>
<trans-unit id="sc4804358f202968c">
<source>Internal user usage</source>
</trans-unit>
<trans-unit id="s087d6f07b52b30ec">
<source><x id="0" equiv-text="${internalUserPercentage &lt; Infinity ? internalUserPercentage : &quot;∞&quot;}"/>%</source>
</trans-unit>
<trans-unit id="sae72e1569d34fb02">
<source>External user usage</source>
</trans-unit>
</body>
</file>

View File

@ -5707,36 +5707,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="sa65f772cfc5aa67e">
<source>Selected Transports</source>
</trans-unit>
<trans-unit id="sd923f95605fed7b2">
<source>Expired</source>
</trans-unit>
<trans-unit id="s86959994f28077dc">
<source>Expiring soon</source>
</trans-unit>
<trans-unit id="sc60a5fba70bf5a53">
<source>Unlicensed</source>
</trans-unit>
<trans-unit id="s4f9880ce82953741">
<source>Read Only</source>
</trans-unit>
<trans-unit id="s4220dda46d622211">
<source>Valid</source>
</trans-unit>
<trans-unit id="sdc94711a2eb66a45">
<source>Current license status</source>
</trans-unit>
<trans-unit id="se10fb73c1f1039c5">
<source>Overall license status</source>
</trans-unit>
<trans-unit id="sc4804358f202968c">
<source>Internal user usage</source>
</trans-unit>
<trans-unit id="s087d6f07b52b30ec">
<source><x id="0" equiv-text="${internalUserPercentage &lt; Infinity ? internalUserPercentage : &quot;∞&quot;}"/>%</source>
</trans-unit>
<trans-unit id="sae72e1569d34fb02">
<source>External user usage</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@ -9028,36 +9028,6 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="sa65f772cfc5aa67e">
<source>Selected Transports</source>
</trans-unit>
<trans-unit id="sd923f95605fed7b2">
<source>Expired</source>
</trans-unit>
<trans-unit id="s86959994f28077dc">
<source>Expiring soon</source>
</trans-unit>
<trans-unit id="sc60a5fba70bf5a53">
<source>Unlicensed</source>
</trans-unit>
<trans-unit id="s4f9880ce82953741">
<source>Read Only</source>
</trans-unit>
<trans-unit id="s4220dda46d622211">
<source>Valid</source>
</trans-unit>
<trans-unit id="sdc94711a2eb66a45">
<source>Current license status</source>
</trans-unit>
<trans-unit id="se10fb73c1f1039c5">
<source>Overall license status</source>
</trans-unit>
<trans-unit id="sc4804358f202968c">
<source>Internal user usage</source>
</trans-unit>
<trans-unit id="s087d6f07b52b30ec">
<source><x id="0" equiv-text="${internalUserPercentage &lt; Infinity ? internalUserPercentage : &quot;∞&quot;}"/>%</source>
</trans-unit>
<trans-unit id="sae72e1569d34fb02">
<source>External user usage</source>
</trans-unit>
</body>
</file>

View File

@ -6818,36 +6818,6 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="sa65f772cfc5aa67e">
<source>Selected Transports</source>
</trans-unit>
<trans-unit id="sd923f95605fed7b2">
<source>Expired</source>
</trans-unit>
<trans-unit id="s86959994f28077dc">
<source>Expiring soon</source>
</trans-unit>
<trans-unit id="sc60a5fba70bf5a53">
<source>Unlicensed</source>
</trans-unit>
<trans-unit id="s4f9880ce82953741">
<source>Read Only</source>
</trans-unit>
<trans-unit id="s4220dda46d622211">
<source>Valid</source>
</trans-unit>
<trans-unit id="sdc94711a2eb66a45">
<source>Current license status</source>
</trans-unit>
<trans-unit id="se10fb73c1f1039c5">
<source>Overall license status</source>
</trans-unit>
<trans-unit id="sc4804358f202968c">
<source>Internal user usage</source>
</trans-unit>
<trans-unit id="s087d6f07b52b30ec">
<source><x id="0" equiv-text="${internalUserPercentage &lt; Infinity ? internalUserPercentage : &quot;∞&quot;}"/>%</source>
</trans-unit>
<trans-unit id="sae72e1569d34fb02">
<source>External user usage</source>
</trans-unit>
</body>
</file>

View File

@ -8657,36 +8657,6 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="sa65f772cfc5aa67e">
<source>Selected Transports</source>
</trans-unit>
<trans-unit id="sd923f95605fed7b2">
<source>Expired</source>
</trans-unit>
<trans-unit id="s86959994f28077dc">
<source>Expiring soon</source>
</trans-unit>
<trans-unit id="sc60a5fba70bf5a53">
<source>Unlicensed</source>
</trans-unit>
<trans-unit id="s4f9880ce82953741">
<source>Read Only</source>
</trans-unit>
<trans-unit id="s4220dda46d622211">
<source>Valid</source>
</trans-unit>
<trans-unit id="sdc94711a2eb66a45">
<source>Current license status</source>
</trans-unit>
<trans-unit id="se10fb73c1f1039c5">
<source>Overall license status</source>
</trans-unit>
<trans-unit id="sc4804358f202968c">
<source>Internal user usage</source>
</trans-unit>
<trans-unit id="s087d6f07b52b30ec">
<source><x id="0" equiv-text="${internalUserPercentage &lt; Infinity ? internalUserPercentage : &quot;∞&quot;}"/>%</source>
</trans-unit>
<trans-unit id="sae72e1569d34fb02">
<source>External user usage</source>
</trans-unit>
</body>
</file>

View File

@ -341,7 +341,7 @@ def get_as_base64(url):
def get_avatar_from_avatar_url(url):
"""Returns an authentik-avatar-attributes-compatible string from an image url"""
cut_url = f"{url}"
cut_url = f"{url}?size=64"
return AVATAR_STREAM_CONTENT.format(
base64_string=(get_as_base64(cut_url).decode("utf-8"))
)

View File

@ -12322,11 +12322,10 @@
"license": "MIT"
},
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"version": "4.0.5",
"license": "MIT",
"dependencies": {
"braces": "^3.0.3",
"braces": "^3.0.2",
"picomatch": "^2.3.1"
},
"engines": {