Compare commits
85 Commits
stages/ide
...
web/sdk/em
| Author | SHA1 | Date | |
|---|---|---|---|
| a407d903ab | |||
| 7e6f36b0b4 | |||
| 8dd7b0569c | |||
| 616be78e10 | |||
| 7125db1fbd | |||
| 8c7c7c3fee | |||
| bbbd00db22 | |||
| 6d60c5f7c7 | |||
| accf25a626 | |||
| 5f261bed96 | |||
| 4d51671f88 | |||
| 1c570b2502 | |||
| 78e4370b98 | |||
| 9a26111f1c | |||
| 02ae099bdf | |||
| 05fe4e5e7b | |||
| 24c289fdd1 | |||
| cd8de3e526 | |||
| 243bd03785 | |||
| e02cf0b3bd | |||
| 3018735579 | |||
| 6a956d149a | |||
| 3fd1bc6673 | |||
| 6bc4877702 | |||
| 9592b42501 | |||
| 517a5bc689 | |||
| a19e350ca6 | |||
| 88577145fb | |||
| bc93df1e29 | |||
| 9d5eb54504 | |||
| d95c433027 | |||
| 7416c90efb | |||
| 34a073b0f7 | |||
| 86eb112a03 | |||
| e48d001ea6 | |||
| 2070372f03 | |||
| bea4679192 | |||
| 32d1488a56 | |||
| 1003c79d8c | |||
| a05ed7e237 | |||
| 087d4f6a48 | |||
| 141cfe75d8 | |||
| a3a13d265b | |||
| 9c45ec1918 | |||
| 59fa449abe | |||
| f83c84b04d | |||
| 835a4097eb | |||
| da43df44b2 | |||
| d03b14aac4 | |||
| 62d990e91b | |||
| 6c8cfc9ef7 | |||
| e93d8b1646 | |||
| 6faa250574 | |||
| c8e4b187b8 | |||
| 98acca896a | |||
| 7dc4b70ee1 | |||
| 8f55d3fc07 | |||
| 17fb90e0af | |||
| 870ed99097 | |||
| 1c5af88ea9 | |||
| 461856d067 | |||
| cdbf448769 | |||
| 0fcac0e165 | |||
| 40e857fdb3 | |||
| 7d86593d05 | |||
| c95ce0a5d4 | |||
| a7b31ce6de | |||
| b5f4303fbd | |||
| 29f04ea801 | |||
| 7141702c9e | |||
| 0f30d135b6 | |||
| 73f326b21b | |||
| 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"
|
||||
|
||||
|
||||
|
||||
@ -25,3 +25,31 @@ class BrandMiddleware:
|
||||
if locale != "":
|
||||
activate(locale)
|
||||
return self.get_response(request)
|
||||
|
||||
|
||||
class BrandCORSAPIMiddleware:
|
||||
"""CORS for API requests depending on Brand"""
|
||||
|
||||
get_response: Callable[[HttpRequest], HttpResponse]
|
||||
|
||||
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
|
||||
self.get_response = get_response
|
||||
|
||||
def set_headers(self, request: HttpRequest, response: HttpResponse):
|
||||
response["Access-Control-Allow-Origin"] = "http://localhost:8080"
|
||||
response["Access-Control-Allow-Credentials"] = "true"
|
||||
|
||||
def __call__(self, request: HttpRequest) -> HttpResponse:
|
||||
if request.method == "OPTIONS":
|
||||
response = HttpResponse(
|
||||
status=200,
|
||||
)
|
||||
self.set_headers(request, response)
|
||||
response["Access-Control-Allow-Headers"] = (
|
||||
"authorization,sentry-trace,x-authentik-csrf,content-type"
|
||||
)
|
||||
response["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"
|
||||
return response
|
||||
response = self.get_response(request)
|
||||
self.set_headers(request, response)
|
||||
return response
|
||||
|
||||
@ -9,10 +9,11 @@ class Command(TenantCommand):
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("--type", type=str, required=True)
|
||||
parser.add_argument("--all", action="store_true")
|
||||
parser.add_argument("usernames", nargs="+", type=str)
|
||||
parser.add_argument("--all", action="store_true", default=False)
|
||||
parser.add_argument("usernames", nargs="*", type=str)
|
||||
|
||||
def handle_per_tenant(self, **options):
|
||||
print(options)
|
||||
new_type = UserTypes(options["type"])
|
||||
qs = (
|
||||
User.objects.exclude_anonymous()
|
||||
@ -22,6 +23,9 @@ class Command(TenantCommand):
|
||||
if options["usernames"] and options["all"]:
|
||||
self.stderr.write("--all and usernames specified, only one can be specified")
|
||||
return
|
||||
if not options["usernames"] and not options["all"]:
|
||||
self.stderr.write("--all or usernames must be specified")
|
||||
return
|
||||
if options["usernames"] and not options["all"]:
|
||||
qs = qs.filter(username__in=options["usernames"])
|
||||
updated = qs.update(type=new_type)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
<link rel="shortcut icon" href="{{ brand.branding_favicon }}">
|
||||
{% block head_before %}
|
||||
{% endblock %}
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly-base.css' %}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}" data-inject>
|
||||
{% versioned_script "dist/poly-%v.js" %}
|
||||
|
||||
@ -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={
|
||||
|
||||
@ -16,12 +16,14 @@ from django.views.decorators.clickjacking import xframe_options_sameorigin
|
||||
from django.views.generic import View
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiParameter, PolymorphicProxySerializer, extend_schema
|
||||
from rest_framework.exceptions import AuthenticationFailed
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.views import APIView
|
||||
from sentry_sdk import capture_exception, start_span
|
||||
from sentry_sdk.api import set_tag
|
||||
from structlog.stdlib import BoundLogger, get_logger
|
||||
|
||||
from authentik.api.authentication import bearer_auth, get_authorization_header
|
||||
from authentik.brands.models import Brand
|
||||
from authentik.core.models import Application
|
||||
from authentik.events.models import Event, EventAction, cleanse_dict
|
||||
@ -116,6 +118,14 @@ class FlowExecutorView(APIView):
|
||||
super().setup(request, flow_slug=flow_slug)
|
||||
self.flow = get_object_or_404(Flow.objects.select_related(), slug=flow_slug)
|
||||
self._logger = get_logger().bind(flow_slug=flow_slug)
|
||||
# Usually flows are authenticated by session, we don't really use rest_framework's
|
||||
# authentication method.
|
||||
try:
|
||||
user = bearer_auth(get_authorization_header(request))
|
||||
if user:
|
||||
request.user = user
|
||||
except AuthenticationFailed:
|
||||
pass
|
||||
set_tag("authentik.flow", self.flow.slug)
|
||||
|
||||
def handle_invalid_flow(self, exc: FlowNonApplicableException) -> HttpResponse:
|
||||
|
||||
@ -22,6 +22,8 @@ def migrate_search_group(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
LDAPProvider = apps.get_model("authentik_providers_ldap", "ldapprovider")
|
||||
|
||||
for provider in LDAPProvider.objects.using(db_alias).all():
|
||||
if not provider.search_group:
|
||||
continue
|
||||
for user_pk in (
|
||||
provider.search_group.users.using(db_alias).all().values_list("pk", flat=True)
|
||||
):
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -248,6 +248,7 @@ MIDDLEWARE = [
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"authentik.core.middleware.RequestIDMiddleware",
|
||||
"authentik.brands.middleware.BrandMiddleware",
|
||||
"authentik.brands.middleware.BrandCORSAPIMiddleware",
|
||||
"authentik.events.middleware.AuditMiddleware",
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
|
||||
@ -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()
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -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.3.0
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
|
||||
github.com/pires/go-proxyproto v0.7.0
|
||||
github.com/prometheus/client_golang v1.20.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.3.0 h1:BdoC9cE81qXfrxeb9eoJi9dWrdhSuwXMAnHTbnBm4Wc=
|
||||
github.com/jellydator/ttlcache/v3 v3.3.0/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"
|
||||
|
||||
@ -16,7 +16,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-08-15 00:09+0000\n"
|
||||
"POT-Creation-Date: 2024-08-18 00:08+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: Anton Babenko, 2024\n"
|
||||
"Language-Team: Russian (https://app.transifex.com/authentik/teams/119923/ru/)\n"
|
||||
@ -739,7 +739,7 @@ msgstr "Правило Уведомления"
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid "Notification Rules"
|
||||
msgstr "Правило Уведомлений"
|
||||
msgstr "Правила уведомлений"
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid "Webhook Mapping"
|
||||
@ -1771,6 +1771,14 @@ msgstr "Сопоставление свойства Radius провайдера"
|
||||
msgid "Radius Provider Property Mappings"
|
||||
msgstr "Сопоставление свойств Radius провайдера"
|
||||
|
||||
#: authentik/providers/saml/api/providers.py
|
||||
msgid ""
|
||||
"With a signing keypair selected, at least one of 'Sign assertion' and 'Sign "
|
||||
"Response' must be selected."
|
||||
msgstr ""
|
||||
"При выборе пары ключей для подписи необходимо выбрать как минимум один из "
|
||||
"вариантов: 'Подписывать утверждение' или 'Подписывать ответ'."
|
||||
|
||||
#: authentik/providers/saml/api/providers.py
|
||||
msgid "Invalid XML Syntax"
|
||||
msgstr "Некорректный синтаксис XML"
|
||||
@ -1918,6 +1926,21 @@ msgstr ""
|
||||
msgid "Signing Keypair"
|
||||
msgstr "Пара ключей для подписи"
|
||||
|
||||
#: authentik/providers/saml/models.py authentik/sources/saml/models.py
|
||||
msgid ""
|
||||
"When selected, incoming assertions are encrypted by the IdP using the public"
|
||||
" key of the encryption keypair. The assertion is decrypted by the SP using "
|
||||
"the the private key."
|
||||
msgstr ""
|
||||
"При выборе этого варианта, входящие утверждения шифруются поставщиком "
|
||||
"идентификации (IdP) с использованием открытого ключа из пары ключей "
|
||||
"шифрования. Утверждение расшифровывается поставщиком услуг (SP) с "
|
||||
"использованием закрытого ключа."
|
||||
|
||||
#: authentik/providers/saml/models.py authentik/sources/saml/models.py
|
||||
msgid "Encryption Keypair"
|
||||
msgstr "Пара ключей шифрования"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "Default relay_state value for IDP-initiated logins"
|
||||
msgstr "Значение relay_state по умолчанию для логинов, инициированных IDP"
|
||||
@ -2446,21 +2469,6 @@ msgstr ""
|
||||
"Пара ключей, используемая для подписи исходящих ответов, направляемых "
|
||||
"провайдеру идентификационных данных."
|
||||
|
||||
#: authentik/sources/saml/models.py
|
||||
msgid ""
|
||||
"When selected, incoming assertions are encrypted by the IdP using the public"
|
||||
" key of the encryption keypair. The assertion is decrypted by the SP using "
|
||||
"the the private key."
|
||||
msgstr ""
|
||||
"При выборе этого варианта, входящие утверждения шифруются поставщиком "
|
||||
"идентификации (IdP) с использованием открытого ключа из пары ключей "
|
||||
"шифрования. Утверждение расшифровывается поставщиком услуг (SP) с "
|
||||
"использованием закрытого ключа."
|
||||
|
||||
#: authentik/sources/saml/models.py
|
||||
msgid "Encryption Keypair"
|
||||
msgstr "Пара ключей шифрования"
|
||||
|
||||
#: authentik/sources/saml/models.py
|
||||
msgid "SAML Source"
|
||||
msgstr "Источник SAML"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2024.6.4",
|
||||
"version": "2024.8.0",
|
||||
"private": true
|
||||
}
|
||||
|
||||
241
poetry.lock
generated
241
poetry.lock
generated
@ -1165,15 +1165,18 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "deepmerge"
|
||||
version = "1.1.1"
|
||||
description = "a toolset to deeply merge python dictionaries."
|
||||
version = "2.0"
|
||||
description = "A toolset for deeply merging Python dictionaries."
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "deepmerge-1.1.1-py3-none-any.whl", hash = "sha256:7219dad9763f15be9dcd4bcb53e00f48e4eed6f5ed8f15824223eb934bb35977"},
|
||||
{file = "deepmerge-1.1.1.tar.gz", hash = "sha256:53a489dc9449636e480a784359ae2aab3191748c920649551c8e378622f0eca4"},
|
||||
{file = "deepmerge-2.0-py3-none-any.whl", hash = "sha256:6de9ce507115cff0bed95ff0ce9ecc31088ef50cbdf09bc90a09349a318b3d00"},
|
||||
{file = "deepmerge-2.0.tar.gz", hash = "sha256:5c3d86081fbebd04dd5de03626a0607b809a98fb6ccba5770b62466fe940ff20"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["black", "build", "mypy", "pytest", "pyupgrade", "twine", "validate-pyproject[all]"]
|
||||
|
||||
[[package]]
|
||||
name = "defusedxml"
|
||||
version = "0.7.1"
|
||||
@ -1312,17 +1315,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]]
|
||||
@ -1758,13 +1761,13 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"]
|
||||
|
||||
[[package]]
|
||||
name = "google-api-python-client"
|
||||
version = "2.142.0"
|
||||
version = "2.143.0"
|
||||
description = "Google API Client Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "google_api_python_client-2.142.0-py2.py3-none-any.whl", hash = "sha256:266799082bb8301f423ec204dffbffb470b502abbf29efd1f83e644d36eb5a8f"},
|
||||
{file = "google_api_python_client-2.142.0.tar.gz", hash = "sha256:a1101ac9e24356557ca22f07ff48b7f61fa5d4b4e7feeef3bda16e5dcb86350e"},
|
||||
{file = "google_api_python_client-2.143.0-py2.py3-none-any.whl", hash = "sha256:d5654134522b9b574b82234e96f7e0aeeabcbf33643fbabcd449ef0068e3a476"},
|
||||
{file = "google_api_python_client-2.143.0.tar.gz", hash = "sha256:6a75441f9078e6e2fcdf4946a153fda1e2cc81b5e9c8d6e8c0750c85c7f8a566"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -2047,13 +2050,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "importlib-metadata"
|
||||
version = "8.0.0"
|
||||
version = "8.4.0"
|
||||
description = "Read metadata from Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "importlib_metadata-8.0.0-py3-none-any.whl", hash = "sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f"},
|
||||
{file = "importlib_metadata-8.0.0.tar.gz", hash = "sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812"},
|
||||
{file = "importlib_metadata-8.4.0-py3-none-any.whl", hash = "sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1"},
|
||||
{file = "importlib_metadata-8.4.0.tar.gz", hash = "sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -3025,49 +3028,49 @@ resolved_reference = "20d69d9cc50a0fef31605b46f06da0c94f1ec3cf"
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-api"
|
||||
version = "1.26.0"
|
||||
version = "1.27.0"
|
||||
description = "OpenTelemetry Python API"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "opentelemetry_api-1.26.0-py3-none-any.whl", hash = "sha256:7d7ea33adf2ceda2dd680b18b1677e4152000b37ca76e679da71ff103b943064"},
|
||||
{file = "opentelemetry_api-1.26.0.tar.gz", hash = "sha256:2bd639e4bed5b18486fef0b5a520aaffde5a18fc225e808a1ac4df363f43a1ce"},
|
||||
{file = "opentelemetry_api-1.27.0-py3-none-any.whl", hash = "sha256:953d5871815e7c30c81b56d910c707588000fff7a3ca1c73e6531911d53065e7"},
|
||||
{file = "opentelemetry_api-1.27.0.tar.gz", hash = "sha256:ed673583eaa5f81b5ce5e86ef7cdaf622f88ef65f0b9aab40b843dcae5bef342"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
deprecated = ">=1.2.6"
|
||||
importlib-metadata = ">=6.0,<=8.0.0"
|
||||
importlib-metadata = ">=6.0,<=8.4.0"
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-sdk"
|
||||
version = "1.26.0"
|
||||
version = "1.27.0"
|
||||
description = "OpenTelemetry Python SDK"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "opentelemetry_sdk-1.26.0-py3-none-any.whl", hash = "sha256:feb5056a84a88670c041ea0ded9921fca559efec03905dddeb3885525e0af897"},
|
||||
{file = "opentelemetry_sdk-1.26.0.tar.gz", hash = "sha256:c90d2868f8805619535c05562d699e2f4fb1f00dbd55a86dcefca4da6fa02f85"},
|
||||
{file = "opentelemetry_sdk-1.27.0-py3-none-any.whl", hash = "sha256:365f5e32f920faf0fd9e14fdfd92c086e317eaa5f860edba9cdc17a380d9197d"},
|
||||
{file = "opentelemetry_sdk-1.27.0.tar.gz", hash = "sha256:d525017dea0ccce9ba4e0245100ec46ecdc043f2d7b8315d56b19aff0904fa6f"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
opentelemetry-api = "1.26.0"
|
||||
opentelemetry-semantic-conventions = "0.47b0"
|
||||
opentelemetry-api = "1.27.0"
|
||||
opentelemetry-semantic-conventions = "0.48b0"
|
||||
typing-extensions = ">=3.7.4"
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-semantic-conventions"
|
||||
version = "0.47b0"
|
||||
version = "0.48b0"
|
||||
description = "OpenTelemetry Semantic Conventions"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "opentelemetry_semantic_conventions-0.47b0-py3-none-any.whl", hash = "sha256:4ff9d595b85a59c1c1413f02bba320ce7ea6bf9e2ead2b0913c4395c7bbc1063"},
|
||||
{file = "opentelemetry_semantic_conventions-0.47b0.tar.gz", hash = "sha256:a8d57999bbe3495ffd4d510de26a97dadc1dace53e0275001b2c1b2f67992a7e"},
|
||||
{file = "opentelemetry_semantic_conventions-0.48b0-py3-none-any.whl", hash = "sha256:a0de9f45c413a8669788a38569c7e0a11ce6ce97861a628cca785deecdc32a1f"},
|
||||
{file = "opentelemetry_semantic_conventions-0.48b0.tar.gz", hash = "sha256:12d74983783b6878162208be57c9effcb89dc88691c64992d70bb89dc00daa1a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
deprecated = ">=1.2.6"
|
||||
opentelemetry-api = "1.26.0"
|
||||
opentelemetry-api = "1.27.0"
|
||||
|
||||
[[package]]
|
||||
name = "orjson"
|
||||
@ -3201,13 +3204,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "pdoc"
|
||||
version = "14.6.0"
|
||||
version = "14.6.1"
|
||||
description = "API Documentation for Python Projects"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pdoc-14.6.0-py3-none-any.whl", hash = "sha256:36c42c546a317d8e3e8c0b39645f24161374de0c7066ccaae76628d721e49ba5"},
|
||||
{file = "pdoc-14.6.0.tar.gz", hash = "sha256:6e98a24c5e0ca5d188397969cf82581836eaef13f172fc3820047bfe15c61c9a"},
|
||||
{file = "pdoc-14.6.1-py3-none-any.whl", hash = "sha256:efbed433655264392c60551615a3d42b8f21e492373419756d20234c667b54bc"},
|
||||
{file = "pdoc-14.6.1.tar.gz", hash = "sha256:ee598f30d5c55dd4702086dabc412a26022acc35aa88aa382cda8ac655fead98"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -3781,13 +3784,13 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments
|
||||
|
||||
[[package]]
|
||||
name = "pytest-django"
|
||||
version = "4.8.0"
|
||||
version = "4.9.0"
|
||||
description = "A Django plugin for pytest."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pytest-django-4.8.0.tar.gz", hash = "sha256:5d054fe011c56f3b10f978f41a8efb2e5adfc7e680ef36fb571ada1f24779d90"},
|
||||
{file = "pytest_django-4.8.0-py3-none-any.whl", hash = "sha256:ca1ddd1e0e4c227cf9e3e40a6afc6d106b3e70868fd2ac5798a22501271cd0c7"},
|
||||
{file = "pytest_django-4.9.0-py3-none-any.whl", hash = "sha256:1d83692cb39188682dbb419ff0393867e9904094a549a7d38a3154d5731b2b99"},
|
||||
{file = "pytest_django-4.9.0.tar.gz", hash = "sha256:8bf7bc358c9ae6f6fc51b6cebb190fe20212196e6807121f11bd6a3b03428314"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -4195,29 +4198,29 @@ pyasn1 = ">=0.1.3"
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.6.2"
|
||||
version = "0.6.3"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"},
|
||||
{file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"},
|
||||
{file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"},
|
||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"},
|
||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"},
|
||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"},
|
||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"},
|
||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"},
|
||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"},
|
||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"},
|
||||
{file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"},
|
||||
{file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"},
|
||||
{file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"},
|
||||
{file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"},
|
||||
{file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"},
|
||||
{file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"},
|
||||
{file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"},
|
||||
{file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"},
|
||||
{file = "ruff-0.6.3-py3-none-linux_armv6l.whl", hash = "sha256:97f58fda4e309382ad30ede7f30e2791d70dd29ea17f41970119f55bdb7a45c3"},
|
||||
{file = "ruff-0.6.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3b061e49b5cf3a297b4d1c27ac5587954ccb4ff601160d3d6b2f70b1622194dc"},
|
||||
{file = "ruff-0.6.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:34e2824a13bb8c668c71c1760a6ac7d795ccbd8d38ff4a0d8471fdb15de910b1"},
|
||||
{file = "ruff-0.6.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bddfbb8d63c460f4b4128b6a506e7052bad4d6f3ff607ebbb41b0aa19c2770d1"},
|
||||
{file = "ruff-0.6.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ced3eeb44df75353e08ab3b6a9e113b5f3f996bea48d4f7c027bc528ba87b672"},
|
||||
{file = "ruff-0.6.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47021dff5445d549be954eb275156dfd7c37222acc1e8014311badcb9b4ec8c1"},
|
||||
{file = "ruff-0.6.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d7bd20dc07cebd68cc8bc7b3f5ada6d637f42d947c85264f94b0d1cd9d87384"},
|
||||
{file = "ruff-0.6.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:500f166d03fc6d0e61c8e40a3ff853fa8a43d938f5d14c183c612df1b0d6c58a"},
|
||||
{file = "ruff-0.6.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42844ff678f9b976366b262fa2d1d1a3fe76f6e145bd92c84e27d172e3c34500"},
|
||||
{file = "ruff-0.6.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70452a10eb2d66549de8e75f89ae82462159855e983ddff91bc0bce6511d0470"},
|
||||
{file = "ruff-0.6.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:65a533235ed55f767d1fc62193a21cbf9e3329cf26d427b800fdeacfb77d296f"},
|
||||
{file = "ruff-0.6.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2e2c23cef30dc3cbe9cc5d04f2899e7f5e478c40d2e0a633513ad081f7361b5"},
|
||||
{file = "ruff-0.6.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d8a136aa7d228975a6aee3dd8bea9b28e2b43e9444aa678fb62aeb1956ff2351"},
|
||||
{file = "ruff-0.6.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f92fe93bc72e262b7b3f2bba9879897e2d58a989b4714ba6a5a7273e842ad2f8"},
|
||||
{file = "ruff-0.6.3-py3-none-win32.whl", hash = "sha256:7a62d3b5b0d7f9143d94893f8ba43aa5a5c51a0ffc4a401aa97a81ed76930521"},
|
||||
{file = "ruff-0.6.3-py3-none-win_amd64.whl", hash = "sha256:746af39356fee2b89aada06c7376e1aa274a23493d7016059c3a72e3b296befb"},
|
||||
{file = "ruff-0.6.3-py3-none-win_arm64.whl", hash = "sha256:14a9528a8b70ccc7a847637c29e56fd1f9183a9db743bbc5b8e0c4ad60592a82"},
|
||||
{file = "ruff-0.6.3.tar.gz", hash = "sha256:183b99e9edd1ef63be34a3b51fee0a9f4ab95add123dbf89a71f7b1f0c991983"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4256,13 +4259,13 @@ django-query = ["django (>=3.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "selenium"
|
||||
version = "4.23.1"
|
||||
version = "4.24.0"
|
||||
description = "Official Python bindings for Selenium WebDriver"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "selenium-4.23.1-py3-none-any.whl", hash = "sha256:3a8d9f23dc636bd3840dd56f00c2739e32ec0c1e34a821dd553e15babef24477"},
|
||||
{file = "selenium-4.23.1.tar.gz", hash = "sha256:128d099e66284437e7128d2279176ec7a06e6ec7426e167f5d34987166bd8f46"},
|
||||
{file = "selenium-4.24.0-py3-none-any.whl", hash = "sha256:42c23f60753d5415b261b236cecbd69bd4eb5271e1563915f546b443cb6b71c6"},
|
||||
{file = "selenium-4.24.0.tar.gz", hash = "sha256:88281e5b5b90fe231868905d5ea745b9ee5e30db280b33498cc73fb0fa06d571"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -4652,13 +4655,13 @@ wsproto = ">=0.14"
|
||||
|
||||
[[package]]
|
||||
name = "twilio"
|
||||
version = "9.2.3"
|
||||
version = "9.2.4"
|
||||
description = "Twilio API client and TwiML generator"
|
||||
optional = false
|
||||
python-versions = ">=3.7.0"
|
||||
files = [
|
||||
{file = "twilio-9.2.3-py2.py3-none-any.whl", hash = "sha256:76bfc39aa8d854510907cb7f9465814dfdea9e91ec199bb44f0785f05746f4cc"},
|
||||
{file = "twilio-9.2.3.tar.gz", hash = "sha256:da2255b5f3753cb3bf647fc6c50edbdb367ebc3cde6802806f6f863058a65f75"},
|
||||
{file = "twilio-9.2.4-py2.py3-none-any.whl", hash = "sha256:490da2518c0da370d738d436f9086b2463902707a811cd306ec8dcc8ce831758"},
|
||||
{file = "twilio-9.2.4.tar.gz", hash = "sha256:454b7d075c6bee3b64c81c39151be1f9105c695df6dbb0021b0c43e2930263e7"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -4669,13 +4672,13 @@ requests = ">=2.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "twisted"
|
||||
version = "24.3.0"
|
||||
version = "24.7.0"
|
||||
description = "An asynchronous networking framework written in Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8.0"
|
||||
files = [
|
||||
{file = "twisted-24.3.0-py3-none-any.whl", hash = "sha256:039f2e6a49ab5108abd94de187fa92377abe5985c7a72d68d0ad266ba19eae63"},
|
||||
{file = "twisted-24.3.0.tar.gz", hash = "sha256:6b38b6ece7296b5e122c9eb17da2eeab3d98a198f50ca9efd00fb03e5b4fd4ae"},
|
||||
{file = "twisted-24.7.0-py3-none-any.whl", hash = "sha256:734832ef98108136e222b5230075b1079dad8a3fc5637319615619a7725b0c81"},
|
||||
{file = "twisted-24.7.0.tar.gz", hash = "sha256:5a60147f044187a127ec7da96d170d49bcce50c6fd36f594e60f4587eff4d394"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -4684,55 +4687,26 @@ automat = ">=0.8.0"
|
||||
constantly = ">=15.1"
|
||||
hyperlink = ">=17.1.1"
|
||||
idna = {version = ">=2.4", optional = true, markers = "extra == \"tls\""}
|
||||
incremental = ">=22.10.0"
|
||||
incremental = ">=24.7.0"
|
||||
pyopenssl = {version = ">=21.0.0", optional = true, markers = "extra == \"tls\""}
|
||||
service-identity = {version = ">=18.1.0", optional = true, markers = "extra == \"tls\""}
|
||||
twisted-iocpsupport = {version = ">=1.0.2,<2", markers = "platform_system == \"Windows\""}
|
||||
typing-extensions = ">=4.2.0"
|
||||
zope-interface = ">=5"
|
||||
|
||||
[package.extras]
|
||||
all-non-platform = ["twisted[conch,http2,serial,test,tls]", "twisted[conch,http2,serial,test,tls]"]
|
||||
all-non-platform = ["appdirs (>=1.4.0)", "appdirs (>=1.4.0)", "bcrypt (>=3.1.3)", "bcrypt (>=3.1.3)", "cryptography (>=3.3)", "cryptography (>=3.3)", "cython-test-exception-raiser (>=1.0.2,<2)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.0,<5.0)", "h2 (>=3.0,<5.0)", "hypothesis (>=6.56)", "hypothesis (>=6.56)", "idna (>=2.4)", "idna (>=2.4)", "priority (>=1.1.0,<2.0)", "priority (>=1.1.0,<2.0)", "pyhamcrest (>=2)", "pyhamcrest (>=2)", "pyopenssl (>=21.0.0)", "pyopenssl (>=21.0.0)", "pyserial (>=3.0)", "pyserial (>=3.0)", "pywin32 (!=226)", "pywin32 (!=226)", "service-identity (>=18.1.0)", "service-identity (>=18.1.0)"]
|
||||
conch = ["appdirs (>=1.4.0)", "bcrypt (>=3.1.3)", "cryptography (>=3.3)"]
|
||||
dev = ["coverage (>=6b1,<7)", "pyflakes (>=2.2,<3.0)", "python-subunit (>=1.4,<2.0)", "twisted[dev-release]", "twistedchecker (>=0.7,<1.0)"]
|
||||
dev = ["coverage (>=7.5,<8.0)", "cython-test-exception-raiser (>=1.0.2,<2)", "hypothesis (>=6.56)", "pydoctor (>=23.9.0,<23.10.0)", "pyflakes (>=2.2,<3.0)", "pyhamcrest (>=2)", "python-subunit (>=1.4,<2.0)", "sphinx (>=6,<7)", "sphinx-rtd-theme (>=1.3,<2.0)", "towncrier (>=23.6,<24.0)", "twistedchecker (>=0.7,<1.0)"]
|
||||
dev-release = ["pydoctor (>=23.9.0,<23.10.0)", "pydoctor (>=23.9.0,<23.10.0)", "sphinx (>=6,<7)", "sphinx (>=6,<7)", "sphinx-rtd-theme (>=1.3,<2.0)", "sphinx-rtd-theme (>=1.3,<2.0)", "towncrier (>=23.6,<24.0)", "towncrier (>=23.6,<24.0)"]
|
||||
gtk-platform = ["pygobject", "pygobject", "twisted[all-non-platform]", "twisted[all-non-platform]"]
|
||||
gtk-platform = ["appdirs (>=1.4.0)", "appdirs (>=1.4.0)", "bcrypt (>=3.1.3)", "bcrypt (>=3.1.3)", "cryptography (>=3.3)", "cryptography (>=3.3)", "cython-test-exception-raiser (>=1.0.2,<2)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.0,<5.0)", "h2 (>=3.0,<5.0)", "hypothesis (>=6.56)", "hypothesis (>=6.56)", "idna (>=2.4)", "idna (>=2.4)", "priority (>=1.1.0,<2.0)", "priority (>=1.1.0,<2.0)", "pygobject", "pygobject", "pyhamcrest (>=2)", "pyhamcrest (>=2)", "pyopenssl (>=21.0.0)", "pyopenssl (>=21.0.0)", "pyserial (>=3.0)", "pyserial (>=3.0)", "pywin32 (!=226)", "pywin32 (!=226)", "service-identity (>=18.1.0)", "service-identity (>=18.1.0)"]
|
||||
http2 = ["h2 (>=3.0,<5.0)", "priority (>=1.1.0,<2.0)"]
|
||||
macos-platform = ["pyobjc-core", "pyobjc-core", "pyobjc-framework-cfnetwork", "pyobjc-framework-cfnetwork", "pyobjc-framework-cocoa", "pyobjc-framework-cocoa", "twisted[all-non-platform]", "twisted[all-non-platform]"]
|
||||
mypy = ["mypy (>=1.8,<2.0)", "mypy-zope (>=1.0.3,<1.1.0)", "twisted[all-non-platform,dev]", "types-pyopenssl", "types-setuptools"]
|
||||
osx-platform = ["twisted[macos-platform]", "twisted[macos-platform]"]
|
||||
macos-platform = ["appdirs (>=1.4.0)", "appdirs (>=1.4.0)", "bcrypt (>=3.1.3)", "bcrypt (>=3.1.3)", "cryptography (>=3.3)", "cryptography (>=3.3)", "cython-test-exception-raiser (>=1.0.2,<2)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.0,<5.0)", "h2 (>=3.0,<5.0)", "hypothesis (>=6.56)", "hypothesis (>=6.56)", "idna (>=2.4)", "idna (>=2.4)", "priority (>=1.1.0,<2.0)", "priority (>=1.1.0,<2.0)", "pyhamcrest (>=2)", "pyhamcrest (>=2)", "pyobjc-core", "pyobjc-core", "pyobjc-framework-cfnetwork", "pyobjc-framework-cfnetwork", "pyobjc-framework-cocoa", "pyobjc-framework-cocoa", "pyopenssl (>=21.0.0)", "pyopenssl (>=21.0.0)", "pyserial (>=3.0)", "pyserial (>=3.0)", "pywin32 (!=226)", "pywin32 (!=226)", "service-identity (>=18.1.0)", "service-identity (>=18.1.0)"]
|
||||
mypy = ["appdirs (>=1.4.0)", "bcrypt (>=3.1.3)", "coverage (>=7.5,<8.0)", "cryptography (>=3.3)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.0,<5.0)", "hypothesis (>=6.56)", "idna (>=2.4)", "mypy (>=1.8,<2.0)", "mypy-zope (>=1.0.3,<1.1.0)", "priority (>=1.1.0,<2.0)", "pydoctor (>=23.9.0,<23.10.0)", "pyflakes (>=2.2,<3.0)", "pyhamcrest (>=2)", "pyopenssl (>=21.0.0)", "pyserial (>=3.0)", "python-subunit (>=1.4,<2.0)", "pywin32 (!=226)", "service-identity (>=18.1.0)", "sphinx (>=6,<7)", "sphinx-rtd-theme (>=1.3,<2.0)", "towncrier (>=23.6,<24.0)", "twistedchecker (>=0.7,<1.0)", "types-pyopenssl", "types-setuptools"]
|
||||
osx-platform = ["appdirs (>=1.4.0)", "appdirs (>=1.4.0)", "bcrypt (>=3.1.3)", "bcrypt (>=3.1.3)", "cryptography (>=3.3)", "cryptography (>=3.3)", "cython-test-exception-raiser (>=1.0.2,<2)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.0,<5.0)", "h2 (>=3.0,<5.0)", "hypothesis (>=6.56)", "hypothesis (>=6.56)", "idna (>=2.4)", "idna (>=2.4)", "priority (>=1.1.0,<2.0)", "priority (>=1.1.0,<2.0)", "pyhamcrest (>=2)", "pyhamcrest (>=2)", "pyobjc-core", "pyobjc-core", "pyobjc-framework-cfnetwork", "pyobjc-framework-cfnetwork", "pyobjc-framework-cocoa", "pyobjc-framework-cocoa", "pyopenssl (>=21.0.0)", "pyopenssl (>=21.0.0)", "pyserial (>=3.0)", "pyserial (>=3.0)", "pywin32 (!=226)", "pywin32 (!=226)", "service-identity (>=18.1.0)", "service-identity (>=18.1.0)"]
|
||||
serial = ["pyserial (>=3.0)", "pywin32 (!=226)"]
|
||||
test = ["cython-test-exception-raiser (>=1.0.2,<2)", "hypothesis (>=6.56)", "pyhamcrest (>=2)"]
|
||||
tls = ["idna (>=2.4)", "pyopenssl (>=21.0.0)", "service-identity (>=18.1.0)"]
|
||||
windows-platform = ["pywin32 (!=226)", "pywin32 (!=226)", "twisted[all-non-platform]", "twisted[all-non-platform]"]
|
||||
|
||||
[[package]]
|
||||
name = "twisted-iocpsupport"
|
||||
version = "1.0.4"
|
||||
description = "An extension for use in the twisted I/O Completion Ports reactor."
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "twisted-iocpsupport-1.0.4.tar.gz", hash = "sha256:858096c0d15e33f15ac157f455d8f86f2f2cdd223963e58c0f682a3af8362d89"},
|
||||
{file = "twisted_iocpsupport-1.0.4-cp310-cp310-win32.whl", hash = "sha256:afa2b630797f9ed2f27f3d9f55e3f72b4244911e45a8c82756f44babbf0b243e"},
|
||||
{file = "twisted_iocpsupport-1.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:0058c963c8957bcd3deda62122e89953c9de1e867a274facc9b15dde1a9f31e8"},
|
||||
{file = "twisted_iocpsupport-1.0.4-cp311-cp311-win32.whl", hash = "sha256:196f7c7ccad4ba4d1783b1c4e1d1b22d93c04275cd780bf7498d16c77319ad6e"},
|
||||
{file = "twisted_iocpsupport-1.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:4e5f97bcbabdd79cbaa969b63439b89801ea560f11d42b0a387634275c633623"},
|
||||
{file = "twisted_iocpsupport-1.0.4-cp312-cp312-win32.whl", hash = "sha256:6081bd7c2f4fcf9b383dcdb3b3385d75a26a7c9d2be25b6950c3d8ea652d2d2d"},
|
||||
{file = "twisted_iocpsupport-1.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:76f7e67cec1f1d097d1f4ed7de41be3d74546e1a4ede0c7d56e775c4dce5dfb0"},
|
||||
{file = "twisted_iocpsupport-1.0.4-cp36-cp36m-win32.whl", hash = "sha256:3d306fc4d88a6bcf61ce9d572c738b918578121bfd72891625fab314549024b5"},
|
||||
{file = "twisted_iocpsupport-1.0.4-cp36-cp36m-win_amd64.whl", hash = "sha256:391ac4d6002a80e15f35adc4ad6056f4fe1c17ceb0d1f98ba01b0f4f917adfd7"},
|
||||
{file = "twisted_iocpsupport-1.0.4-cp37-cp37m-win32.whl", hash = "sha256:0c1b5cf37f0b2d96cc3c9bc86fff16613b9f5d0ca565c96cf1f1fb8cfca4b81c"},
|
||||
{file = "twisted_iocpsupport-1.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:3c5dc11d72519e55f727320e3cee535feedfaee09c0f0765ed1ca7badff1ab3c"},
|
||||
{file = "twisted_iocpsupport-1.0.4-cp38-cp38-win32.whl", hash = "sha256:cc86c2ef598c15d824a243c2541c29459881c67fc3c0adb6efe2242f8f0ec3af"},
|
||||
{file = "twisted_iocpsupport-1.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:c27985e949b9b1a1fb4c20c71d315c10ea0f93fdf3ccdd4a8c158b5926edd8c8"},
|
||||
{file = "twisted_iocpsupport-1.0.4-cp39-cp39-win32.whl", hash = "sha256:e311dfcb470696e3c077249615893cada598e62fa7c4e4ca090167bd2b7d331f"},
|
||||
{file = "twisted_iocpsupport-1.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:4574eef1f3bb81501fb02f911298af3c02fe8179c31a33b361dd49180c3e644d"},
|
||||
{file = "twisted_iocpsupport-1.0.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:872747a3b64e2909aee59c803ccd0bceb9b75bf27915520ebd32d69687040fa2"},
|
||||
{file = "twisted_iocpsupport-1.0.4-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:c2712b778bacf1db434e3e065adfed3db300754186a29aecac1efae9ef4bcaff"},
|
||||
{file = "twisted_iocpsupport-1.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7c66fa0aa4236b27b3c61cb488662d85dae746a6d1c7b0d91cf7aae118445adf"},
|
||||
{file = "twisted_iocpsupport-1.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:300437af17396a945a58dcfffd77863303a8b6d9e65c6e81f1d2eed55b50d444"},
|
||||
]
|
||||
windows-platform = ["appdirs (>=1.4.0)", "appdirs (>=1.4.0)", "bcrypt (>=3.1.3)", "bcrypt (>=3.1.3)", "cryptography (>=3.3)", "cryptography (>=3.3)", "cython-test-exception-raiser (>=1.0.2,<2)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.0,<5.0)", "h2 (>=3.0,<5.0)", "hypothesis (>=6.56)", "hypothesis (>=6.56)", "idna (>=2.4)", "idna (>=2.4)", "priority (>=1.1.0,<2.0)", "priority (>=1.1.0,<2.0)", "pyhamcrest (>=2)", "pyhamcrest (>=2)", "pyopenssl (>=21.0.0)", "pyopenssl (>=21.0.0)", "pyserial (>=3.0)", "pyserial (>=3.0)", "pywin32 (!=226)", "pywin32 (!=226)", "pywin32 (!=226)", "pywin32 (!=226)", "service-identity (>=18.1.0)", "service-identity (>=18.1.0)", "twisted-iocpsupport (>=1.0.2)", "twisted-iocpsupport (>=1.0.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "txaio"
|
||||
@ -4896,46 +4870,41 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "watchdog"
|
||||
version = "4.0.2"
|
||||
version = "5.0.1"
|
||||
description = "Filesystem events monitoring"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "watchdog-4.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ede7f010f2239b97cc79e6cb3c249e72962404ae3865860855d5cbe708b0fd22"},
|
||||
{file = "watchdog-4.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a2cffa171445b0efa0726c561eca9a27d00a1f2b83846dbd5a4f639c4f8ca8e1"},
|
||||
{file = "watchdog-4.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c50f148b31b03fbadd6d0b5980e38b558046b127dc483e5e4505fcef250f9503"},
|
||||
{file = "watchdog-4.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7c7d4bf585ad501c5f6c980e7be9c4f15604c7cc150e942d82083b31a7548930"},
|
||||
{file = "watchdog-4.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:914285126ad0b6eb2258bbbcb7b288d9dfd655ae88fa28945be05a7b475a800b"},
|
||||
{file = "watchdog-4.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:984306dc4720da5498b16fc037b36ac443816125a3705dfde4fd90652d8028ef"},
|
||||
{file = "watchdog-4.0.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1cdcfd8142f604630deef34722d695fb455d04ab7cfe9963055df1fc69e6727a"},
|
||||
{file = "watchdog-4.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d7ab624ff2f663f98cd03c8b7eedc09375a911794dfea6bf2a359fcc266bff29"},
|
||||
{file = "watchdog-4.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:132937547a716027bd5714383dfc40dc66c26769f1ce8a72a859d6a48f371f3a"},
|
||||
{file = "watchdog-4.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd67c7df93eb58f360c43802acc945fa8da70c675b6fa37a241e17ca698ca49b"},
|
||||
{file = "watchdog-4.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcfd02377be80ef3b6bc4ce481ef3959640458d6feaae0bd43dd90a43da90a7d"},
|
||||
{file = "watchdog-4.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:980b71510f59c884d684b3663d46e7a14b457c9611c481e5cef08f4dd022eed7"},
|
||||
{file = "watchdog-4.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:aa160781cafff2719b663c8a506156e9289d111d80f3387cf3af49cedee1f040"},
|
||||
{file = "watchdog-4.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f6ee8dedd255087bc7fe82adf046f0b75479b989185fb0bdf9a98b612170eac7"},
|
||||
{file = "watchdog-4.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0b4359067d30d5b864e09c8597b112fe0a0a59321a0f331498b013fb097406b4"},
|
||||
{file = "watchdog-4.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:770eef5372f146997638d737c9a3c597a3b41037cfbc5c41538fc27c09c3a3f9"},
|
||||
{file = "watchdog-4.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eeea812f38536a0aa859972d50c76e37f4456474b02bd93674d1947cf1e39578"},
|
||||
{file = "watchdog-4.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b2c45f6e1e57ebb4687690c05bc3a2c1fb6ab260550c4290b8abb1335e0fd08b"},
|
||||
{file = "watchdog-4.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:10b6683df70d340ac3279eff0b2766813f00f35a1d37515d2c99959ada8f05fa"},
|
||||
{file = "watchdog-4.0.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:f7c739888c20f99824f7aa9d31ac8a97353e22d0c0e54703a547a218f6637eb3"},
|
||||
{file = "watchdog-4.0.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c100d09ac72a8a08ddbf0629ddfa0b8ee41740f9051429baa8e31bb903ad7508"},
|
||||
{file = "watchdog-4.0.2-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:f5315a8c8dd6dd9425b974515081fc0aadca1d1d61e078d2246509fd756141ee"},
|
||||
{file = "watchdog-4.0.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:2d468028a77b42cc685ed694a7a550a8d1771bb05193ba7b24006b8241a571a1"},
|
||||
{file = "watchdog-4.0.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f15edcae3830ff20e55d1f4e743e92970c847bcddc8b7509bcd172aa04de506e"},
|
||||
{file = "watchdog-4.0.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:936acba76d636f70db8f3c66e76aa6cb5136a936fc2a5088b9ce1c7a3508fc83"},
|
||||
{file = "watchdog-4.0.2-py3-none-manylinux2014_armv7l.whl", hash = "sha256:e252f8ca942a870f38cf785aef420285431311652d871409a64e2a0a52a2174c"},
|
||||
{file = "watchdog-4.0.2-py3-none-manylinux2014_i686.whl", hash = "sha256:0e83619a2d5d436a7e58a1aea957a3c1ccbf9782c43c0b4fed80580e5e4acd1a"},
|
||||
{file = "watchdog-4.0.2-py3-none-manylinux2014_ppc64.whl", hash = "sha256:88456d65f207b39f1981bf772e473799fcdc10801062c36fd5ad9f9d1d463a73"},
|
||||
{file = "watchdog-4.0.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:32be97f3b75693a93c683787a87a0dc8db98bb84701539954eef991fb35f5fbc"},
|
||||
{file = "watchdog-4.0.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:c82253cfc9be68e3e49282831afad2c1f6593af80c0daf1287f6a92657986757"},
|
||||
{file = "watchdog-4.0.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c0b14488bd336c5b1845cee83d3e631a1f8b4e9c5091ec539406e4a324f882d8"},
|
||||
{file = "watchdog-4.0.2-py3-none-win32.whl", hash = "sha256:0d8a7e523ef03757a5aa29f591437d64d0d894635f8a50f370fe37f913ce4e19"},
|
||||
{file = "watchdog-4.0.2-py3-none-win_amd64.whl", hash = "sha256:c344453ef3bf875a535b0488e3ad28e341adbd5a9ffb0f7d62cefacc8824ef2b"},
|
||||
{file = "watchdog-4.0.2-py3-none-win_ia64.whl", hash = "sha256:baececaa8edff42cd16558a639a9b0ddf425f93d892e8392a56bf904f5eff22c"},
|
||||
{file = "watchdog-4.0.2.tar.gz", hash = "sha256:b4dfbb6c49221be4535623ea4474a4d6ee0a9cef4a80b20c28db4d858b64e270"},
|
||||
{file = "watchdog-5.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a6b8c6c82ada78479a0df568d27d69aa07105aba9301ac66d1ae162645f4ba34"},
|
||||
{file = "watchdog-5.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1e8ca9b7f5f03d2f0556a43db1e9adf1e5af6adf52e0890f781324514b67a612"},
|
||||
{file = "watchdog-5.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c92812a358eabebe92b12b9290d16dc95c8003654658f6b2676c9a2103a73ceb"},
|
||||
{file = "watchdog-5.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a03a6ccb846ead406a25a0b702d0a6b88fdfa77becaf907cfcfce7737ebbda1f"},
|
||||
{file = "watchdog-5.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39f0de161a822402f0f00c68b82349a4d71c9814e749148ca2b083a25606dbf9"},
|
||||
{file = "watchdog-5.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5541a8765c4090decb4dba55d3dceb57724748a717ceaba8dc4f213edb0026e0"},
|
||||
{file = "watchdog-5.0.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e321f1561adea30e447130882efe451af519646178d04189d6ba91a8cd7d88a5"},
|
||||
{file = "watchdog-5.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c4ae0b3e95455fa9d959aa3b253c87845ad454ef188a4bf5a69cab287c131216"},
|
||||
{file = "watchdog-5.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b2d56425dfa0c1e6f8a510f21d3d54ef7fe50bbc29638943c2cb1394b7b49156"},
|
||||
{file = "watchdog-5.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:70e30116849f4ec52240eb1fad83d27e525eae179bfe1c09b3bf120163d731b6"},
|
||||
{file = "watchdog-5.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f66df2c152edf5a2fe472bb2f8a5d562165bcf6cf9686cee5d75e524c21ca895"},
|
||||
{file = "watchdog-5.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6bb68d9adb9c45f0dc1c2b12f4fb6eab0463a8f9741e371e4ede6769064e0785"},
|
||||
{file = "watchdog-5.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6fbb4dd5ace074a2969825fde10034b35b31efcb6973defb22eb945b1d3acc37"},
|
||||
{file = "watchdog-5.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:753c6a4c1eea9d3b96cd58159b49103e66cb288216a414ab9ad234ccc7642ec2"},
|
||||
{file = "watchdog-5.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:20a28c8b0b3edf4ea2b27fb3527fc0a348e983f22a4317d316bb561524391932"},
|
||||
{file = "watchdog-5.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a1cd7c919940b15f253db8279a579fb81e4e4e434b39b11a1cb7f54fe3fa46a6"},
|
||||
{file = "watchdog-5.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a791dfc050ed24b82f7f100ae794192594fe863a7e9bdafcdfa5c6e405a981e5"},
|
||||
{file = "watchdog-5.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8ba1472b5fa7c644e49641f70d7ccc567f70b54d776defa5d6f755dc2edc3fbb"},
|
||||
{file = "watchdog-5.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b21e6601efe8453514c2fc21aca57fb5413c3d8b157bfe520b05b57b1788a167"},
|
||||
{file = "watchdog-5.0.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:763c6f82bb65504b47d4aea268462b2fb662676676356e04787f332a11f03eb0"},
|
||||
{file = "watchdog-5.0.1-py3-none-manylinux2014_armv7l.whl", hash = "sha256:664917cd513538728875a42d5654584b533da88cf06680452c98e73b45466968"},
|
||||
{file = "watchdog-5.0.1-py3-none-manylinux2014_i686.whl", hash = "sha256:39e828c4270452b966bc9d814911a3c7e24c62d726d2a3245f5841664ff56b5e"},
|
||||
{file = "watchdog-5.0.1-py3-none-manylinux2014_ppc64.whl", hash = "sha256:59ec6111f3750772badae3403ef17263489ed6f27ac01ec50c0244b2afa258fb"},
|
||||
{file = "watchdog-5.0.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:f3006361dba2005552cc8aa49c44d16a10e0a1939bb3286e888a14f722122808"},
|
||||
{file = "watchdog-5.0.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:72dbdffe4aa0c36c59f4a5190bceeb7fdfdf849ab98a562b3a783a64cc6dacdd"},
|
||||
{file = "watchdog-5.0.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c93aa24899cb4e8a51492c7ccc420bea45ced502fe9ef2e83f9ab1107e5a13b5"},
|
||||
{file = "watchdog-5.0.1-py3-none-win32.whl", hash = "sha256:2b8cd627b76194e725ed6f48d9524b1ad93a51a0dc3bd0225c56023716245091"},
|
||||
{file = "watchdog-5.0.1-py3-none-win_amd64.whl", hash = "sha256:4eaebff2f938f5325788cef26521891b2d8ecc8e7852aa123a9b458815f93875"},
|
||||
{file = "watchdog-5.0.1-py3-none-win_ia64.whl", hash = "sha256:9b1b32f89f95162f09aea6e15d9384f6e0490152f10d7ed241f8a85cddc50658"},
|
||||
{file = "watchdog-5.0.1.tar.gz", hash = "sha256:f0180e84e6493ef7c82e051334e8c9b00ffd89fa9de5e0613d3c267f6ccf2d38"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
|
||||
@ -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.
|
||||
|
||||
4270
tests/wdio/package-lock.json
generated
4270
tests/wdio/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,9 +1,13 @@
|
||||
{
|
||||
"name": "@goauthentik/web-tests",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"chromedriver": "^128.0.1",
|
||||
"lockfile-lint": "^4.14.0",
|
||||
"syncpack": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"@types/mocha": "^10.0.7",
|
||||
"@typescript-eslint/eslint-plugin": "^7.17.0",
|
||||
"@typescript-eslint/parser": "^7.17.0",
|
||||
"@wdio/cli": "^9.0.3",
|
||||
@ -19,19 +23,20 @@
|
||||
"typescript": "^5.5.4",
|
||||
"wdio-wait-for": "^3.0.11"
|
||||
},
|
||||
"scripts": {
|
||||
"wdio": "wdio run ./wdio.conf.ts",
|
||||
"lint:precommit": "eslint --max-warnings 0 --config ./.eslintrc.precommit.json $(git status --porcelain . | grep '^[AM?][M?]' | cut -d'/' -f3- | grep -E '\\.(ts|js|tsx|jsx)$')",
|
||||
"lint": "eslint . --max-warnings 0 --fix",
|
||||
"lint:spelling": "codespell -D - -D $(git rev-parse --show-toplevel 2> /dev/null)/.github/codespell-dictionary.txt -I $(git rev-parse --show-toplevel 2> /dev/null)/.github/codespell-words.txt ./test -s",
|
||||
"precommit": "run-s lint:precommit lint:spelling prettier",
|
||||
"prettier-check": "prettier --check .",
|
||||
"prettier": "prettier --write ."
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"dependencies": {
|
||||
"chromedriver": "^128.0.0"
|
||||
}
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"lint": "eslint . --max-warnings 0 --fix",
|
||||
"lint:lockfile": "lockfile-lint --path package.json --type npm --allowed-hosts npm --validate-https",
|
||||
"lint:package": "syncpack format -i ' '",
|
||||
"lint:precommit": "eslint --max-warnings 0 --config ./.eslintrc.precommit.json $(git status --porcelain . | grep '^[AM?][M?]' | cut -d'/' -f3- | grep -E '\\.(ts|js|tsx|jsx)$')",
|
||||
"lint:spelling": "codespell -D - -D $(git rev-parse --show-toplevel 2> /dev/null)/.github/codespell-dictionary.txt -I $(git rev-parse --show-toplevel 2> /dev/null)/.github/codespell-words.txt ./test -s",
|
||||
"precommit": "run-s lint:precommit lint:spelling prettier",
|
||||
"prettier": "prettier --write .",
|
||||
"prettier-check": "prettier --check .",
|
||||
"wdio": "wdio run ./wdio.conf.ts"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
|
||||
@ -1,25 +1,11 @@
|
||||
import Page from "../pageobjects/page.js";
|
||||
import { browser } from "@wdio/globals";
|
||||
|
||||
const CLICK_TIME_DELAY = 250;
|
||||
|
||||
export default class AdminPage extends Page {
|
||||
public get pageHeader() {
|
||||
return $('>>>ak-page-header slot[name="header"]');
|
||||
public async pageHeader() {
|
||||
return await $("ak-page-header").$('slot[name="header"]');
|
||||
}
|
||||
|
||||
async openApplicationsListPage() {
|
||||
await this.open("if/admin/#/core/applications");
|
||||
}
|
||||
|
||||
public open(path: string) {
|
||||
return browser.url(`http://localhost:9000/${path}`);
|
||||
}
|
||||
|
||||
public pause(selector?: string) {
|
||||
if (selector) {
|
||||
return $(selector).waitForDisplayed();
|
||||
}
|
||||
return browser.pause(CLICK_TIME_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,24 +27,24 @@ class ApplicationWizardView extends AdminPage {
|
||||
radius = RadiusForm;
|
||||
app = ApplicationForm;
|
||||
|
||||
get wizardTitle() {
|
||||
return $(">>>ak-wizard-frame .pf-c-wizard__header h1.pf-c-title");
|
||||
async wizardTitle() {
|
||||
return await $("ak-wizard-frame").$(".pf-c-wizard__title");
|
||||
}
|
||||
|
||||
get providerList() {
|
||||
return $(">>>ak-application-wizard-authentication-method-choice");
|
||||
async providerList() {
|
||||
return await $("ak-application-wizard-authentication-method-choice");
|
||||
}
|
||||
|
||||
get nextButton() {
|
||||
return $(">>>ak-wizard-frame footer button.pf-m-primary");
|
||||
async nextButton() {
|
||||
return await $("ak-wizard-frame").$("footer button.pf-m-primary");
|
||||
}
|
||||
|
||||
async getProviderType(type: string) {
|
||||
return await this.providerList.$(`>>>input[value="${type}"]`);
|
||||
return await this.providerList().$(`input[value="${type}"]`);
|
||||
}
|
||||
|
||||
get successMessage() {
|
||||
return $('>>>[data-commit-state="success"]');
|
||||
async successMessage() {
|
||||
return await $('[data-commit-state="success"]');
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,8 +65,10 @@ const providerValues: Pair[] = [
|
||||
providerValues.forEach(([value, name]: Pair) => {
|
||||
Object.defineProperties(ApplicationWizardView.prototype, {
|
||||
[name]: {
|
||||
get: function () {
|
||||
return this.providerList.$(`>>>input[value="${value}"]`);
|
||||
get: async function () {
|
||||
return await (
|
||||
await this.providerList()
|
||||
).$(`div[data-ouid-component-name="${value}"]`);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -9,8 +9,8 @@ class ApplicationsListPage extends AdminPage {
|
||||
* define selectors using getter methods
|
||||
*/
|
||||
|
||||
get startWizardButton() {
|
||||
return $('>>>ak-wizard-frame button[slot="trigger"]');
|
||||
async startWizardButton() {
|
||||
return await $("ak-application-wizard").$('button[slot="trigger"]');
|
||||
}
|
||||
|
||||
async open() {
|
||||
|
||||
@ -2,16 +2,16 @@ import Page from "../page.js";
|
||||
import { $ } from "@wdio/globals";
|
||||
|
||||
export class ApplicationForm extends Page {
|
||||
get name() {
|
||||
return $('>>>ak-form-element-horizontal input[name="name"]');
|
||||
async name() {
|
||||
return await $('ak-text-input[name="name"]').$("input");
|
||||
}
|
||||
|
||||
get uiSettings() {
|
||||
return $('>>>ak-form-group button[aria-label="UI Settings"]');
|
||||
async uiSettings() {
|
||||
return await $("ak-form-group").$('button[aria-label="UI Settings"]');
|
||||
}
|
||||
|
||||
get launchUrl() {
|
||||
return $('>>>input[name="metaLaunchUrl"]');
|
||||
async launchUrl() {
|
||||
return await $('input[name="metaLaunchUrl"]');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,14 +4,14 @@ import { $ } from "@wdio/globals";
|
||||
export class ForwardProxyForm extends Page {
|
||||
async setAuthorizationFlow(selector: string) {
|
||||
await this.searchSelect(
|
||||
'>>>ak-flow-search[name="authorizationFlow"] input[type="text"]',
|
||||
'ak-flow-search[name="authorizationFlow"]',
|
||||
"authorizationFlow",
|
||||
`button*=${selector}`,
|
||||
selector,
|
||||
);
|
||||
}
|
||||
|
||||
get externalHost() {
|
||||
return $('>>>input[name="externalHost"]');
|
||||
return $('input[name="externalHost"]');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import Page from "../page.js";
|
||||
|
||||
export class LdapForm extends Page {
|
||||
async setBindFlow(selector: string) {
|
||||
async setBindFlow(_selector: string) {
|
||||
await this.searchSelect(
|
||||
'>>>ak-branded-flow-search[name="authorizationFlow"] input[type="text"]',
|
||||
'ak-search-select-view[name="authorizationFlow"]',
|
||||
"authorizationFlow",
|
||||
`button*=${selector}`,
|
||||
"default-authentication-flow",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,14 +4,14 @@ import { $ } from "@wdio/globals";
|
||||
export class OauthForm extends Page {
|
||||
async setAuthorizationFlow(selector: string) {
|
||||
await this.searchSelect(
|
||||
'>>>ak-flow-search[name="authorizationFlow"] input[type="text"]',
|
||||
'ak-flow-search[name="authorizationFlow"]',
|
||||
"authorizationFlow",
|
||||
`button*=${selector}`,
|
||||
`${selector}`,
|
||||
);
|
||||
}
|
||||
|
||||
get providerName() {
|
||||
return $('>>>ak-form-element-horizontal[name="name"] input');
|
||||
async providerName() {
|
||||
return await $('ak-form-element-horizontal[name="name"]').$("input");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -3,9 +3,9 @@ import Page from "../page.js";
|
||||
export class RadiusForm extends Page {
|
||||
async setAuthenticationFlow(selector: string) {
|
||||
await this.searchSelect(
|
||||
'>>>ak-branded-flow-search[name="authorizationFlow"] input[type="text"]',
|
||||
'ak-branded-flow-search[name="authorizationFlow"]',
|
||||
"authorizationFlow",
|
||||
`button*=${selector}`,
|
||||
selector,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,14 +4,14 @@ import { $ } from "@wdio/globals";
|
||||
export class SamlForm extends Page {
|
||||
async setAuthorizationFlow(selector: string) {
|
||||
await this.searchSelect(
|
||||
'>>>ak-flow-search[name="authorizationFlow"] input[type="text"]',
|
||||
'ak-flow-search[name="authorizationFlow"]',
|
||||
"authorizationFlow",
|
||||
`button*=${selector}`,
|
||||
selector,
|
||||
);
|
||||
}
|
||||
|
||||
get acsUrl() {
|
||||
return $('>>>input[name="acsUrl"]');
|
||||
return $('input[name="acsUrl"]');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,11 +2,11 @@ import Page from "../page.js";
|
||||
|
||||
export class ScimForm extends Page {
|
||||
get url() {
|
||||
return $('>>>input[name="url"]');
|
||||
return $('input[name="url"]');
|
||||
}
|
||||
|
||||
get token() {
|
||||
return $('>>>input[name="token"]');
|
||||
return $('input[name="token"]');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,18 +4,18 @@ import { $ } from "@wdio/globals";
|
||||
export class TransparentProxyForm extends Page {
|
||||
async setAuthorizationFlow(selector: string) {
|
||||
await this.searchSelect(
|
||||
'>>>ak-flow-search[name="authorizationFlow"] input[type="text"]',
|
||||
'ak-flow-search[name="authorizationFlow"]',
|
||||
"authorizationFlow",
|
||||
`button*=${selector}`,
|
||||
selector,
|
||||
);
|
||||
}
|
||||
|
||||
get externalHost() {
|
||||
return $('>>>input[name="externalHost"]');
|
||||
return $('input[name="externalHost"]');
|
||||
}
|
||||
|
||||
get internalHost() {
|
||||
return $('>>>input[name="internalHost"]');
|
||||
return $('input[name="internalHost"]');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -9,20 +9,20 @@ class LoginPage extends Page {
|
||||
/**
|
||||
* Selectors
|
||||
*/
|
||||
get inputUsername() {
|
||||
return $('>>>input[name="uidField"]');
|
||||
async inputUsername() {
|
||||
return await $('input[name="uidField"]');
|
||||
}
|
||||
|
||||
get inputPassword() {
|
||||
return $('>>>input[name="password"]');
|
||||
async inputPassword() {
|
||||
return await $('input[name="password"]');
|
||||
}
|
||||
|
||||
get btnSubmit() {
|
||||
return $('>>>button[type="submit"]');
|
||||
async btnSubmit() {
|
||||
return await $('button[type="submit"]');
|
||||
}
|
||||
|
||||
get authFailure() {
|
||||
return $(">>>h4.pf-c-alert__title");
|
||||
async authFailure() {
|
||||
return await $(".pf-m-error");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -30,17 +30,15 @@ class LoginPage extends Page {
|
||||
*/
|
||||
|
||||
async username(username: string) {
|
||||
await this.inputUsername.waitForClickable();
|
||||
await this.inputUsername.setValue(username);
|
||||
await this.btnSubmit.waitForEnabled();
|
||||
await this.btnSubmit.click();
|
||||
await (await this.inputUsername()).setValue(username);
|
||||
await (await this.btnSubmit()).waitForEnabled();
|
||||
await (await this.btnSubmit()).click();
|
||||
}
|
||||
|
||||
async password(password: string) {
|
||||
await this.inputPassword.waitForClickable();
|
||||
await this.inputPassword.setValue(password);
|
||||
await this.btnSubmit.waitForEnabled();
|
||||
await this.btnSubmit.click();
|
||||
await (await this.inputPassword()).setValue(password);
|
||||
await (await this.btnSubmit()).waitForEnabled();
|
||||
await (await this.btnSubmit()).click();
|
||||
}
|
||||
|
||||
async login(username: string, password: string) {
|
||||
@ -48,7 +46,7 @@ class LoginPage extends Page {
|
||||
await this.pause();
|
||||
await this.password(password);
|
||||
await this.pause();
|
||||
await this.pause(">>>div.header h1");
|
||||
await this.pause("div.header h1");
|
||||
return UserLibraryPage;
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { browser } from "@wdio/globals";
|
||||
import { Key } from "webdriverio";
|
||||
|
||||
const CLICK_TIME_DELAY = 250;
|
||||
|
||||
@ -11,15 +12,15 @@ export default class Page {
|
||||
* Opens a sub page of the page
|
||||
* @param path path of the sub page (e.g. /path/to/page.html)
|
||||
*/
|
||||
public open(path: string) {
|
||||
return browser.url(`http://localhost:9000/${path}`);
|
||||
public async open(path: string) {
|
||||
return await browser.url(`http://localhost:9000/${path}`);
|
||||
}
|
||||
|
||||
public pause(selector?: string) {
|
||||
public async pause(selector?: string) {
|
||||
if (selector) {
|
||||
return $(selector).waitForDisplayed();
|
||||
return await $(selector).waitForDisplayed();
|
||||
}
|
||||
return browser.pause(CLICK_TIME_DELAY);
|
||||
return await browser.pause(CLICK_TIME_DELAY);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -33,10 +34,20 @@ export default class Page {
|
||||
|
||||
async searchSelect(searchSelector: string, managedSelector: string, buttonSelector: string) {
|
||||
const inputBind = await $(searchSelector);
|
||||
await inputBind.click();
|
||||
const searchBlock = await $(`>>>div[data-managed-for="${managedSelector}"]`);
|
||||
const target = searchBlock.$(buttonSelector);
|
||||
return await target.click();
|
||||
const inputMain = await inputBind.$('input[type="text"]');
|
||||
await inputMain.click();
|
||||
const searchBlock = await (
|
||||
await $(`div[data-managed-for="${managedSelector}"]`).$("ak-list-select")
|
||||
).shadow$$("button");
|
||||
let target: WebdriverIO.Element;
|
||||
for (const button of searchBlock) {
|
||||
if ((await button.getText()).includes(buttonSelector)) {
|
||||
target = button;
|
||||
break;
|
||||
}
|
||||
}
|
||||
await (await target).click();
|
||||
await browser.keys(Key.Tab);
|
||||
}
|
||||
|
||||
public async logout() {
|
||||
|
||||
@ -9,13 +9,13 @@ class UserLibraryPage extends Page {
|
||||
* define selectors using getter methods
|
||||
*/
|
||||
|
||||
public get pageHeader() {
|
||||
return $('>>>h1[aria-level="1"]');
|
||||
public async pageHeader() {
|
||||
return await $('h1[aria-level="1"]');
|
||||
}
|
||||
|
||||
public async goToAdmin() {
|
||||
await $('>>>a[href="/if/admin"]').click();
|
||||
await $(">>>ak-admin-overview").waitForDisplayed();
|
||||
await $('a[href="/if/admin"]').click();
|
||||
return await $("ak-admin-overview").waitForDisplayed();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
15
tests/wdio/test/specs/bad-logins-2.ts
Normal file
15
tests/wdio/test/specs/bad-logins-2.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import LoginPage from "../pageobjects/login.page.js";
|
||||
import { BAD_PASSWORD, GOOD_USERNAME } from "../utils/constants.js";
|
||||
import { expect } from "@wdio/globals";
|
||||
|
||||
describe("Log into authentik", () => {
|
||||
it("should fail on a bad password", async () => {
|
||||
await LoginPage.open();
|
||||
await LoginPage.username(GOOD_USERNAME);
|
||||
await LoginPage.pause();
|
||||
await LoginPage.password(BAD_PASSWORD);
|
||||
const failure = await LoginPage.authFailure();
|
||||
await expect(failure).toBeDisplayedInViewport();
|
||||
await expect(failure).toHaveText("Invalid password");
|
||||
});
|
||||
});
|
||||
@ -1,21 +1,15 @@
|
||||
import LoginPage from "../pageobjects/login.page.js";
|
||||
import { BAD_PASSWORD, BAD_USERNAME, GOOD_USERNAME } from "../utils/constants.js";
|
||||
import { BAD_USERNAME, GOOD_PASSWORD } from "../utils/constants.js";
|
||||
import { expect } from "@wdio/globals";
|
||||
|
||||
describe("Log into authentik", () => {
|
||||
it("should fail on a bad username", async () => {
|
||||
await LoginPage.open();
|
||||
await LoginPage.username(BAD_USERNAME);
|
||||
const failure = await LoginPage.authFailure;
|
||||
expect(failure).toHaveText("Failed to authenticate.");
|
||||
});
|
||||
|
||||
it("should fail on a bad password", async () => {
|
||||
await LoginPage.open();
|
||||
await LoginPage.username(GOOD_USERNAME);
|
||||
await LoginPage.pause();
|
||||
await LoginPage.password(BAD_PASSWORD);
|
||||
const failure = await LoginPage.authFailure;
|
||||
expect(failure).toHaveText("Failed to authenticate.");
|
||||
await LoginPage.password(GOOD_PASSWORD);
|
||||
const failure = await LoginPage.authFailure();
|
||||
await expect(failure).toBeDisplayedInViewport();
|
||||
await expect(failure).toHaveText("Invalid password");
|
||||
});
|
||||
});
|
||||
|
||||
@ -10,25 +10,27 @@ async function reachTheProvider(title: string) {
|
||||
await ApplicationsListPage.logout();
|
||||
await login();
|
||||
await ApplicationsListPage.open();
|
||||
await expect(await ApplicationsListPage.pageHeader).toHaveText("Applications");
|
||||
await ApplicationsListPage.pause("ak-page-header");
|
||||
await expect(await ApplicationsListPage.pageHeader()).toBeDisplayed();
|
||||
await expect(await ApplicationsListPage.pageHeader()).toHaveText("Applications");
|
||||
|
||||
await ApplicationsListPage.startWizardButton.click();
|
||||
await ApplicationWizardView.wizardTitle.waitForDisplayed();
|
||||
await expect(await ApplicationWizardView.wizardTitle).toHaveText("New application");
|
||||
await (await ApplicationsListPage.startWizardButton()).click();
|
||||
await (await ApplicationWizardView.wizardTitle()).waitForDisplayed();
|
||||
await expect(await ApplicationWizardView.wizardTitle()).toHaveText("New application");
|
||||
|
||||
await ApplicationWizardView.app.name.setValue(`${title} - ${newPrefix}`);
|
||||
await ApplicationWizardView.app.uiSettings.scrollIntoView();
|
||||
await ApplicationWizardView.app.uiSettings.click();
|
||||
await ApplicationWizardView.app.launchUrl.scrollIntoView();
|
||||
await ApplicationWizardView.app.launchUrl.setValue("http://example.goauthentik.io");
|
||||
await (await ApplicationWizardView.app.name()).setValue(`${title} - ${newPrefix}`);
|
||||
await (await ApplicationWizardView.app.uiSettings()).scrollIntoView();
|
||||
await (await ApplicationWizardView.app.uiSettings()).click();
|
||||
await (await ApplicationWizardView.app.launchUrl()).scrollIntoView();
|
||||
await (await ApplicationWizardView.app.launchUrl()).setValue("http://example.goauthentik.io");
|
||||
|
||||
await ApplicationWizardView.nextButton.click();
|
||||
await (await ApplicationWizardView.nextButton()).click();
|
||||
return await ApplicationWizardView.pause();
|
||||
}
|
||||
|
||||
async function getCommitMessage() {
|
||||
await ApplicationWizardView.successMessage.waitForDisplayed();
|
||||
return await ApplicationWizardView.successMessage;
|
||||
await (await ApplicationWizardView.successMessage()).waitForDisplayed();
|
||||
return await ApplicationWizardView.successMessage();
|
||||
}
|
||||
|
||||
const SUCCESS_MESSAGE = "Your application has been saved";
|
||||
@ -38,97 +40,97 @@ describe("Configure Applications with the Application Wizard", () => {
|
||||
it("Should configure a simple LDAP Application", async () => {
|
||||
await reachTheProvider("New LDAP Application");
|
||||
|
||||
await ApplicationWizardView.providerList.waitForDisplayed();
|
||||
await ApplicationWizardView.ldapProvider.scrollIntoView();
|
||||
await ApplicationWizardView.ldapProvider.click();
|
||||
await (await ApplicationWizardView.providerList()).waitForDisplayed();
|
||||
await (await ApplicationWizardView.ldapProvider).scrollIntoView();
|
||||
await (await ApplicationWizardView.ldapProvider).click();
|
||||
|
||||
await ApplicationWizardView.nextButton.click();
|
||||
await (await ApplicationWizardView.nextButton()).click();
|
||||
await ApplicationWizardView.pause();
|
||||
|
||||
await ApplicationWizardView.ldap.setBindFlow("default-authentication-flow");
|
||||
await ApplicationWizardView.nextButton.click();
|
||||
await (await ApplicationWizardView.nextButton()).click();
|
||||
await ApplicationWizardView.pause();
|
||||
|
||||
await expect(getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
|
||||
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
|
||||
});
|
||||
|
||||
it("Should configure a simple Oauth2 Application", async () => {
|
||||
await reachTheProvider("New Oauth2 Application");
|
||||
|
||||
await ApplicationWizardView.providerList.waitForDisplayed();
|
||||
await ApplicationWizardView.oauth2Provider.scrollIntoView();
|
||||
await ApplicationWizardView.oauth2Provider.click();
|
||||
await (await ApplicationWizardView.providerList()).waitForDisplayed();
|
||||
await (await ApplicationWizardView.oauth2Provider).scrollIntoView();
|
||||
await (await ApplicationWizardView.oauth2Provider).click();
|
||||
|
||||
await ApplicationWizardView.nextButton.click();
|
||||
await (await ApplicationWizardView.nextButton()).click();
|
||||
await ApplicationWizardView.pause();
|
||||
|
||||
await ApplicationWizardView.oauth.setAuthorizationFlow(EXPLICIT_CONSENT);
|
||||
await ApplicationWizardView.nextButton.click();
|
||||
await (await ApplicationWizardView.nextButton()).click();
|
||||
await ApplicationWizardView.pause();
|
||||
|
||||
await expect(getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
|
||||
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
|
||||
});
|
||||
|
||||
it("Should configure a simple SAML Application", async () => {
|
||||
await reachTheProvider("New SAML Application");
|
||||
|
||||
await ApplicationWizardView.providerList.waitForDisplayed();
|
||||
await ApplicationWizardView.samlProvider.scrollIntoView();
|
||||
await ApplicationWizardView.samlProvider.click();
|
||||
await (await ApplicationWizardView.providerList()).waitForDisplayed();
|
||||
await (await ApplicationWizardView.samlProvider).scrollIntoView();
|
||||
await (await ApplicationWizardView.samlProvider).click();
|
||||
|
||||
await ApplicationWizardView.nextButton.click();
|
||||
await (await ApplicationWizardView.nextButton()).click();
|
||||
await ApplicationWizardView.pause();
|
||||
|
||||
await ApplicationWizardView.saml.setAuthorizationFlow(EXPLICIT_CONSENT);
|
||||
await ApplicationWizardView.saml.acsUrl.setValue("http://example.com:8000/");
|
||||
await ApplicationWizardView.nextButton.click();
|
||||
await (await ApplicationWizardView.nextButton()).click();
|
||||
await ApplicationWizardView.pause();
|
||||
|
||||
await expect(getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
|
||||
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
|
||||
});
|
||||
|
||||
it("Should configure a simple SCIM Application", async () => {
|
||||
await reachTheProvider("New SCIM Application");
|
||||
|
||||
await ApplicationWizardView.providerList.waitForDisplayed();
|
||||
await ApplicationWizardView.scimProvider.scrollIntoView();
|
||||
await ApplicationWizardView.scimProvider.click();
|
||||
await (await ApplicationWizardView.providerList()).waitForDisplayed();
|
||||
await (await ApplicationWizardView.scimProvider).scrollIntoView();
|
||||
await (await ApplicationWizardView.scimProvider).click();
|
||||
|
||||
await ApplicationWizardView.nextButton.click();
|
||||
await (await ApplicationWizardView.nextButton()).click();
|
||||
await ApplicationWizardView.pause();
|
||||
|
||||
await ApplicationWizardView.scim.url.setValue("http://example.com:8000/");
|
||||
await ApplicationWizardView.scim.token.setValue("a-very-basic-token");
|
||||
await ApplicationWizardView.nextButton.click();
|
||||
await (await ApplicationWizardView.nextButton()).click();
|
||||
await ApplicationWizardView.pause();
|
||||
|
||||
await expect(getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
|
||||
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
|
||||
});
|
||||
|
||||
it("Should configure a simple Radius Application", async () => {
|
||||
await reachTheProvider("New Radius Application");
|
||||
|
||||
await ApplicationWizardView.providerList.waitForDisplayed();
|
||||
await ApplicationWizardView.radiusProvider.scrollIntoView();
|
||||
await ApplicationWizardView.radiusProvider.click();
|
||||
await (await ApplicationWizardView.providerList()).waitForDisplayed();
|
||||
await (await ApplicationWizardView.radiusProvider).scrollIntoView();
|
||||
await (await ApplicationWizardView.radiusProvider).click();
|
||||
|
||||
await ApplicationWizardView.nextButton.click();
|
||||
await (await ApplicationWizardView.nextButton()).click();
|
||||
await ApplicationWizardView.pause();
|
||||
|
||||
await ApplicationWizardView.radius.setAuthenticationFlow("default-authentication-flow");
|
||||
await ApplicationWizardView.nextButton.click();
|
||||
await (await ApplicationWizardView.nextButton()).click();
|
||||
await ApplicationWizardView.pause();
|
||||
|
||||
await expect(getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
|
||||
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
|
||||
});
|
||||
|
||||
it("Should configure a simple Transparent Proxy Application", async () => {
|
||||
await reachTheProvider("New Transparent Proxy Application");
|
||||
|
||||
await ApplicationWizardView.providerList.waitForDisplayed();
|
||||
await ApplicationWizardView.proxyProviderProxy.scrollIntoView();
|
||||
await ApplicationWizardView.proxyProviderProxy.click();
|
||||
await ApplicationWizardView.nextButton.click();
|
||||
await (await ApplicationWizardView.providerList()).waitForDisplayed();
|
||||
await (await ApplicationWizardView.proxyProviderProxy).scrollIntoView();
|
||||
await (await ApplicationWizardView.proxyProviderProxy).click();
|
||||
await (await ApplicationWizardView.nextButton()).click();
|
||||
await ApplicationWizardView.pause();
|
||||
|
||||
await ApplicationWizardView.transparentProxy.setAuthorizationFlow(EXPLICIT_CONSENT);
|
||||
@ -139,19 +141,19 @@ describe("Configure Applications with the Application Wizard", () => {
|
||||
"http://internal.example.com",
|
||||
);
|
||||
|
||||
await ApplicationWizardView.nextButton.click();
|
||||
await (await ApplicationWizardView.nextButton()).click();
|
||||
await ApplicationWizardView.pause();
|
||||
|
||||
await expect(getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
|
||||
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
|
||||
});
|
||||
|
||||
it("Should configure a simple Forward Proxy Application", async () => {
|
||||
await reachTheProvider("New Forward Proxy Application");
|
||||
|
||||
await ApplicationWizardView.providerList.waitForDisplayed();
|
||||
await ApplicationWizardView.proxyProviderForwardsingle.scrollIntoView();
|
||||
await ApplicationWizardView.proxyProviderForwardsingle.click();
|
||||
await ApplicationWizardView.nextButton.click();
|
||||
await (await ApplicationWizardView.providerList()).waitForDisplayed();
|
||||
await (await ApplicationWizardView.proxyProviderForwardsingle).scrollIntoView();
|
||||
await (await ApplicationWizardView.proxyProviderForwardsingle).click();
|
||||
await (await ApplicationWizardView.nextButton()).click();
|
||||
await ApplicationWizardView.pause();
|
||||
|
||||
await ApplicationWizardView.forwardProxy.setAuthorizationFlow(EXPLICIT_CONSENT);
|
||||
@ -159,9 +161,9 @@ describe("Configure Applications with the Application Wizard", () => {
|
||||
"http://external.example.com",
|
||||
);
|
||||
|
||||
await ApplicationWizardView.nextButton.click();
|
||||
await (await ApplicationWizardView.nextButton()).click();
|
||||
await ApplicationWizardView.pause();
|
||||
|
||||
await expect(getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
|
||||
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
|
||||
});
|
||||
});
|
||||
|
||||
@ -6,5 +6,5 @@ import { expect } from "@wdio/globals";
|
||||
export const login = async () => {
|
||||
await LoginPage.open();
|
||||
await LoginPage.login(GOOD_USERNAME, GOOD_PASSWORD);
|
||||
await expect(UserLibraryPage.pageHeader).toHaveText("My applications");
|
||||
await expect(await UserLibraryPage.pageHeader()).toHaveText("My applications");
|
||||
};
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"moduleResolution": "node",
|
||||
"module": "ESNext",
|
||||
"target": "es2022",
|
||||
"types": ["node", "@wdio/globals/types", "expect-webdriverio", "@wdio/mocha-framework"],
|
||||
"types": ["node", "@wdio/globals/types", "expect-webdriverio", "@wdio/mocha-framework", "@types/mocha"],
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
|
||||
@ -41,6 +41,7 @@ const definitions = {
|
||||
|
||||
const otherFiles = [
|
||||
["node_modules/@patternfly/patternfly/patternfly.min.css", "."],
|
||||
["node_modules/@patternfly/patternfly/patternfly-base.css", "."],
|
||||
["node_modules/@patternfly/patternfly/assets/**", ".", "node_modules/@patternfly/patternfly/"],
|
||||
["src/custom.css", "."],
|
||||
["src/common/styles/**", "."],
|
||||
@ -79,6 +80,12 @@ const interfaces = [
|
||||
["polyfill/poly.ts", "."],
|
||||
];
|
||||
|
||||
const extraTargets = [
|
||||
["sdk/index.ts", "sdk", { entryNames: "[dir]/[name]" }],
|
||||
["sdk/user-settings.ts", "sdk/user-settings", { entryNames: "[dir]/[name]" }],
|
||||
["sdk/flow.ts", "sdk/flow", { entryNames: "[dir]/[name]" }],
|
||||
];
|
||||
|
||||
const baseArgs = {
|
||||
bundle: true,
|
||||
write: true,
|
||||
@ -101,7 +108,11 @@ function getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
async function buildOneSource(source, dest) {
|
||||
function getAllTargets() {
|
||||
return [...interfaces, ...extraTargets];
|
||||
}
|
||||
|
||||
async function buildSingleTarget(source, dest, options) {
|
||||
const DIST = path.join(__dirname, "./dist", dest);
|
||||
console.log(`[${new Date(Date.now()).toISOString()}] Starting build for target ${source}`);
|
||||
|
||||
@ -112,6 +123,7 @@ async function buildOneSource(source, dest) {
|
||||
entryPoints: [`./src/${source}`],
|
||||
entryNames: `[dir]/[name]-${getVersion()}`,
|
||||
outdir: DIST,
|
||||
...options,
|
||||
});
|
||||
const end = Date.now();
|
||||
console.log(
|
||||
@ -124,8 +136,10 @@ async function buildOneSource(source, dest) {
|
||||
}
|
||||
}
|
||||
|
||||
async function buildAuthentik(interfaces) {
|
||||
await Promise.allSettled(interfaces.map(([source, dest]) => buildOneSource(source, dest)));
|
||||
async function buildTargets(targets) {
|
||||
await Promise.allSettled(
|
||||
targets.map(([source, dest, options]) => buildSingleTarget(source, dest, options)),
|
||||
);
|
||||
}
|
||||
|
||||
let timeoutId = null;
|
||||
@ -135,7 +149,7 @@ function debouncedBuild() {
|
||||
}
|
||||
timeoutId = setTimeout(() => {
|
||||
console.clear();
|
||||
buildAuthentik(interfaces);
|
||||
buildTargets(getAllTargets());
|
||||
}, 250);
|
||||
}
|
||||
|
||||
@ -143,7 +157,7 @@ if (process.argv.length > 2 && (process.argv[2] === "-h" || process.argv[2] ===
|
||||
console.log(`Build the authentikUI
|
||||
|
||||
options:
|
||||
-w, --watch: Build all ${interfaces.length} interfaces
|
||||
-w, --watch: Build all ${getAllTargets().length} interfaces
|
||||
-p, --proxy: Build only the polyfills and the loading application
|
||||
-h, --help: This help message
|
||||
`);
|
||||
@ -163,11 +177,11 @@ if (process.argv.length > 2 && (process.argv[2] === "-w" || process.argv[2] ===
|
||||
});
|
||||
} else if (process.argv.length > 2 && (process.argv[2] === "-p" || process.argv[2] === "--proxy")) {
|
||||
// There's no watch-for-proxy, sorry.
|
||||
await buildAuthentik(
|
||||
await buildTargets(
|
||||
interfaces.filter(([_, dest]) => ["standalone/loading", "."].includes(dest)),
|
||||
);
|
||||
process.exit(0);
|
||||
} else {
|
||||
// And the fallback: just build it.
|
||||
await buildAuthentik(interfaces);
|
||||
await buildTargets(interfaces);
|
||||
}
|
||||
|
||||
1788
web/package-lock.json
generated
1788
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -11,15 +11,15 @@
|
||||
"@floating-ui/dom": "^1.6.9",
|
||||
"@formatjs/intl-listformat": "^7.5.7",
|
||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||
"@goauthentik/api": "^2024.6.3-1724414734",
|
||||
"@goauthentik/api": "^2024.8.0-1725367323",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@lit/localize": "^0.12.2",
|
||||
"@lit/reactive-element": "^2.0.4",
|
||||
"@lit/task": "^1.0.1",
|
||||
"@open-wc/lit-helpers": "^0.7.0",
|
||||
"@patternfly/elements": "^4.0.0",
|
||||
"@patternfly/elements": "^4.0.1",
|
||||
"@patternfly/patternfly": "^4.224.2",
|
||||
"@sentry/browser": "^8.26.0",
|
||||
"@sentry/browser": "^8.27.0",
|
||||
"@webcomponents/webcomponentsjs": "^2.8.0",
|
||||
"base64-js": "^1.5.1",
|
||||
"chart.js": "^4.4.4",
|
||||
@ -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.1.0",
|
||||
"rapidoc": "^9.3.4",
|
||||
"showdown": "^2.1.0",
|
||||
"style-mod": "^4.1.2",
|
||||
@ -51,13 +51,13 @@
|
||||
"@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",
|
||||
"@lit/localize-tools": "^0.8.0",
|
||||
"@rollup/plugin-replace": "^5.0.7",
|
||||
"@spotlightjs/spotlight": "^2.3.0",
|
||||
"@spotlightjs/spotlight": "^2.3.2",
|
||||
"@storybook/addon-essentials": "^8.2.9",
|
||||
"@storybook/addon-links": "^8.2.9",
|
||||
"@storybook/api": "^7.6.17",
|
||||
@ -71,6 +71,7 @@
|
||||
"@types/eslint__js": "^8.42.3",
|
||||
"@types/grecaptcha": "^3.0.9",
|
||||
"@types/guacamole-common-js": "1.5.2",
|
||||
"@types/node": "^22.5.0",
|
||||
"@types/showdown": "^2.0.6",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
||||
"@typescript-eslint/parser": "^8.0.1",
|
||||
@ -90,6 +91,7 @@
|
||||
"github-slugger": "^2.0.0",
|
||||
"glob": "^11.0.0",
|
||||
"globals": "^15.9.0",
|
||||
"knip": "^5.27.4",
|
||||
"lit-analyzer": "^2.0.3",
|
||||
"lockfile-lint": "^4.14.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
@ -101,13 +103,13 @@
|
||||
"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",
|
||||
"typescript-eslint": "^8.4.0",
|
||||
"vite-tsconfig-paths": "^5.0.1",
|
||||
"wdio-wait-for": "^3.0.11",
|
||||
"wireit": "^0.14.8"
|
||||
@ -120,9 +122,9 @@
|
||||
"@esbuild/darwin-arm64": "^0.23.0",
|
||||
"@esbuild/linux-amd64": "^0.18.11",
|
||||
"@esbuild/linux-arm64": "^0.23.0",
|
||||
"@rollup/rollup-darwin-arm64": "4.21.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.21.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.21.0"
|
||||
"@rollup/rollup-darwin-arm64": "4.21.2",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.21.2",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.21.2"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@ -135,6 +137,7 @@
|
||||
"extract-locales": "wireit",
|
||||
"format": "wireit",
|
||||
"lint": "wireit",
|
||||
"lint:imports": "wireit",
|
||||
"lint:lockfile": "wireit",
|
||||
"lint:nightmare": "wireit",
|
||||
"lint:package": "wireit",
|
||||
@ -149,8 +152,7 @@
|
||||
"storybook:build": "wireit",
|
||||
"storybook:build-import-map": "wireit",
|
||||
"test": "wireit",
|
||||
"test-view": "wireit",
|
||||
"test-watch": "npx wdio run ./wdio.conf.ts --autoCompileOpts.tsNodeOpts.project=tsconfig.test.json --watch",
|
||||
"test-watch": "wireit",
|
||||
"tsc": "wireit",
|
||||
"watch": "run-s build-locales esbuild:watch"
|
||||
},
|
||||
@ -250,6 +252,9 @@
|
||||
"lint:components": {
|
||||
"command": "lit-analyzer src"
|
||||
},
|
||||
"lint:imports": {
|
||||
"command": "knip --config scripts/knip.config.ts"
|
||||
},
|
||||
"lint:types": {
|
||||
"command": "tsc --noEmit -p .",
|
||||
"dependencies": [
|
||||
@ -330,7 +335,7 @@
|
||||
"TS_NODE_PROJECT": "tsconfig.test.json"
|
||||
}
|
||||
},
|
||||
"test-view": {
|
||||
"test-watch": {
|
||||
"command": "wdio run ./wdio.conf.ts",
|
||||
"env": {
|
||||
"TS_NODE_PROJECT": "tsconfig.test.json"
|
||||
|
||||
@ -14,18 +14,18 @@
|
||||
"@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.23",
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"@types/jquery": "^3.5.30",
|
||||
"lockfile-lint": "^4.14.0",
|
||||
"prettier": "^3.3.2",
|
||||
"rollup": "^4.21.0",
|
||||
"rollup": "^4.21.2",
|
||||
"rollup-plugin-copy": "^3.5.0",
|
||||
"wireit": "^0.14.8"
|
||||
},
|
||||
"license": "MIT",
|
||||
"optionalDependencies": {
|
||||
"@swc/core": "^1.7.14",
|
||||
"@swc/core": "^1.7.23",
|
||||
"@swc/core-darwin-arm64": "^1.6.13",
|
||||
"@swc/core-darwin-x64": "^1.6.13",
|
||||
"@swc/core-linux-arm-gnueabihf": "^1.6.13",
|
||||
|
||||
48
web/scripts/knip.config.ts
Normal file
48
web/scripts/knip.config.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { type KnipConfig } from "knip";
|
||||
|
||||
const config: KnipConfig = {
|
||||
"entry": [
|
||||
"./src/admin/AdminInterface/AdminInterface.ts",
|
||||
"./src/user/UserInterface.ts",
|
||||
"./src/flow/FlowInterface.ts",
|
||||
"./src/standalone/api-browser/index.ts",
|
||||
"./src/enterprise/rac/index.ts",
|
||||
"./src/standalone/loading/index.ts",
|
||||
"./src/polyfill/poly.ts",
|
||||
],
|
||||
"project": ["src/**/*.ts", "src/**/*.js", "./scripts/*.mjs", ".storybook/*.ts"],
|
||||
// "ignore": ["src/**/*.test.ts", "src/**/*.stories.ts"],
|
||||
// Prevent Knip from complaining about web components, which export their classes but also
|
||||
// export their registration, and we don't always use both.
|
||||
"ignoreExportsUsedInFile": true,
|
||||
"typescript": {
|
||||
config: ["tsconfig.json"],
|
||||
},
|
||||
"wireit": {
|
||||
config: ["package.json"],
|
||||
},
|
||||
"storybook": {
|
||||
config: [".storybook/{main,test-runner}.{js,ts}"],
|
||||
entry: [
|
||||
".storybook/{manager,preview}.{js,jsx,ts,tsx}",
|
||||
"**/*.@(mdx|stories.@(mdx|js|jsx|mjs|ts|tsx))",
|
||||
],
|
||||
project: [".storybook/**/*.{js,jsx,ts,tsx}"],
|
||||
},
|
||||
"eslint": {
|
||||
entry: [
|
||||
"eslint.config.mjs",
|
||||
"scripts/eslint.precommit.mjs",
|
||||
"scripts/eslint.nightmare.mjs",
|
||||
"scripts/eslint-precommit.mjs",
|
||||
"scripts/eslint-nightmare.mjs",
|
||||
"scripts/eslint.mjs",
|
||||
],
|
||||
config: ["package.json"],
|
||||
},
|
||||
"webdriver-io": {
|
||||
config: ["wdio.conf.js"],
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
3057
web/sfe/package-lock.json
generated
3057
web/sfe/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,28 +0,0 @@
|
||||
{
|
||||
"name": "@goauthentik/web-sfe",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@goauthentik/api": "^2024.6.3-1724337552",
|
||||
"base64-js": "^1.5.1",
|
||||
"bootstrap": "^4.6.1",
|
||||
"formdata-polyfill": "^4.0.10",
|
||||
"jquery": "^3.7.1",
|
||||
"weakmap-polyfill": "^2.0.4"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rollup -c rollup.config.js --bundleConfigAsCjs",
|
||||
"watch": "rollup -w -c rollup.config.js --bundleConfigAsCjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^26.0.1",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@rollup/plugin-swc": "^0.3.1",
|
||||
"@swc/cli": "^0.4.0",
|
||||
"@swc/core": "^1.7.14",
|
||||
"@types/jquery": "^3.5.30",
|
||||
"rollup": "^4.21.0",
|
||||
"rollup-plugin-copy": "^3.5.0"
|
||||
}
|
||||
}
|
||||
@ -21,10 +21,20 @@ export class ApplicationWizardAuthenticationMethodChoice extends WithLicenseSumm
|
||||
const selectedTypes = providerModelsList.filter(
|
||||
(t) => t.formName === this.wizard.providerModel,
|
||||
);
|
||||
|
||||
// As a hack, the Application wizard has separate provider paths for our three types of
|
||||
// proxy providers. This patch swaps the form we want to be directed to on page 3 from the
|
||||
// modelName to the formName, so we get the right one. This information isn't modified
|
||||
// or forwarded, so the proxy-plus-subtype is correctly mapped on submission.
|
||||
const typesForWizard = providerModelsList.map((provider) => ({
|
||||
...provider,
|
||||
modelName: provider.formName,
|
||||
}));
|
||||
|
||||
return providerModelsList.length > 0
|
||||
? html`<form class="pf-c-form pf-m-horizontal">
|
||||
<ak-wizard-page-type-create
|
||||
.types=${providerModelsList}
|
||||
.types=${typesForWizard}
|
||||
layout=${TypeCreateWizardPageLayouts.grid}
|
||||
.selectedType=${selectedTypes.length > 0 ? selectedTypes[0] : undefined}
|
||||
@select=${(ev: CustomEvent<LocalTypeCreate>) => {
|
||||
|
||||
@ -265,7 +265,7 @@ export class ApplicationWizardAuthenticationByOauth extends BaseProviderPanel {
|
||||
>
|
||||
<ak-dual-select-provider
|
||||
.provider=${oauth2SourcesProvider}
|
||||
.selected=${provider?.jwksSources}
|
||||
.selected=${provider?.jwksSources ?? []}
|
||||
available-label=${msg("Available Sources")}
|
||||
selected-label=${msg("Selected Sources")}
|
||||
></ak-dual-select-provider>
|
||||
|
||||
@ -230,7 +230,7 @@ export class AkTypeProxyApplicationWizardPage extends BaseProviderPanel {
|
||||
>
|
||||
<ak-dual-select-provider
|
||||
.provider=${oauth2SourcesProvider}
|
||||
.selected=${this.instance?.jwksSources}
|
||||
.selected=${this.instance?.jwksSources ?? []}
|
||||
available-label=${msg("Available Sources")}
|
||||
selected-label=${msg("Selected Sources")}
|
||||
></ak-dual-select-provider>
|
||||
|
||||
@ -97,7 +97,8 @@ export class OutpostForm extends ModelForm<Outpost, string> {
|
||||
embedded = false;
|
||||
|
||||
@state()
|
||||
providers?: DataProvider;
|
||||
providers: DataProvider = providerProvider(this.type);
|
||||
|
||||
defaultConfig?: OutpostDefaultConfig;
|
||||
|
||||
async loadInstance(pk: string): Promise<Outpost> {
|
||||
@ -113,6 +114,7 @@ export class OutpostForm extends ModelForm<Outpost, string> {
|
||||
this.defaultConfig = await new OutpostsApi(
|
||||
DEFAULT_CONFIG,
|
||||
).outpostsInstancesDefaultSettingsRetrieve();
|
||||
this.providers = providerProvider(this.type);
|
||||
}
|
||||
|
||||
getSuccessMessage(): string {
|
||||
|
||||
@ -117,7 +117,7 @@ export class EventMatcherPolicyForm extends BasePolicyForm<EventMatcherPolicy> {
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Matches Event's Client IP (strict matching, for network matching use an Expression Policy.",
|
||||
"Matches Event's Client IP (strict matching, for network matching use an Expression Policy).",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
@ -7,6 +7,9 @@ export function renderSourceIcon(name: string, iconUrl: string | undefined | nul
|
||||
const url = iconUrl.replaceAll("fa://", "");
|
||||
return html`<i class="fas ${url}" title="${name}"></i>`;
|
||||
}
|
||||
if (window.authentik_sdk?.base) {
|
||||
return html`<img src="${window.authentik_sdk?.base}${iconUrl}" alt="${name}" />`;
|
||||
}
|
||||
return html`<img src="${iconUrl}" alt="${name}" />`;
|
||||
}
|
||||
return icon;
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -2,6 +2,7 @@ import {
|
||||
CSRFMiddleware,
|
||||
EventMiddleware,
|
||||
LoggingMiddleware,
|
||||
SDKMiddleware,
|
||||
} from "@goauthentik/common/api/middleware";
|
||||
import { EVENT_LOCALE_REQUEST, VERSION } from "@goauthentik/common/constants";
|
||||
import { globalAK } from "@goauthentik/common/global";
|
||||
@ -67,8 +68,18 @@ export function getMetaContent(key: string): string {
|
||||
return metaEl.content;
|
||||
}
|
||||
|
||||
export function apiBase(): string {
|
||||
if (process.env.AK_API_BASE_PATH) {
|
||||
return process.env.AK_API_BASE_PATH;
|
||||
}
|
||||
if (window.authentik_sdk?.base) {
|
||||
return window.authentik_sdk?.base;
|
||||
}
|
||||
return window.location.origin;
|
||||
}
|
||||
|
||||
export const DEFAULT_CONFIG = new Configuration({
|
||||
basePath: (process.env.AK_API_BASE_PATH || window.location.origin) + "/api/v3",
|
||||
basePath: `${apiBase()}/api/v3`,
|
||||
headers: {
|
||||
"sentry-trace": getMetaContent("sentry-trace"),
|
||||
},
|
||||
@ -76,6 +87,7 @@ export const DEFAULT_CONFIG = new Configuration({
|
||||
new CSRFMiddleware(),
|
||||
new EventMiddleware(),
|
||||
new LoggingMiddleware(globalAK().brand),
|
||||
new SDKMiddleware(),
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@ -44,6 +44,21 @@ export class CSRFMiddleware implements Middleware {
|
||||
}
|
||||
}
|
||||
|
||||
export class SDKMiddleware implements Middleware {
|
||||
token?: string;
|
||||
constructor() {
|
||||
this.token = window.authentik_sdk?.token;
|
||||
}
|
||||
pre?(context: RequestContext): Promise<FetchParams | void> {
|
||||
if (this.token) {
|
||||
context.init.credentials = "include";
|
||||
// @ts-ignore
|
||||
context.init.headers["Authorization"] = `Bearer ${this.token}`;
|
||||
}
|
||||
return Promise.resolve(context);
|
||||
}
|
||||
}
|
||||
|
||||
export class EventMiddleware implements Middleware {
|
||||
post?(context: ResponseContext): Promise<Response | void> {
|
||||
const request: RequestInfo = {
|
||||
|
||||
@ -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 = ";";
|
||||
|
||||
|
||||
@ -1,4 +1,11 @@
|
||||
import { Config, ConfigFromJSON, CurrentBrand, CurrentBrandFromJSON } from "@goauthentik/api";
|
||||
import {
|
||||
Config,
|
||||
ConfigFromJSON,
|
||||
CurrentBrand,
|
||||
CurrentBrandFromJSON,
|
||||
ErrorReportingConfigFromJSON,
|
||||
UiThemeEnum,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
export interface GlobalAuthentik {
|
||||
_converted?: boolean;
|
||||
@ -28,9 +35,12 @@ export function globalAK(): GlobalAuthentik {
|
||||
return {
|
||||
config: ConfigFromJSON({
|
||||
capabilities: [],
|
||||
error_reporting: ErrorReportingConfigFromJSON({}),
|
||||
}),
|
||||
brand: CurrentBrandFromJSON({
|
||||
matched_domain: window.location.host,
|
||||
ui_footer_links: [],
|
||||
ui_theme: window.authentik_sdk?.forceTheme ?? UiThemeEnum.Automatic,
|
||||
}),
|
||||
versionFamily: "",
|
||||
versionSubdomain: "",
|
||||
@ -40,6 +50,10 @@ export function globalAK(): GlobalAuthentik {
|
||||
return ak;
|
||||
}
|
||||
|
||||
export function isEmbedded() {
|
||||
return !!window.authentik_sdk;
|
||||
}
|
||||
|
||||
export function docLink(path: string): string {
|
||||
const ak = globalAK();
|
||||
// Default case or beta build which should always point to latest
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user