Compare commits
23 Commits
stages/ide
...
version/20
Author | SHA1 | Date | |
---|---|---|---|
2fb097061d | |||
8962d17e03 | |||
8326e1490c | |||
091e4d3e4c | |||
6ee77edcbb | |||
763e2288bf | |||
9cdb177ca7 | |||
6070508058 | |||
ec13a5d84d | |||
057de82b01 | |||
4316fa9e5c | |||
8099a4a291 | |||
5d2d9c90ff | |||
befce18eda | |||
af3ace47b0 | |||
11e506bb94 | |||
5c6704d4e7 | |||
b29cb1d36d | |||
a87a111b8b | |||
e83a1c65f6 | |||
d8a74435f8 | |||
4e910446ed | |||
cfd8d7cf91 |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 2024.6.4
|
||||
current_version = 2024.8.0
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
|
||||
|
@ -29,9 +29,9 @@ outputs:
|
||||
imageTags:
|
||||
description: "Docker image tags"
|
||||
value: ${{ steps.ev.outputs.imageTags }}
|
||||
imageNames:
|
||||
description: "Docker image names"
|
||||
value: ${{ steps.ev.outputs.imageNames }}
|
||||
attestImageNames:
|
||||
description: "Docker image names used for attestation"
|
||||
value: ${{ steps.ev.outputs.attestImageNames }}
|
||||
imageMainTag:
|
||||
description: "Docker image main tag"
|
||||
value: ${{ steps.ev.outputs.imageMainTag }}
|
||||
|
@ -51,15 +51,24 @@ else:
|
||||
]
|
||||
|
||||
image_main_tag = image_tags[0].split(":")[-1]
|
||||
image_tags_rendered = ",".join(image_tags)
|
||||
image_names_rendered = ",".join(set(name.split(":")[0] for name in image_tags))
|
||||
|
||||
|
||||
def get_attest_image_names(image_with_tags: list[str]):
|
||||
"""Attestation only for GHCR"""
|
||||
image_tags = []
|
||||
for image_name in set(name.split(":")[0] for name in image_with_tags):
|
||||
if not image_name.startswith("ghcr.io"):
|
||||
continue
|
||||
image_tags.append(image_name)
|
||||
return ",".join(set(image_tags))
|
||||
|
||||
|
||||
with open(os.environ["GITHUB_OUTPUT"], "a+", encoding="utf-8") as _output:
|
||||
print(f"shouldBuild={should_build}", file=_output)
|
||||
print(f"sha={sha}", file=_output)
|
||||
print(f"version={version}", file=_output)
|
||||
print(f"prerelease={prerelease}", file=_output)
|
||||
print(f"imageTags={image_tags_rendered}", file=_output)
|
||||
print(f"imageNames={image_names_rendered}", file=_output)
|
||||
print(f"imageTags={','.join(image_tags)}", file=_output)
|
||||
print(f"attestImageNames={get_attest_image_names(image_tags)}", file=_output)
|
||||
print(f"imageMainTag={image_main_tag}", file=_output)
|
||||
print(f"imageMainName={image_tags[0]}", file=_output)
|
||||
|
2
.github/workflows/ci-main.yml
vendored
2
.github/workflows/ci-main.yml
vendored
@ -261,7 +261,7 @@ jobs:
|
||||
id: attest
|
||||
if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
||||
with:
|
||||
subject-name: ${{ steps.ev.outputs.imageNames }}
|
||||
subject-name: ${{ steps.ev.outputs.attestImageNames }}
|
||||
subject-digest: ${{ steps.push.outputs.digest }}
|
||||
push-to-registry: true
|
||||
pr-comment:
|
||||
|
2
.github/workflows/ci-outpost.yml
vendored
2
.github/workflows/ci-outpost.yml
vendored
@ -115,7 +115,7 @@ jobs:
|
||||
id: attest
|
||||
if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
||||
with:
|
||||
subject-name: ${{ steps.ev.outputs.imageNames }}
|
||||
subject-name: ${{ steps.ev.outputs.attestImageNames }}
|
||||
subject-digest: ${{ steps.push.outputs.digest }}
|
||||
push-to-registry: true
|
||||
build-binary:
|
||||
|
2
.github/workflows/ci-web.yml
vendored
2
.github/workflows/ci-web.yml
vendored
@ -92,4 +92,4 @@ jobs:
|
||||
run: make gen-client-ts
|
||||
- name: test
|
||||
working-directory: web/
|
||||
run: npm run test
|
||||
run: npm run test || exit 0
|
||||
|
4
.github/workflows/release-publish.yml
vendored
4
.github/workflows/release-publish.yml
vendored
@ -58,7 +58,7 @@ jobs:
|
||||
- uses: actions/attest-build-provenance@v1
|
||||
id: attest
|
||||
with:
|
||||
subject-name: ${{ steps.ev.outputs.imageNames }}
|
||||
subject-name: ${{ steps.ev.outputs.attestImageNames }}
|
||||
subject-digest: ${{ steps.push.outputs.digest }}
|
||||
push-to-registry: true
|
||||
build-outpost:
|
||||
@ -122,7 +122,7 @@ jobs:
|
||||
- uses: actions/attest-build-provenance@v1
|
||||
id: attest
|
||||
with:
|
||||
subject-name: ${{ steps.ev.outputs.imageNames }}
|
||||
subject-name: ${{ steps.ev.outputs.attestImageNames }}
|
||||
subject-digest: ${{ steps.push.outputs.digest }}
|
||||
push-to-registry: true
|
||||
build-outpost-binary:
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
from os import environ
|
||||
|
||||
__version__ = "2024.6.4"
|
||||
__version__ = "2024.8.0"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
|
||||
|
@ -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.current_stage, "message", "")
|
||||
level = getattr(self.current_stage, "level", messages.SUCCESS)
|
||||
message = getattr(self.executor.current_stage, "message", "")
|
||||
level = getattr(self.executor.current_stage, "level", messages.SUCCESS)
|
||||
messages.add_message(
|
||||
self.request,
|
||||
level,
|
||||
@ -486,7 +486,9 @@ 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.current_stage.group_connection_type
|
||||
self.group_connection_type: GroupSourceConnection = (
|
||||
self.executor.current_stage.group_connection_type
|
||||
)
|
||||
|
||||
raw_groups: dict[str, dict[str, Any | dict[str, Any]]] = self.executor.plan.context[
|
||||
PLAN_CONTEXT_SOURCE_GROUPS
|
||||
|
@ -25,4 +25,4 @@ class AuthentikEnterpriseConfig(EnterpriseConfig):
|
||||
"""Actual enterprise check, cached"""
|
||||
from authentik.enterprise.license import LicenseKey
|
||||
|
||||
return LicenseKey.cached_summary().status
|
||||
return LicenseKey.cached_summary().status.is_valid
|
||||
|
@ -117,7 +117,7 @@ class LicenseKey:
|
||||
our_cert.public_key(),
|
||||
algorithms=["ES512"],
|
||||
audience=get_license_aud(),
|
||||
options={"verify_exp": check_expiry},
|
||||
options={"verify_exp": check_expiry, "verify_signature": check_expiry},
|
||||
),
|
||||
)
|
||||
except PyJWTError:
|
||||
@ -134,7 +134,7 @@ class LicenseKey:
|
||||
exp_ts = int(mktime(lic.expiry.timetuple()))
|
||||
if total.exp == 0:
|
||||
total.exp = exp_ts
|
||||
total.exp = min(total.exp, exp_ts)
|
||||
total.exp = max(total.exp, exp_ts)
|
||||
total.license_flags.extend(lic.status.license_flags)
|
||||
return total
|
||||
|
||||
|
@ -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 RedirectStageChallengeView
|
||||
from authentik.flows.stage import RedirectStage
|
||||
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(RedirectStageChallengeView):
|
||||
class RACFinalStage(RedirectStage):
|
||||
"""RAC Connection final stage, set the connection token in the stage"""
|
||||
|
||||
endpoint: Endpoint
|
||||
@ -91,9 +91,9 @@ class RACFinalStage(RedirectStageChallengeView):
|
||||
application: Application
|
||||
|
||||
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
self.endpoint = self.current_stage.endpoint
|
||||
self.provider = self.current_stage.provider
|
||||
self.application = self.current_stage.application
|
||||
self.endpoint = self.executor.current_stage.endpoint
|
||||
self.provider = self.executor.current_stage.provider
|
||||
self.application = self.executor.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(RedirectStageChallengeView):
|
||||
flow=self.executor.plan.flow_pk,
|
||||
endpoint=self.endpoint.name,
|
||||
).from_http(self.request)
|
||||
self.current_stage.destination = self.request.build_absolute_uri(
|
||||
self.executor.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)
|
||||
|
@ -21,15 +21,16 @@ from authentik.lib.utils.time import timedelta_from_string
|
||||
PLAN_CONTEXT_RESUME_TOKEN = "resume_token" # nosec
|
||||
|
||||
|
||||
class SourceStageView(ChallengeStageView[SourceStage]):
|
||||
class SourceStageView(ChallengeStageView):
|
||||
"""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=self.current_stage.source_id).select_subclasses().first()
|
||||
Source.objects.filter(pk=current_stage.source_id).select_subclasses().first()
|
||||
)
|
||||
if not source:
|
||||
self.logger.warning("Source does not exist")
|
||||
@ -55,10 +56,11 @@ class SourceStageView(ChallengeStageView[SourceStage]):
|
||||
pending_user: User = self.get_pending_user()
|
||||
if pending_user.is_anonymous or not pending_user.pk:
|
||||
pending_user = get_anonymous_user()
|
||||
identifier = slugify(f"ak-source-stage-{self.current_stage.name}-{str(uuid4())}")
|
||||
current_stage: SourceStage = self.executor.current_stage
|
||||
identifier = slugify(f"ak-source-stage-{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(self.current_stage.resume_timeout)
|
||||
valid_delta = timedelta_from_string(current_stage.resume_timeout)
|
||||
if not tokens.exists():
|
||||
return FlowToken.objects.create(
|
||||
expires=now() + valid_delta,
|
||||
|
@ -74,9 +74,9 @@ class FlowPlan:
|
||||
|
||||
def redirect(self, destination: str):
|
||||
"""Insert a redirect stage as next stage"""
|
||||
from authentik.flows.stage import RedirectStageChallengeView
|
||||
from authentik.flows.stage import RedirectStage
|
||||
|
||||
self.insert_stage(in_memory_stage(RedirectStageChallengeView, destination=destination))
|
||||
self.insert_stage(in_memory_stage(RedirectStage, destination=destination))
|
||||
|
||||
def next(self, http_request: HttpRequest | None) -> FlowStageBinding | None:
|
||||
"""Return next pending stage from the bottom of the list"""
|
||||
|
@ -30,7 +30,6 @@ 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"
|
||||
@ -41,21 +40,20 @@ HIST_FLOWS_STAGE_TIME = Histogram(
|
||||
)
|
||||
|
||||
|
||||
class StageView[TStage: "Stage"](View):
|
||||
class StageView(View):
|
||||
"""Abstract Stage"""
|
||||
|
||||
executor: "FlowExecutorView"
|
||||
current_stage: TStage
|
||||
|
||||
request: HttpRequest = None
|
||||
|
||||
logger: BoundLogger
|
||||
|
||||
def __init__(self, executor: "FlowExecutorView", current_stage: TStage | None = None, **kwargs):
|
||||
def __init__(self, executor: "FlowExecutorView", **kwargs):
|
||||
self.executor = executor
|
||||
self.current_stage = current_stage or executor.current_stage
|
||||
current_stage = getattr(self.executor, "current_stage", None)
|
||||
self.logger = get_logger().bind(
|
||||
stage=getattr(self.current_stage, "name", None),
|
||||
stage=getattr(current_stage, "name", None),
|
||||
stage_view=class_to_path(type(self)),
|
||||
)
|
||||
super().__init__(**kwargs)
|
||||
@ -82,7 +80,7 @@ class StageView[TStage: "Stage"](View):
|
||||
"""Cleanup session"""
|
||||
|
||||
|
||||
class ChallengeStageView[TStage: "Stage"](StageView[TStage]):
|
||||
class ChallengeStageView(StageView):
|
||||
"""Stage view which response with a challenge"""
|
||||
|
||||
response_class = ChallengeResponse
|
||||
@ -255,12 +253,12 @@ class AccessDeniedChallengeView(ChallengeStageView):
|
||||
return self.executor.cancel()
|
||||
|
||||
|
||||
class RedirectStageChallengeView(ChallengeStageView):
|
||||
class RedirectStage(ChallengeStageView):
|
||||
"""Redirect to any URL"""
|
||||
|
||||
def get_challenge(self, *args, **kwargs) -> RedirectChallenge:
|
||||
destination = getattr(
|
||||
self.current_stage, "destination", reverse("authentik_core:root-redirect")
|
||||
self.executor.current_stage, "destination", reverse("authentik_core:root-redirect")
|
||||
)
|
||||
return RedirectChallenge(
|
||||
data={
|
||||
|
@ -433,20 +433,21 @@ class TokenParams:
|
||||
app = Application.objects.filter(provider=self.provider).first()
|
||||
if not app or not app.provider:
|
||||
raise TokenError("invalid_grant")
|
||||
self.user, _ = User.objects.update_or_create(
|
||||
# trim username to ensure the entire username is max 150 chars
|
||||
# (22 chars being the length of the "template")
|
||||
username=f"ak-{self.provider.name[:150-22]}-client_credentials",
|
||||
defaults={
|
||||
"attributes": {
|
||||
USER_ATTRIBUTE_GENERATED: True,
|
||||
with audit_ignore():
|
||||
self.user, _ = User.objects.update_or_create(
|
||||
# trim username to ensure the entire username is max 150 chars
|
||||
# (22 chars being the length of the "template")
|
||||
username=f"ak-{self.provider.name[:150-22]}-client_credentials",
|
||||
defaults={
|
||||
"attributes": {
|
||||
USER_ATTRIBUTE_GENERATED: True,
|
||||
},
|
||||
"last_login": timezone.now(),
|
||||
"name": f"Autogenerated user from application {app.name} (client credentials)",
|
||||
"path": f"{USER_PATH_SYSTEM_PREFIX}/apps/{app.slug}",
|
||||
"type": UserTypes.SERVICE_ACCOUNT,
|
||||
},
|
||||
"last_login": timezone.now(),
|
||||
"name": f"Autogenerated user from application {app.name} (client credentials)",
|
||||
"path": f"{USER_PATH_SYSTEM_PREFIX}/apps/{app.slug}",
|
||||
"type": UserTypes.SERVICE_ACCOUNT,
|
||||
},
|
||||
)
|
||||
)
|
||||
self.__check_policy_access(app, request)
|
||||
|
||||
Event.new(
|
||||
|
@ -54,7 +54,11 @@ class TestServiceProviderMetadataParser(TestCase):
|
||||
request = self.factory.get("/")
|
||||
metadata = lxml_from_string(MetadataProcessor(provider, request).build_entity_descriptor())
|
||||
|
||||
schema = etree.XMLSchema(etree.parse("schemas/saml-schema-metadata-2.0.xsd")) # nosec
|
||||
schema = etree.XMLSchema(
|
||||
etree.parse(
|
||||
source="schemas/saml-schema-metadata-2.0.xsd", parser=etree.XMLParser()
|
||||
) # nosec
|
||||
)
|
||||
self.assertTrue(schema.validate(metadata))
|
||||
|
||||
def test_schema_want_authn_requests_signed(self):
|
||||
|
@ -47,7 +47,9 @@ class TestSchema(TestCase):
|
||||
|
||||
metadata = lxml_from_string(request)
|
||||
|
||||
schema = etree.XMLSchema(etree.parse("schemas/saml-schema-protocol-2.0.xsd")) # nosec
|
||||
schema = etree.XMLSchema(
|
||||
etree.parse("schemas/saml-schema-protocol-2.0.xsd", parser=etree.XMLParser()) # nosec
|
||||
)
|
||||
self.assertTrue(schema.validate(metadata))
|
||||
|
||||
def test_response_schema(self):
|
||||
@ -68,5 +70,7 @@ class TestSchema(TestCase):
|
||||
|
||||
metadata = lxml_from_string(response)
|
||||
|
||||
schema = etree.XMLSchema(etree.parse("schemas/saml-schema-protocol-2.0.xsd")) # nosec
|
||||
schema = etree.XMLSchema(
|
||||
etree.parse("schemas/saml-schema-protocol-2.0.xsd", parser=etree.XMLParser()) # nosec
|
||||
)
|
||||
self.assertTrue(schema.validate(metadata))
|
||||
|
@ -30,7 +30,9 @@ 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")) # nosec
|
||||
schema = etree.XMLSchema(
|
||||
etree.parse("schemas/saml-schema-metadata-2.0.xsd", parser=etree.XMLParser()) # nosec
|
||||
)
|
||||
self.assertTrue(schema.validate(metadata))
|
||||
|
||||
def test_metadata_consistent(self):
|
||||
|
@ -32,7 +32,7 @@ class AuthenticatorDuoChallengeResponse(ChallengeResponse):
|
||||
component = CharField(default="ak-stage-authenticator-duo")
|
||||
|
||||
|
||||
class AuthenticatorDuoStageView(ChallengeStageView[AuthenticatorDuoStage]):
|
||||
class AuthenticatorDuoStageView(ChallengeStageView):
|
||||
"""Duo stage"""
|
||||
|
||||
response_class = AuthenticatorDuoChallengeResponse
|
||||
@ -40,8 +40,9 @@ class AuthenticatorDuoStageView(ChallengeStageView[AuthenticatorDuoStage]):
|
||||
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 = self.current_stage.auth_client().enroll(user.username)
|
||||
enroll = stage.auth_client().enroll(user.username)
|
||||
except RuntimeError as exc:
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
@ -53,6 +54,7 @@ class AuthenticatorDuoStageView(ChallengeStageView[AuthenticatorDuoStage]):
|
||||
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]
|
||||
@ -60,14 +62,15 @@ class AuthenticatorDuoStageView(ChallengeStageView[AuthenticatorDuoStage]):
|
||||
data={
|
||||
"activation_barcode": enroll["activation_barcode"],
|
||||
"activation_code": enroll["activation_code"],
|
||||
"stage_uuid": str(self.current_stage.stage_uuid),
|
||||
"stage_uuid": str(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 = self.current_stage.auth_client().enroll_status(
|
||||
enroll_status = stage.auth_client().enroll_status(
|
||||
enroll["user_id"], enroll["activation_code"]
|
||||
)
|
||||
if enroll_status != "success":
|
||||
@ -79,7 +82,7 @@ class AuthenticatorDuoStageView(ChallengeStageView[AuthenticatorDuoStage]):
|
||||
name="Duo Authenticator",
|
||||
user=self.get_pending_user(),
|
||||
duo_user_id=enroll["user_id"],
|
||||
stage=self.current_stage,
|
||||
stage=stage,
|
||||
last_t=now(),
|
||||
)
|
||||
else:
|
||||
|
@ -57,20 +57,21 @@ class AuthenticatorSMSChallengeResponse(ChallengeResponse):
|
||||
return super().validate(attrs)
|
||||
|
||||
|
||||
class AuthenticatorSMSStageView(ChallengeStageView[AuthenticatorSMSStage]):
|
||||
class AuthenticatorSMSStageView(ChallengeStageView):
|
||||
"""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=self.current_stage.pk).exists():
|
||||
if SMSDevice.objects.filter(query, stage=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]
|
||||
self.current_stage.send(device.token, device)
|
||||
stage.send(device.token, device)
|
||||
|
||||
def _has_phone_number(self) -> str | None:
|
||||
context = self.executor.plan.context
|
||||
@ -100,10 +101,10 @@ class AuthenticatorSMSStageView(ChallengeStageView[AuthenticatorSMSStage]):
|
||||
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=self.current_stage, name="SMS Device"
|
||||
)
|
||||
device = SMSDevice(user=user, confirmed=False, stage=stage, name="SMS Device")
|
||||
device.generate_token(commit=False)
|
||||
self.request.session[SESSION_KEY_SMS_DEVICE] = device
|
||||
if phone_number := self._has_phone_number():
|
||||
@ -129,7 +130,8 @@ class AuthenticatorSMSStageView(ChallengeStageView[AuthenticatorSMSStage]):
|
||||
device: SMSDevice = self.request.session[SESSION_KEY_SMS_DEVICE]
|
||||
if not device.confirmed:
|
||||
return self.challenge_invalid(response)
|
||||
if self.current_stage.verify_only:
|
||||
stage: AuthenticatorSMSStage = self.executor.current_stage
|
||||
if stage.verify_only:
|
||||
self.logger.debug("Hashing number on device")
|
||||
device.set_hashed_number()
|
||||
device.save()
|
||||
|
@ -29,7 +29,7 @@ class AuthenticatorStaticChallengeResponse(ChallengeResponse):
|
||||
component = CharField(default="ak-stage-authenticator-static")
|
||||
|
||||
|
||||
class AuthenticatorStaticStageView(ChallengeStageView[AuthenticatorStaticStage]):
|
||||
class AuthenticatorStaticStageView(ChallengeStageView):
|
||||
"""Static OTP Setup stage"""
|
||||
|
||||
response_class = AuthenticatorStaticChallengeResponse
|
||||
@ -48,14 +48,14 @@ class AuthenticatorStaticStageView(ChallengeStageView[AuthenticatorStaticStage])
|
||||
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, self.current_stage.token_count):
|
||||
for _ in range(0, stage.token_count):
|
||||
tokens.append(
|
||||
StaticToken(
|
||||
device=device, token=generate_id(length=self.current_stage.token_length)
|
||||
)
|
||||
StaticToken(device=device, token=generate_id(length=stage.token_length))
|
||||
)
|
||||
self.request.session[SESSION_STATIC_DEVICE] = device
|
||||
self.request.session[SESSION_STATIC_TOKENS] = tokens
|
||||
|
@ -45,7 +45,7 @@ class AuthenticatorTOTPChallengeResponse(ChallengeResponse):
|
||||
return code
|
||||
|
||||
|
||||
class AuthenticatorTOTPStageView(ChallengeStageView[AuthenticatorTOTPStage]):
|
||||
class AuthenticatorTOTPStageView(ChallengeStageView):
|
||||
"""OTP totp Setup stage"""
|
||||
|
||||
response_class = AuthenticatorTOTPChallengeResponse
|
||||
@ -71,12 +71,11 @@ class AuthenticatorTOTPStageView(ChallengeStageView[AuthenticatorTOTPStage]):
|
||||
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=self.current_stage.digits,
|
||||
name="TOTP Authenticator",
|
||||
user=user, confirmed=False, digits=stage.digits, name="TOTP Authenticator"
|
||||
)
|
||||
|
||||
self.request.session[SESSION_TOTP_DEVICE] = device
|
||||
|
@ -151,7 +151,7 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
|
||||
return attrs
|
||||
|
||||
|
||||
class AuthenticatorValidateStageView(ChallengeStageView[AuthenticatorValidateStage]):
|
||||
class AuthenticatorValidateStageView(ChallengeStageView):
|
||||
"""Authenticator Validation"""
|
||||
|
||||
response_class = AuthenticatorValidationChallengeResponse
|
||||
@ -177,14 +177,16 @@ class AuthenticatorValidateStageView(ChallengeStageView[AuthenticatorValidateSta
|
||||
# since their challenges are device-independent
|
||||
seen_classes = []
|
||||
|
||||
threshold = timedelta_from_string(self.current_stage.last_auth_threshold)
|
||||
stage: AuthenticatorValidateStage = self.executor.current_stage
|
||||
|
||||
threshold = timedelta_from_string(stage.last_auth_threshold)
|
||||
allowed_devices = []
|
||||
|
||||
has_webauthn_filters_set = self.current_stage.webauthn_allowed_device_types.exists()
|
||||
has_webauthn_filters_set = stage.webauthn_allowed_device_types.exists()
|
||||
|
||||
for device in user_devices:
|
||||
device_class = device.__class__.__name__.lower().replace("device", "")
|
||||
if device_class not in self.current_stage.device_classes:
|
||||
if device_class not in stage.device_classes:
|
||||
self.logger.debug("device class not allowed", device_class=device_class)
|
||||
continue
|
||||
if isinstance(device, SMSDevice) and device.is_hashed:
|
||||
@ -197,7 +199,7 @@ class AuthenticatorValidateStageView(ChallengeStageView[AuthenticatorValidateSta
|
||||
and device.device_type
|
||||
and has_webauthn_filters_set
|
||||
):
|
||||
if not self.current_stage.webauthn_allowed_device_types.filter(
|
||||
if not stage.webauthn_allowed_device_types.filter(
|
||||
pk=device.device_type.pk
|
||||
).exists():
|
||||
self.logger.debug(
|
||||
@ -214,7 +216,7 @@ class AuthenticatorValidateStageView(ChallengeStageView[AuthenticatorValidateSta
|
||||
data={
|
||||
"device_class": device_class,
|
||||
"device_uid": device.pk,
|
||||
"challenge": get_challenge_for_device(self.request, self.current_stage, device),
|
||||
"challenge": get_challenge_for_device(self.request, stage, device),
|
||||
}
|
||||
)
|
||||
challenge.is_valid()
|
||||
@ -233,7 +235,7 @@ class AuthenticatorValidateStageView(ChallengeStageView[AuthenticatorValidateSta
|
||||
"device_uid": -1,
|
||||
"challenge": get_webauthn_challenge_without_user(
|
||||
self.request,
|
||||
self.current_stage,
|
||||
self.executor.current_stage,
|
||||
),
|
||||
}
|
||||
)
|
||||
@ -244,6 +246,7 @@ class AuthenticatorValidateStageView(ChallengeStageView[AuthenticatorValidateSta
|
||||
"""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()
|
||||
@ -254,7 +257,7 @@ class AuthenticatorValidateStageView(ChallengeStageView[AuthenticatorValidateSta
|
||||
self.logger.debug("Refusing passwordless flow in non-authentication flow")
|
||||
return self.executor.stage_ok()
|
||||
# Passwordless auth, with just webauthn
|
||||
if DeviceClasses.WEBAUTHN in self.current_stage.device_classes:
|
||||
if DeviceClasses.WEBAUTHN in stage.device_classes:
|
||||
self.logger.debug("Flow without user, getting generic webauthn challenge")
|
||||
challenges = self.get_webauthn_challenge_without_user()
|
||||
else:
|
||||
@ -264,13 +267,13 @@ class AuthenticatorValidateStageView(ChallengeStageView[AuthenticatorValidateSta
|
||||
|
||||
# No allowed devices
|
||||
if len(challenges) < 1:
|
||||
if self.current_stage.not_configured_action == NotConfiguredAction.SKIP:
|
||||
if stage.not_configured_action == NotConfiguredAction.SKIP:
|
||||
self.logger.debug("Authenticator not configured, skipping stage")
|
||||
return self.executor.stage_ok()
|
||||
if self.current_stage.not_configured_action == NotConfiguredAction.DENY:
|
||||
if stage.not_configured_action == NotConfiguredAction.DENY:
|
||||
self.logger.debug("Authenticator not configured, denying")
|
||||
return self.executor.stage_invalid(_("No (allowed) MFA authenticator configured."))
|
||||
if self.current_stage.not_configured_action == NotConfiguredAction.CONFIGURE:
|
||||
if 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)
|
||||
@ -279,7 +282,8 @@ class AuthenticatorValidateStageView(ChallengeStageView[AuthenticatorValidateSta
|
||||
"""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."""
|
||||
if not self.current_stage.configuration_stages.exists():
|
||||
stage: AuthenticatorValidateStage = self.executor.current_stage
|
||||
if not stage.configuration_stages.exists():
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message=(
|
||||
@ -289,19 +293,15 @@ class AuthenticatorValidateStageView(ChallengeStageView[AuthenticatorValidateSta
|
||||
stage=self,
|
||||
).from_http(self.request).set_user(user).save()
|
||||
return self.executor.stage_invalid()
|
||||
if self.current_stage.configuration_stages.count() == 1:
|
||||
next_stage = Stage.objects.get_subclass(
|
||||
pk=self.current_stage.configuration_stages.first().pk
|
||||
)
|
||||
if stage.configuration_stages.count() == 1:
|
||||
next_stage = Stage.objects.get_subclass(pk=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=self.current_stage.configuration_stages.all()
|
||||
).select_subclasses()
|
||||
stages = Stage.objects.filter(pk__in=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[AuthenticatorValidateSta
|
||||
res = super().post(request, *args, **kwargs)
|
||||
if (
|
||||
PLAN_CONTEXT_SELECTED_STAGE in self.executor.plan.context
|
||||
and self.current_stage.not_configured_action == NotConfiguredAction.CONFIGURE
|
||||
and self.executor.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[AuthenticatorValidateSta
|
||||
def cookie_jwt_key(self) -> str:
|
||||
"""Signing key for MFA Cookie for this stage"""
|
||||
return sha256(
|
||||
f"{get_unique_identifier()}:{self.current_stage.pk.hex}".encode("ascii")
|
||||
f"{get_unique_identifier()}:{self.executor.current_stage.pk.hex}".encode("ascii")
|
||||
).hexdigest()
|
||||
|
||||
def check_mfa_cookie(self, allowed_devices: list[Device]):
|
||||
@ -362,11 +362,12 @@ class AuthenticatorValidateStageView(ChallengeStageView[AuthenticatorValidateSta
|
||||
correct user and with an allowed class"""
|
||||
if COOKIE_NAME_MFA not in self.request.COOKIES:
|
||||
return
|
||||
threshold = timedelta_from_string(self.current_stage.last_auth_threshold)
|
||||
stage: AuthenticatorValidateStage = self.executor.current_stage
|
||||
threshold = timedelta_from_string(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"] != self.current_stage.pk.hex:
|
||||
if payload["stage"] != stage.pk.hex:
|
||||
self.logger.warning("Invalid stage PK")
|
||||
return
|
||||
if datetime.fromtimestamp(payload["exp"]) > latest_allowed:
|
||||
@ -384,14 +385,15 @@ class AuthenticatorValidateStageView(ChallengeStageView[AuthenticatorValidateSta
|
||||
"""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"""
|
||||
delta = timedelta_from_string(self.current_stage.last_auth_threshold)
|
||||
stage: AuthenticatorValidateStage = self.executor.current_stage
|
||||
delta = timedelta_from_string(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": self.current_stage.pk.hex,
|
||||
"stage": stage.pk.hex,
|
||||
"exp": expiry.timestamp(),
|
||||
}
|
||||
response = self.executor.stage_ok()
|
||||
|
@ -108,7 +108,7 @@ class AuthenticatorWebAuthnChallengeResponse(ChallengeResponse):
|
||||
return registration
|
||||
|
||||
|
||||
class AuthenticatorWebAuthnStageView(ChallengeStageView[AuthenticatorWebAuthnStage]):
|
||||
class AuthenticatorWebAuthnStageView(ChallengeStageView):
|
||||
"""WebAuthn stage"""
|
||||
|
||||
response_class = AuthenticatorWebAuthnChallengeResponse
|
||||
@ -116,11 +116,12 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView[AuthenticatorWebAuthnSta
|
||||
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 = self.current_stage.authenticator_attachment
|
||||
authenticator_attachment = stage.authenticator_attachment
|
||||
if authenticator_attachment:
|
||||
authenticator_attachment = AuthenticatorAttachment(str(authenticator_attachment))
|
||||
|
||||
@ -131,12 +132,8 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView[AuthenticatorWebAuthnSta
|
||||
user_name=user.username,
|
||||
user_display_name=user.name,
|
||||
authenticator_selection=AuthenticatorSelectionCriteria(
|
||||
resident_key=ResidentKeyRequirement(
|
||||
str(self.current_stage.resident_key_requirement)
|
||||
),
|
||||
user_verification=UserVerificationRequirement(
|
||||
str(self.current_stage.user_verification)
|
||||
),
|
||||
resident_key=ResidentKeyRequirement(str(stage.resident_key_requirement)),
|
||||
user_verification=UserVerificationRequirement(str(stage.user_verification)),
|
||||
authenticator_attachment=authenticator_attachment,
|
||||
),
|
||||
attestation=AttestationConveyancePreference.DIRECT,
|
||||
|
@ -70,7 +70,7 @@ class CaptchaChallengeResponse(ChallengeResponse):
|
||||
return data
|
||||
|
||||
|
||||
class CaptchaStageView(ChallengeStageView[CaptchaChallenge]):
|
||||
class CaptchaStageView(ChallengeStageView):
|
||||
"""Simple captcha checker, logic is handled in django-captcha module"""
|
||||
|
||||
response_class = CaptchaChallengeResponse
|
||||
@ -78,8 +78,8 @@ class CaptchaStageView(ChallengeStageView[CaptchaChallenge]):
|
||||
def get_challenge(self, *args, **kwargs) -> Challenge:
|
||||
return CaptchaChallenge(
|
||||
data={
|
||||
"js_url": self.current_stage.js_url,
|
||||
"site_key": self.current_stage.public_key,
|
||||
"js_url": self.executor.current_stage.js_url,
|
||||
"site_key": self.executor.current_stage.public_key,
|
||||
}
|
||||
)
|
||||
|
||||
@ -87,6 +87,6 @@ class CaptchaStageView(ChallengeStageView[CaptchaChallenge]):
|
||||
response = response.validated_data["token"]
|
||||
self.executor.plan.context[PLAN_CONTEXT_CAPTCHA] = {
|
||||
"response": response,
|
||||
"stage": self.current_stage,
|
||||
"stage": self.executor.current_stage,
|
||||
}
|
||||
return self.executor.stage_ok()
|
||||
|
@ -48,7 +48,7 @@ class ConsentChallengeResponse(ChallengeResponse):
|
||||
token = CharField(required=True)
|
||||
|
||||
|
||||
class ConsentStageView(ChallengeStageView[ConsentStage]):
|
||||
class ConsentStageView(ChallengeStageView):
|
||||
"""Simple consent checker."""
|
||||
|
||||
response_class = ConsentChallengeResponse
|
||||
@ -72,13 +72,14 @@ class ConsentStageView(ChallengeStageView[ConsentStage]):
|
||||
"""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(self.current_stage, ConsentStage):
|
||||
if not isinstance(current_stage, ConsentStage):
|
||||
return True
|
||||
# For always require, we always return the challenge
|
||||
if self.current_stage.mode == ConsentMode.ALWAYS_REQUIRE:
|
||||
if 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:
|
||||
@ -124,6 +125,7 @@ class ConsentStageView(ChallengeStageView[ConsentStage]):
|
||||
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, []
|
||||
@ -137,9 +139,9 @@ class ConsentStageView(ChallengeStageView[ConsentStage]):
|
||||
)
|
||||
consent: UserConsent = self.executor.plan.context[PLAN_CONTEXT_CONSENT]
|
||||
consent.permissions = permissions_string
|
||||
if self.current_stage.mode == ConsentMode.PERMANENT:
|
||||
if current_stage.mode == ConsentMode.PERMANENT:
|
||||
consent.expiring = False
|
||||
if self.current_stage.mode == ConsentMode.EXPIRING:
|
||||
consent.expires = now() + timedelta_from_string(self.current_stage.consent_expire_in)
|
||||
if current_stage.mode == ConsentMode.EXPIRING:
|
||||
consent.expires = now() + timedelta_from_string(current_stage.consent_expire_in)
|
||||
consent.save()
|
||||
return self.executor.stage_ok()
|
||||
|
@ -6,10 +6,11 @@ from authentik.flows.stage import StageView
|
||||
from authentik.stages.deny.models import DenyStage
|
||||
|
||||
|
||||
class DenyStageView(StageView[DenyStage]):
|
||||
class DenyStageView(StageView):
|
||||
"""Cancels the current flow"""
|
||||
|
||||
def dispatch(self, request: HttpRequest) -> HttpResponse:
|
||||
"""Cancels the current flow"""
|
||||
message = self.executor.plan.context.get("deny_message", self.current_stage.deny_message)
|
||||
stage: DenyStage = self.executor.current_stage
|
||||
message = self.executor.plan.context.get("deny_message", stage.deny_message)
|
||||
return self.executor.stage_invalid(message)
|
||||
|
@ -30,11 +30,11 @@ class DummyStageView(ChallengeStageView):
|
||||
return self.executor.stage_ok()
|
||||
|
||||
def get_challenge(self, *args, **kwargs) -> Challenge:
|
||||
if self.current_stage.throw_error:
|
||||
if self.executor.current_stage.throw_error:
|
||||
raise SentryIgnoredException("Test error")
|
||||
return DummyChallenge(
|
||||
data={
|
||||
"title": self.current_stage.name,
|
||||
"name": self.current_stage.name,
|
||||
"title": self.executor.current_stage.name,
|
||||
"name": self.executor.current_stage.name,
|
||||
}
|
||||
)
|
||||
|
@ -46,7 +46,7 @@ class EmailChallengeResponse(ChallengeResponse):
|
||||
raise ValidationError(detail="email-sent", code="email-sent")
|
||||
|
||||
|
||||
class EmailStageView(ChallengeStageView[EmailStage]):
|
||||
class EmailStageView(ChallengeStageView):
|
||||
"""Email stage which sends Email for verification"""
|
||||
|
||||
response_class = EmailChallengeResponse
|
||||
@ -72,10 +72,11 @@ class EmailStageView(ChallengeStageView[EmailStage]):
|
||||
def get_token(self) -> FlowToken:
|
||||
"""Get token"""
|
||||
pending_user = self.get_pending_user()
|
||||
current_stage: EmailStage = self.executor.current_stage
|
||||
valid_delta = timedelta(
|
||||
minutes=self.current_stage.token_expiry + 1
|
||||
minutes=current_stage.token_expiry + 1
|
||||
) # + 1 because django timesince always rounds down
|
||||
identifier = slugify(f"ak-email-stage-{self.current_stage.name}-{str(uuid4())}")
|
||||
identifier = slugify(f"ak-email-stage-{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():
|
||||
@ -104,14 +105,15 @@ class EmailStageView(ChallengeStageView[EmailStage]):
|
||||
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=_(self.current_stage.subject),
|
||||
subject=_(current_stage.subject),
|
||||
to=[(pending_user.name, email)],
|
||||
language=pending_user.locale(self.request),
|
||||
template_name=self.current_stage.template,
|
||||
template_name=current_stage.template,
|
||||
template_context={
|
||||
"url": self.get_full_url(**{QS_KEY_TOKEN: token.key}),
|
||||
"user": pending_user,
|
||||
@ -119,28 +121,26 @@ class EmailStageView(ChallengeStageView[EmailStage]):
|
||||
"token": token.key,
|
||||
},
|
||||
)
|
||||
send_mails(self.current_stage, message)
|
||||
send_mails(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=self.current_stage.template,
|
||||
template=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 | None = self.executor.plan.context.get(
|
||||
PLAN_CONTEXT_IS_RESTORED, None
|
||||
)
|
||||
restore_token: FlowToken = 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.current_stage.activate_user_on_success:
|
||||
if self.executor.current_stage.activate_user_on_success:
|
||||
user.is_active = True
|
||||
user.save()
|
||||
return self.executor.stage_ok()
|
||||
|
@ -27,7 +27,6 @@ class IdentificationStageSerializer(StageSerializer):
|
||||
fields = StageSerializer.Meta.fields + [
|
||||
"user_fields",
|
||||
"password_stage",
|
||||
"captcha_stage",
|
||||
"case_insensitive_matching",
|
||||
"show_matched_user",
|
||||
"enrollment_flow",
|
||||
|
@ -1,26 +0,0 @@
|
||||
# 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",
|
||||
),
|
||||
),
|
||||
]
|
@ -8,7 +8,6 @@ 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
|
||||
|
||||
|
||||
@ -43,15 +42,6 @@ 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,
|
||||
|
@ -30,14 +30,9 @@ 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 PasswordChallenge, PasswordStageView, authenticate
|
||||
from authentik.stages.password.stage import authenticate
|
||||
|
||||
|
||||
@extend_schema_field(
|
||||
@ -68,8 +63,8 @@ class IdentificationChallenge(Challenge):
|
||||
"""Identification challenges with all UI elements"""
|
||||
|
||||
user_fields = ListField(child=CharField(), allow_empty=True, allow_null=True)
|
||||
password_stage = PasswordChallenge(required=False)
|
||||
captcha_stage = CaptchaChallenge(required=False)
|
||||
password_fields = BooleanField()
|
||||
allow_show_password = BooleanField(default=False)
|
||||
application_pre = CharField(required=False)
|
||||
flow_designation = ChoiceField(FlowDesignation.choices)
|
||||
|
||||
@ -89,7 +84,6 @@ 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
|
||||
|
||||
@ -134,50 +128,49 @@ class IdentificationChallengeResponse(ChallengeResponse):
|
||||
return attrs
|
||||
raise ValidationError("Failed to authenticate.")
|
||||
self.pre_user = pre_user
|
||||
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"))
|
||||
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
|
||||
return attrs
|
||||
|
||||
|
||||
class IdentificationStageView(ChallengeStageView[IdentificationStage]):
|
||||
class IdentificationStageView(ChallengeStageView):
|
||||
"""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 self.current_stage.user_fields:
|
||||
for search_field in current_stage.user_fields:
|
||||
model_field = {
|
||||
"email": "email",
|
||||
"username": "username",
|
||||
"upn": "attributes__upn",
|
||||
}[search_field]
|
||||
if self.current_stage.case_insensitive_matching:
|
||||
if current_stage.case_insensitive_matching:
|
||||
model_field += "__iexact"
|
||||
else:
|
||||
model_field += "__exact"
|
||||
@ -198,12 +191,16 @@ class IdentificationStageView(ChallengeStageView[IdentificationStage]):
|
||||
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": self.current_stage.user_fields,
|
||||
"show_source_labels": self.current_stage.show_source_labels,
|
||||
"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,
|
||||
"flow_designation": self.executor.flow.designation,
|
||||
}
|
||||
)
|
||||
@ -215,39 +212,29 @@ class IdentificationStageView(ChallengeStageView[IdentificationStage]):
|
||||
).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 self.current_stage.enrollment_flow:
|
||||
if current_stage.enrollment_flow:
|
||||
challenge.initial_data["enroll_url"] = reverse_with_qs(
|
||||
"authentik_core:if-flow",
|
||||
query=get_qs,
|
||||
kwargs={"flow_slug": self.current_stage.enrollment_flow.slug},
|
||||
kwargs={"flow_slug": current_stage.enrollment_flow.slug},
|
||||
)
|
||||
if self.current_stage.recovery_flow:
|
||||
if current_stage.recovery_flow:
|
||||
challenge.initial_data["recovery_url"] = reverse_with_qs(
|
||||
"authentik_core:if-flow",
|
||||
query=get_qs,
|
||||
kwargs={"flow_slug": self.current_stage.recovery_flow.slug},
|
||||
kwargs={"flow_slug": current_stage.recovery_flow.slug},
|
||||
)
|
||||
if self.current_stage.passwordless_flow:
|
||||
if current_stage.passwordless_flow:
|
||||
challenge.initial_data["passwordless_url"] = reverse_with_qs(
|
||||
"authentik_core:if-flow",
|
||||
query=get_qs,
|
||||
kwargs={"flow_slug": self.current_stage.passwordless_flow.slug},
|
||||
kwargs={"flow_slug": 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] = (
|
||||
self.current_stage.sources.filter(enabled=True).order_by("name").select_subclasses()
|
||||
current_stage.sources.filter(enabled=True).order_by("name").select_subclasses()
|
||||
)
|
||||
for source in sources:
|
||||
ui_login_button = source.ui_login_button(self.request)
|
||||
@ -262,7 +249,8 @@ class IdentificationStageView(ChallengeStageView[IdentificationStage]):
|
||||
|
||||
def challenge_valid(self, response: IdentificationChallengeResponse) -> HttpResponse:
|
||||
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = response.pre_user
|
||||
if not self.current_stage.show_matched_user:
|
||||
current_stage: IdentificationStage = self.executor.current_stage
|
||||
if not current_stage.show_matched_user:
|
||||
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER_IDENTIFIER] = (
|
||||
response.validated_data.get("uid_field")
|
||||
)
|
||||
|
@ -17,7 +17,7 @@ INVITATION_IN_EFFECT = "invitation_in_effect"
|
||||
INVITATION = "invitation"
|
||||
|
||||
|
||||
class InvitationStageView(StageView[InvitationStage]):
|
||||
class InvitationStageView(StageView):
|
||||
"""Finalise Authentication flow by logging the user in"""
|
||||
|
||||
def get_token(self) -> str | None:
|
||||
@ -52,10 +52,11 @@ class InvitationStageView(StageView[InvitationStage]):
|
||||
|
||||
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 self.current_stage.continue_flow_without_invitation:
|
||||
if stage.continue_flow_without_invitation:
|
||||
return self.executor.stage_ok()
|
||||
return self.executor.stage_invalid(_("Invalid invite/invite not found"))
|
||||
|
||||
|
@ -130,7 +130,7 @@ class PasswordChallengeResponse(ChallengeResponse):
|
||||
return password
|
||||
|
||||
|
||||
class PasswordStageView(ChallengeStageView[PasswordStage]):
|
||||
class PasswordStageView(ChallengeStageView):
|
||||
"""Authentication stage which authenticates against django's AuthBackend"""
|
||||
|
||||
response_class = PasswordChallengeResponse
|
||||
@ -138,7 +138,7 @@ class PasswordStageView(ChallengeStageView[PasswordStage]):
|
||||
def get_challenge(self) -> Challenge:
|
||||
challenge = PasswordChallenge(
|
||||
data={
|
||||
"allow_show_password": self.current_stage.allow_show_password,
|
||||
"allow_show_password": self.executor.current_stage.allow_show_password,
|
||||
}
|
||||
)
|
||||
recovery_flow = Flow.objects.filter(designation=FlowDesignation.RECOVERY)
|
||||
@ -154,9 +154,10 @@ class PasswordStageView(ChallengeStageView[PasswordStage]):
|
||||
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]
|
||||
>= self.current_stage.failed_attempts_before_cancel
|
||||
>= current_stage.failed_attempts_before_cancel
|
||||
):
|
||||
self.logger.debug("User has exceeded maximum tries")
|
||||
del self.request.session[SESSION_KEY_INVALID_TRIES]
|
||||
|
@ -222,7 +222,7 @@ class PromptStageView(ChallengeStageView):
|
||||
return serializers
|
||||
|
||||
def get_challenge(self, *args, **kwargs) -> Challenge:
|
||||
fields: list[Prompt] = list(self.current_stage.fields.all().order_by("order"))
|
||||
fields: list[Prompt] = list(self.executor.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.current_stage,
|
||||
stage_instance=self.executor.current_stage,
|
||||
stage=self,
|
||||
plan=self.executor.plan,
|
||||
user=self.get_pending_user(),
|
||||
|
@ -7,10 +7,9 @@ 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[UserDeleteStage]):
|
||||
class UserDeleteStageView(StageView):
|
||||
"""Finalise unenrollment flow by deleting the user object."""
|
||||
|
||||
def dispatch(self, request: HttpRequest) -> HttpResponse:
|
||||
|
@ -39,7 +39,7 @@ class UserLoginChallengeResponse(ChallengeResponse):
|
||||
remember_me = BooleanField(required=True)
|
||||
|
||||
|
||||
class UserLoginStageView(ChallengeStageView[UserLoginStage]):
|
||||
class UserLoginStageView(ChallengeStageView):
|
||||
"""Finalise Authentication flow by logging the user in"""
|
||||
|
||||
response_class = UserLoginChallengeResponse
|
||||
@ -49,7 +49,8 @@ class UserLoginStageView(ChallengeStageView[UserLoginStage]):
|
||||
|
||||
def dispatch(self, request: HttpRequest) -> HttpResponse:
|
||||
"""Check for remember_me, and do login"""
|
||||
if timedelta_from_string(self.current_stage.remember_me_offset).total_seconds() > 0:
|
||||
stage: UserLoginStage = self.executor.current_stage
|
||||
if timedelta_from_string(stage.remember_me_offset).total_seconds() > 0:
|
||||
return super().dispatch(request)
|
||||
return self.do_login(request)
|
||||
|
||||
@ -58,9 +59,9 @@ class UserLoginStageView(ChallengeStageView[UserLoginStage]):
|
||||
|
||||
def set_session_duration(self, remember: bool) -> timedelta:
|
||||
"""Update the sessions' expiry"""
|
||||
delta = timedelta_from_string(self.current_stage.session_duration)
|
||||
delta = timedelta_from_string(self.executor.current_stage.session_duration)
|
||||
if remember:
|
||||
offset = timedelta_from_string(self.current_stage.remember_me_offset)
|
||||
offset = timedelta_from_string(self.executor.current_stage.remember_me_offset)
|
||||
delta = delta + offset
|
||||
if delta.total_seconds() == 0:
|
||||
self.request.session.set_expiry(0)
|
||||
@ -70,9 +71,11 @@ class UserLoginStageView(ChallengeStageView[UserLoginStage]):
|
||||
|
||||
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] = self.current_stage.network_binding
|
||||
self.request.session[SESSION_KEY_BINDING_GEO] = self.current_stage.geoip_binding
|
||||
self.request.session[SESSION_KEY_BINDING_NET] = stage.network_binding
|
||||
self.request.session[SESSION_KEY_BINDING_GEO] = stage.geoip_binding
|
||||
|
||||
def do_login(self, request: HttpRequest, remember: bool = False) -> HttpResponse:
|
||||
"""Attach the currently pending user to the current session"""
|
||||
@ -108,7 +111,7 @@ class UserLoginStageView(ChallengeStageView[UserLoginStage]):
|
||||
# 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.current_stage.terminate_other_sessions:
|
||||
if self.executor.current_stage.terminate_other_sessions:
|
||||
AuthenticatedSession.objects.filter(
|
||||
user=user,
|
||||
).exclude(session_key=self.request.session.session_key).delete()
|
||||
|
@ -4,10 +4,9 @@ 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[UserLogoutStage]):
|
||||
class UserLogoutStageView(StageView):
|
||||
"""Finalise Authentication flow by logging the user in"""
|
||||
|
||||
def dispatch(self, request: HttpRequest) -> HttpResponse:
|
||||
|
@ -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.current_stage.user_path_template
|
||||
PLAN_CONTEXT_USER_PATH, self.executor.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.current_stage.user_type,
|
||||
self.executor.current_stage.user_type,
|
||||
)
|
||||
)
|
||||
except ValueError:
|
||||
user_type = self.current_stage.user_type
|
||||
user_type = self.executor.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.current_stage.user_creation_mode == UserCreationMode.ALWAYS_CREATE
|
||||
or self.executor.current_stage.user_creation_mode == UserCreationMode.ALWAYS_CREATE
|
||||
):
|
||||
if self.current_stage.user_creation_mode == UserCreationMode.NEVER_CREATE:
|
||||
if self.executor.current_stage.user_creation_mode == UserCreationMode.NEVER_CREATE:
|
||||
return None, False
|
||||
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = User(
|
||||
is_active=not self.current_stage.create_users_as_inactive,
|
||||
is_active=not self.executor.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.current_stage.create_users_group:
|
||||
user.ak_groups.add(self.current_stage.create_users_group)
|
||||
if self.executor.current_stage.create_users_group:
|
||||
user.ak_groups.add(self.executor.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:
|
||||
|
@ -2,7 +2,7 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "https://goauthentik.io/blueprints/schema.json",
|
||||
"type": "object",
|
||||
"title": "authentik 2024.6.4 Blueprint schema",
|
||||
"title": "authentik 2024.8.0 Blueprint schema",
|
||||
"required": [
|
||||
"version",
|
||||
"entries"
|
||||
@ -10091,11 +10091,6 @@
|
||||
"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",
|
||||
|
@ -31,7 +31,7 @@ services:
|
||||
volumes:
|
||||
- redis:/data
|
||||
server:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.4}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.8.0}
|
||||
restart: unless-stopped
|
||||
command: server
|
||||
environment:
|
||||
@ -52,7 +52,7 @@ services:
|
||||
- postgresql
|
||||
- redis
|
||||
worker:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.4}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.8.0}
|
||||
restart: unless-stopped
|
||||
command: worker
|
||||
environment:
|
||||
|
6
go.mod
6
go.mod
@ -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.0
|
||||
github.com/jellydator/ttlcache/v3 v3.2.1
|
||||
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.1
|
||||
github.com/prometheus/client_golang v1.20.2
|
||||
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.2024063.13
|
||||
goauthentik.io/api/v3 v3.2024064.1
|
||||
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
16
go.sum
@ -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.0 h1:6lqVJ8X3ZaUwvzENqPAobDsXNExfUJd61u++uW8a3LE=
|
||||
github.com/jellydator/ttlcache/v3 v3.2.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4=
|
||||
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/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.1 h1:IMJXHOD6eARkQpxo8KkhgEVFlBNm+nkrFUyGlIu7Na8=
|
||||
github.com/prometheus/client_golang v1.20.1/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
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_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.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=
|
||||
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=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
|
@ -29,4 +29,4 @@ func UserAgent() string {
|
||||
return fmt.Sprintf("authentik@%s", FullVersion())
|
||||
}
|
||||
|
||||
const VERSION = "2024.6.4"
|
||||
const VERSION = "2024.8.0"
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2024.6.4",
|
||||
"version": "2024.8.0",
|
||||
"private": true
|
||||
}
|
||||
|
8
poetry.lock
generated
8
poetry.lock
generated
@ -1312,17 +1312,17 @@ django = ">=3"
|
||||
|
||||
[[package]]
|
||||
name = "django-pglock"
|
||||
version = "1.5.1"
|
||||
version = "1.6.0"
|
||||
description = "Postgres locking routines and lock table access."
|
||||
optional = false
|
||||
python-versions = "<4,>=3.8.0"
|
||||
files = [
|
||||
{file = "django_pglock-1.5.1-py3-none-any.whl", hash = "sha256:d3b977922abbaffd43968714b69cdab7453866adf2b0695fb497491748d7bc67"},
|
||||
{file = "django_pglock-1.5.1.tar.gz", hash = "sha256:291903d5d877b68558003e1d64d764ebd5590344ba3b7aa1d5127df5947869b1"},
|
||||
{file = "django_pglock-1.6.0-py3-none-any.whl", hash = "sha256:41c98d0bd3738d11e6eaefcc3e5146028f118a593ac58c13d663b751170f01de"},
|
||||
{file = "django_pglock-1.6.0.tar.gz", hash = "sha256:724450ecc9886f39af599c477d84ad086545a5373215ef7a670cd25faca25a61"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
django = ">=3"
|
||||
django = ">=4"
|
||||
django-pgactivity = ">=1.2,<2"
|
||||
|
||||
[[package]]
|
||||
|
@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "authentik"
|
||||
version = "2024.6.4"
|
||||
version = "2024.8.0"
|
||||
description = ""
|
||||
authors = ["authentik Team <hello@goauthentik.io>"]
|
||||
|
||||
|
32
schema.yml
32
schema.yml
@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: authentik
|
||||
version: 2024.6.4
|
||||
version: 2024.8.0
|
||||
description: Making authentication simple.
|
||||
contact:
|
||||
email: hello@goauthentik.io
|
||||
@ -40457,10 +40457,11 @@ components:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
password_stage:
|
||||
$ref: '#/components/schemas/PasswordChallenge'
|
||||
captcha_stage:
|
||||
$ref: '#/components/schemas/CaptchaChallenge'
|
||||
password_fields:
|
||||
type: boolean
|
||||
allow_show_password:
|
||||
type: boolean
|
||||
default: false
|
||||
application_pre:
|
||||
type: string
|
||||
flow_designation:
|
||||
@ -40481,6 +40482,7 @@ components:
|
||||
type: boolean
|
||||
required:
|
||||
- flow_designation
|
||||
- password_fields
|
||||
- primary_action
|
||||
- show_source_labels
|
||||
- user_fields
|
||||
@ -40498,8 +40500,6 @@ components:
|
||||
password:
|
||||
type: string
|
||||
nullable: true
|
||||
captcha:
|
||||
$ref: '#/components/schemas/CaptchaChallengeResponseRequest'
|
||||
required:
|
||||
- uid_field
|
||||
IdentificationStage:
|
||||
@ -40545,12 +40545,6 @@ 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.
|
||||
@ -40619,12 +40613,6 @@ 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.
|
||||
@ -45757,12 +45745,6 @@ 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
904
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -32,7 +32,7 @@
|
||||
"guacamole-common-js": "^1.5.0",
|
||||
"lit": "^3.2.0",
|
||||
"md-front-matter": "^1.0.4",
|
||||
"mermaid": "^10.9.1",
|
||||
"mermaid": "^11.0.2",
|
||||
"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.0",
|
||||
"@eslint/js": "^9.9.1",
|
||||
"@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": "^12.3.3",
|
||||
"syncpack": "^13.0.0",
|
||||
"ts-lit-plugin": "^2.0.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"tslib": "^2.6.3",
|
||||
"tslib": "^2.7.0",
|
||||
"turnstile-types": "^1.2.2",
|
||||
"typescript": "^5.5.4",
|
||||
"typescript-eslint": "^8.2.0",
|
||||
|
@ -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.14",
|
||||
"@swc/core": "^1.7.18",
|
||||
"@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.14",
|
||||
"@swc/core": "^1.7.18",
|
||||
"@swc/core-darwin-arm64": "^1.6.13",
|
||||
"@swc/core-darwin-x64": "^1.6.13",
|
||||
"@swc/core-linux-arm-gnueabihf": "^1.6.13",
|
||||
|
96
web/sfe/package-lock.json
generated
96
web/sfe/package-lock.json
generated
@ -9,7 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@goauthentik/api": "^2024.6.3-1724337552",
|
||||
"@goauthentik/api": "^2024.6.3-1724414734",
|
||||
"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.14",
|
||||
"@swc/core": "^1.7.18",
|
||||
"@types/jquery": "^3.5.30",
|
||||
"rollup": "^4.21.0",
|
||||
"rollup-plugin-copy": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@goauthentik/api": {
|
||||
"version": "2024.6.3-1724337552",
|
||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.6.3-1724337552.tgz",
|
||||
"integrity": "sha512-siu5qJqUt13iUPsLI0RfieVkDU8IMhuP2i5C/RRqY6oek0z+srSom9UTBAh6n6a2pTTNQO3clE2zxvAIJPahVg=="
|
||||
"version": "2024.6.3-1724414734",
|
||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.6.3-1724414734.tgz",
|
||||
"integrity": "sha512-2fLKwOh2Znc/unD8Q2U4G0g5QFM4jVqC95e5VRWWVnzp3xB7JWfEDBcRdwyv5PxCdmjBUkvbiul0kiuRwqBf4w=="
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
@ -491,9 +491,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core": {
|
||||
"version": "1.7.14",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.14.tgz",
|
||||
"integrity": "sha512-9aeXeifnyuvc2pcuuhPQgVUwdpGEzZ+9nJu0W8/hNl/aESFsJGR5i9uQJRGu0atoNr01gK092fvmqMmQAPcKow==",
|
||||
"version": "1.7.18",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.18.tgz",
|
||||
"integrity": "sha512-qL9v5N5S38ijmqiQRvCFUUx2vmxWT/JJ2rswElnyaHkOHuVoAFhBB90Ywj4RKjh3R0zOjhEcemENTyF3q3G6WQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
@ -508,16 +508,16 @@
|
||||
"url": "https://opencollective.com/swc"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@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"
|
||||
"@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"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@swc/helpers": "*"
|
||||
@ -529,9 +529,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-darwin-arm64": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -545,9 +545,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-darwin-x64": {
|
||||
"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==",
|
||||
"version": "1.7.18",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.18.tgz",
|
||||
"integrity": "sha512-IkukOQUw7/14VkHp446OkYGCZEHqZg9pTmTdBawlUyz2JwZMSn2VodCl7aFSdGCsU4Cwni8zKA8CCgkCCAELhw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -561,9 +561,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm-gnueabihf": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@ -577,9 +577,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm64-gnu": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -593,9 +593,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm64-musl": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -609,9 +609,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-x64-gnu": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -625,9 +625,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-x64-musl": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -641,9 +641,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-arm64-msvc": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -657,9 +657,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-ia32-msvc": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@ -673,9 +673,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-x64-msvc": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@goauthentik/api": "^2024.6.3-1724337552",
|
||||
"@goauthentik/api": "^2024.6.3-1724414734",
|
||||
"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.14",
|
||||
"@swc/core": "^1.7.18",
|
||||
"@types/jquery": "^3.5.30",
|
||||
"rollup": "^4.21.0",
|
||||
"rollup-plugin-copy": "^3.5.0"
|
||||
|
@ -21,7 +21,6 @@ import {
|
||||
SourcesApi,
|
||||
Stage,
|
||||
StagesApi,
|
||||
StagesCaptchaListRequest,
|
||||
StagesPasswordListRequest,
|
||||
UserFieldsEnum,
|
||||
} from "@goauthentik/api";
|
||||
@ -47,7 +46,8 @@ async function makeSourcesSelector(instanceSources: string[] | undefined) {
|
||||
|
||||
return localSources
|
||||
? ([pk, _]: DualSelectPair) => localSources.has(pk)
|
||||
: ([_0, _1, _2, source]: DualSelectPair<Source>) =>
|
||||
: // Creating a new instance, auto-select built-in source only when no other sources exist
|
||||
([_0, _1, _2, source]: DualSelectPair<Source>) =>
|
||||
source !== undefined && source.component === "";
|
||||
}
|
||||
|
||||
@ -76,11 +76,11 @@ export class IdentificationStageForm extends BaseStageForm<IdentificationStage>
|
||||
stageUuid: this.instance.pk || "",
|
||||
identificationStageRequest: data,
|
||||
});
|
||||
} else {
|
||||
return new StagesApi(DEFAULT_CONFIG).stagesIdentificationCreate({
|
||||
identificationStageRequest: data,
|
||||
});
|
||||
}
|
||||
|
||||
return new StagesApi(DEFAULT_CONFIG).stagesIdentificationCreate({
|
||||
identificationStageRequest: data,
|
||||
});
|
||||
}
|
||||
|
||||
isUserFieldSelected(field: UserFieldsEnum): boolean {
|
||||
@ -161,37 +161,6 @@ 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
|
||||
@ -264,12 +233,12 @@ export class IdentificationStageForm extends BaseStageForm<IdentificationStage>
|
||||
?required=${true}
|
||||
name="sources"
|
||||
>
|
||||
<ak-dual-select-provider-dynamic-selected
|
||||
<ak-dual-select-dynamic-selected
|
||||
.provider=${sourcesProvider}
|
||||
.selected=${makeSourcesSelector(this.instance?.sources)}
|
||||
.selector=${makeSourcesSelector(this.instance?.sources)}
|
||||
available-label="${msg("Available Stages")}"
|
||||
selected-label="${msg("Selected Stages")}"
|
||||
></ak-dual-select-provider-dynamic-selected>
|
||||
></ak-dual-select-dynamic-selected>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Select sources should be shown for users to authenticate with. This only affects web-based sources, not LDAP.",
|
||||
|
@ -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.6.4";
|
||||
export const VERSION = "2024.8.0";
|
||||
export const TITLE_DEFAULT = "authentik";
|
||||
export const ROUTE_SEPARATOR = ";";
|
||||
|
||||
|
@ -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" as unknown as number,
|
||||
logLevel: "fatal",
|
||||
startOnLoad: false,
|
||||
flowchart: {
|
||||
curve: "linear",
|
||||
|
@ -50,3 +50,9 @@ export class AkDualSelectDynamic extends AkDualSelectProvider {
|
||||
></ak-dual-select>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-dual-select-dynamic-selected": AkDualSelectDynamic;
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ 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";
|
||||
@ -32,7 +33,17 @@ export class AccessDeniedStage extends BaseStage<
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
<form class="pf-c-form">
|
||||
${this.renderUserInfo()}
|
||||
<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>
|
||||
<ak-empty-state icon="fa-times" header=${msg("Request has been denied.")}>
|
||||
${this.challenge.errorMessage
|
||||
? html`
|
||||
|
@ -7,6 +7,7 @@ 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";
|
||||
@ -76,7 +77,17 @@ export class AuthenticatorDuoStage extends BaseStage<
|
||||
this.submitForm(e);
|
||||
}}
|
||||
>
|
||||
${this.renderUserInfo()}
|
||||
<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>
|
||||
<img
|
||||
src=${this.challenge.activationBarcode}
|
||||
alt=${msg("Duo activation QR code")}
|
||||
|
@ -41,7 +41,17 @@ export class AuthenticatorSMSStage extends BaseStage<
|
||||
this.submitForm(e);
|
||||
}}
|
||||
>
|
||||
${this.renderUserInfo()}
|
||||
<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>
|
||||
<ak-form-element
|
||||
label="${msg("Phone number")}"
|
||||
required
|
||||
|
@ -6,6 +6,7 @@ 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";
|
||||
@ -65,7 +66,17 @@ export class AuthenticatorStaticStage extends BaseStage<
|
||||
this.submitForm(e);
|
||||
}}
|
||||
>
|
||||
${this.renderUserInfo()}
|
||||
<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>
|
||||
<ak-form-element label="" class="pf-c-form__group">
|
||||
<ul>
|
||||
${this.challenge.codes.map((token) => {
|
||||
|
@ -9,6 +9,7 @@ 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";
|
||||
@ -59,7 +60,17 @@ export class AuthenticatorTOTPStage extends BaseStage<
|
||||
this.submitForm(e);
|
||||
}}
|
||||
>
|
||||
${this.renderUserInfo()}
|
||||
<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>
|
||||
<input type="hidden" name="otp_uri" value=${this.challenge.configUrl} />
|
||||
<ak-form-element>
|
||||
<div class="qr-container">
|
||||
|
@ -10,6 +10,7 @@ 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";
|
||||
@ -132,7 +133,17 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
<form class="pf-c-form">
|
||||
${this.renderUserInfo()}
|
||||
<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>
|
||||
<ak-empty-state
|
||||
?loading="${this.registerRunning}"
|
||||
header=${this.registerRunning
|
||||
|
@ -7,7 +7,8 @@ import type { TurnstileObject } from "turnstile-types";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, PropertyValues, TemplateResult, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { customElement, state } 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";
|
||||
@ -44,9 +45,6 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
@state()
|
||||
scriptElement?: HTMLScriptElement;
|
||||
|
||||
@property({ type: Boolean })
|
||||
embedded = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.captchaContainer = document.createElement("div");
|
||||
@ -163,9 +161,6 @@ 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>`;
|
||||
}
|
||||
@ -173,7 +168,18 @@ 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">
|
||||
${this.renderUserInfo()}
|
||||
<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.renderBody()}
|
||||
</form>
|
||||
</div>
|
||||
|
@ -5,6 +5,7 @@ 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";
|
||||
@ -108,7 +109,17 @@ export class ConsentStage extends BaseStage<ConsentChallenge, ConsentChallengeRe
|
||||
});
|
||||
}}
|
||||
>
|
||||
${this.renderUserInfo()}
|
||||
<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.challenge.additionalPermissions.length > 0
|
||||
? this.renderAdditional()
|
||||
: this.renderNoPrevious()}
|
||||
|
@ -4,7 +4,6 @@ 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";
|
||||
@ -124,7 +123,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.passwordStage) {
|
||||
if (!compatMode && !this.challenge.passwordFields) {
|
||||
const password = document.createElement("input");
|
||||
password.setAttribute("type", "password");
|
||||
password.setAttribute("name", "password");
|
||||
@ -261,7 +260,7 @@ export class IdentificationStage extends BaseStage<
|
||||
required
|
||||
/>
|
||||
</ak-form-element>
|
||||
${this.challenge.passwordStage
|
||||
${this.challenge.passwordFields
|
||||
? html`
|
||||
<ak-flow-input-password
|
||||
label=${msg("Password")}
|
||||
@ -269,20 +268,12 @@ export class IdentificationStage extends BaseStage<
|
||||
required
|
||||
class="pf-c-form__group"
|
||||
.errors=${(this.challenge?.responseErrors || {})["password"]}
|
||||
?allow-show-password=${this.challenge.passwordStage.allowShowPassword}
|
||||
?allow-show-password=${this.challenge.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}
|
||||
@ -293,13 +284,6 @@ 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>`;
|
||||
|
@ -8,6 +8,7 @@ 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";
|
||||
@ -44,7 +45,17 @@ export class PasswordStage extends BaseStage<PasswordChallenge, PasswordChalleng
|
||||
this.submitForm(e);
|
||||
}}
|
||||
>
|
||||
${this.renderUserInfo()}
|
||||
<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>
|
||||
<input
|
||||
name="username"
|
||||
autocomplete="username"
|
||||
|
@ -6,6 +6,7 @@ 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";
|
||||
@ -35,7 +36,17 @@ export class PasswordStage extends BaseStage<
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
<form class="pf-c-form">
|
||||
${this.renderUserInfo()}
|
||||
<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>
|
||||
<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?")}
|
||||
|
@ -6860,6 +6860,36 @@ 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 < Infinity ? internalUserPercentage : "∞"}"/>%</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sae72e1569d34fb02">
|
||||
<source>External user usage</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -7125,6 +7125,36 @@ 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 < Infinity ? internalUserPercentage : "∞"}"/>%</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sae72e1569d34fb02">
|
||||
<source>External user usage</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -6777,6 +6777,36 @@ 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 < Infinity ? internalUserPercentage : "∞"}"/>%</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sae72e1569d34fb02">
|
||||
<source>External user usage</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -9022,6 +9022,36 @@ 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 < Infinity ? internalUserPercentage : "∞"}"/>%</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sae72e1569d34fb02">
|
||||
<source>External user usage</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -8696,6 +8696,36 @@ 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 < Infinity ? internalUserPercentage : "∞"}"/>%</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sae72e1569d34fb02">
|
||||
<source>External user usage</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -8541,6 +8541,36 @@ 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 < Infinity ? internalUserPercentage : "∞"}"/>%</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sae72e1569d34fb02">
|
||||
<source>External user usage</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -8961,6 +8961,36 @@ 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 < Infinity ? internalUserPercentage : "∞"}"/>%</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sae72e1569d34fb02">
|
||||
<source>External user usage</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -8924,4 +8924,34 @@ 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 < Infinity ? internalUserPercentage : "∞"}"/>%</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sae72e1569d34fb02">
|
||||
<source>External user usage</source>
|
||||
</trans-unit>
|
||||
</body></file></xliff>
|
||||
|
@ -9025,6 +9025,36 @@ 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 < Infinity ? internalUserPercentage : "∞"}"/>%</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sae72e1569d34fb02">
|
||||
<source>External user usage</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -6770,6 +6770,36 @@ 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 < Infinity ? internalUserPercentage : "∞"}"/>%</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sae72e1569d34fb02">
|
||||
<source>External user usage</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -5707,6 +5707,36 @@ 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 < Infinity ? internalUserPercentage : "∞"}"/>%</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sae72e1569d34fb02">
|
||||
<source>External user usage</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
@ -9028,6 +9028,36 @@ 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 < Infinity ? internalUserPercentage : "∞"}"/>%</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sae72e1569d34fb02">
|
||||
<source>External user usage</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -6818,6 +6818,36 @@ 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 < Infinity ? internalUserPercentage : "∞"}"/>%</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sae72e1569d34fb02">
|
||||
<source>External user usage</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -8657,6 +8657,36 @@ 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 < Infinity ? internalUserPercentage : "∞"}"/>%</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sae72e1569d34fb02">
|
||||
<source>External user usage</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -36,11 +36,11 @@ To disable existing blueprints, an empty file can be mounted over the existing b
|
||||
|
||||
File-based blueprints are automatically removed once they become unavailable, however none of the objects created by those blueprints afre affected by this.
|
||||
|
||||
:::info
|
||||
Please note that, by default, blueprint discovery and evaluation is not guaranteed to follow any specific order.
|
||||
:::info
|
||||
Please note that, by default, blueprint discovery and evaluation is not guaranteed to follow any specific order.
|
||||
|
||||
If you have dependencies between blueprints, you should use [meta models](/developer-docs/blueprints/v1/meta#authentik_blueprintsmetaapplyblueprint) to make sure that objects are created in the correct order.
|
||||
:::
|
||||
If you have dependencies between blueprints, you should use [meta models](./v1/meta#authentik_blueprintsmetaapplyblueprint) to make sure that objects are created in the correct order.
|
||||
:::
|
||||
|
||||
## Storage - OCI
|
||||
|
||||
|
@ -105,11 +105,7 @@ The following events occur when a license expeires and is not renewed within two
|
||||
|
||||
### About users and licenses
|
||||
|
||||
License usage is calculated based on total user counts and log-in data data that authentik regularly captures. This data is checked against all valid licenses, and the sum total of all users.
|
||||
|
||||
- The **_internal user_** count is calculated based on actual users assigned to the organization.
|
||||
|
||||
- The **_external user_** count is calculated based on how many external users were active (i.e. logged in) since the start of the current month.
|
||||
License usage is calculated based on total user counts that authentik regularly captures. This data is checked against all valid licenses, and the sum total of all users. Internal and external users are counted based on the number of active users of the respective type saved in authentik. Service account users are not counted towards the license.
|
||||
|
||||
:::info
|
||||
An **internal** user is typically a team member, such as company employees, who has access to the full Enterprise feature set. An **external** user might be an external consultant, a volunteer in a charitable site, or a B2C customer who logged onto your website to shop. These users don't get access to Enterprise features.
|
||||
|
@ -18,7 +18,8 @@ Content-Type: application/x-www-form-urlencoded
|
||||
grant_type=client_credentials&
|
||||
client_id=application_client_id&
|
||||
username=my-service-account&
|
||||
password=my-token
|
||||
password=my-token&
|
||||
scope=profile
|
||||
```
|
||||
|
||||
This will return a JSON response with an `access_token`, which is a signed JWT token. This token can be sent along requests to other hosts, which can then validate the JWT based on the signing key configured in authentik.
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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}?size=64"
|
||||
cut_url = f"{url}"
|
||||
return AVATAR_STREAM_CONTENT.format(
|
||||
base64_string=(get_as_base64(cut_url).decode("utf-8"))
|
||||
)
|
||||
|
7
website/package-lock.json
generated
7
website/package-lock.json
generated
@ -12322,10 +12322,11 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.5",
|
||||
"license": "MIT",
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dependencies": {
|
||||
"braces": "^3.0.2",
|
||||
"braces": "^3.0.3",
|
||||
"picomatch": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
|
Reference in New Issue
Block a user