flows: planner error handling (#4812)
* handle FlowNonApplicableException everywhere Signed-off-by: Jens Langhammer <jens@goauthentik.io> * make flow planner check authentication when no pending user is in planning context Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add mailhog to e2e test services, remove local docker requirement Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
parent
6f2f4f4aa3
commit
20e971f5ce
|
@ -68,6 +68,7 @@ from authentik.core.models import (
|
||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
from authentik.events.models import EventAction
|
from authentik.events.models import EventAction
|
||||||
|
from authentik.flows.exceptions import FlowNonApplicableException
|
||||||
from authentik.flows.models import FlowToken
|
from authentik.flows.models import FlowToken
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
|
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
|
||||||
from authentik.flows.views.executor import QS_KEY_TOKEN
|
from authentik.flows.views.executor import QS_KEY_TOKEN
|
||||||
|
@ -326,12 +327,16 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||||
user: User = self.get_object()
|
user: User = self.get_object()
|
||||||
planner = FlowPlanner(flow)
|
planner = FlowPlanner(flow)
|
||||||
planner.allow_empty_flows = True
|
planner.allow_empty_flows = True
|
||||||
plan = planner.plan(
|
try:
|
||||||
self.request._request,
|
plan = planner.plan(
|
||||||
{
|
self.request._request,
|
||||||
PLAN_CONTEXT_PENDING_USER: user,
|
{
|
||||||
},
|
PLAN_CONTEXT_PENDING_USER: user,
|
||||||
)
|
},
|
||||||
|
)
|
||||||
|
except FlowNonApplicableException:
|
||||||
|
LOGGER.warning("Recovery flow not applicable to user")
|
||||||
|
return None, None
|
||||||
token, __ = FlowToken.objects.update_or_create(
|
token, __ = FlowToken.objects.update_or_create(
|
||||||
identifier=f"{user.uid}-password-reset",
|
identifier=f"{user.uid}-password-reset",
|
||||||
defaults={
|
defaults={
|
||||||
|
|
|
@ -11,6 +11,7 @@ from authentik.flows.challenge import (
|
||||||
HttpChallengeResponse,
|
HttpChallengeResponse,
|
||||||
RedirectChallenge,
|
RedirectChallenge,
|
||||||
)
|
)
|
||||||
|
from authentik.flows.exceptions import FlowNonApplicableException
|
||||||
from authentik.flows.models import in_memory_stage
|
from authentik.flows.models import in_memory_stage
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
|
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
|
||||||
from authentik.flows.stage import ChallengeStageView
|
from authentik.flows.stage import ChallengeStageView
|
||||||
|
@ -41,15 +42,18 @@ class RedirectToAppLaunch(View):
|
||||||
flow = tenant.flow_authentication
|
flow = tenant.flow_authentication
|
||||||
planner = FlowPlanner(flow)
|
planner = FlowPlanner(flow)
|
||||||
planner.allow_empty_flows = True
|
planner.allow_empty_flows = True
|
||||||
plan = planner.plan(
|
try:
|
||||||
request,
|
plan = planner.plan(
|
||||||
{
|
request,
|
||||||
PLAN_CONTEXT_APPLICATION: app,
|
{
|
||||||
PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
|
PLAN_CONTEXT_APPLICATION: app,
|
||||||
% {"application": app.name},
|
PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
|
||||||
PLAN_CONTEXT_CONSENT_PERMISSIONS: [],
|
% {"application": app.name},
|
||||||
},
|
PLAN_CONTEXT_CONSENT_PERMISSIONS: [],
|
||||||
)
|
},
|
||||||
|
)
|
||||||
|
except FlowNonApplicableException:
|
||||||
|
raise Http404
|
||||||
plan.insert_stage(in_memory_stage(RedirectToAppStage))
|
plan.insert_stage(in_memory_stage(RedirectToAppStage))
|
||||||
request.session[SESSION_KEY_PLAN] = plan
|
request.session[SESSION_KEY_PLAN] = plan
|
||||||
return redirect_with_qs("authentik_core:if-flow", request.GET, flow_slug=flow.slug)
|
return redirect_with_qs("authentik_core:if-flow", request.GET, flow_slug=flow.slug)
|
||||||
|
|
|
@ -147,7 +147,6 @@ class FlowPlanner:
|
||||||
) -> FlowPlan:
|
) -> FlowPlan:
|
||||||
"""Check each of the flows' policies, check policies for each stage with PolicyBinding
|
"""Check each of the flows' policies, check policies for each stage with PolicyBinding
|
||||||
and return ordered list"""
|
and return ordered list"""
|
||||||
self._check_authentication(request)
|
|
||||||
with Hub.current.start_span(
|
with Hub.current.start_span(
|
||||||
op="authentik.flow.planner.plan", description=self.flow.slug
|
op="authentik.flow.planner.plan", description=self.flow.slug
|
||||||
) as span:
|
) as span:
|
||||||
|
@ -165,6 +164,12 @@ class FlowPlanner:
|
||||||
user = default_context[PLAN_CONTEXT_PENDING_USER]
|
user = default_context[PLAN_CONTEXT_PENDING_USER]
|
||||||
else:
|
else:
|
||||||
user = request.user
|
user = request.user
|
||||||
|
# We only need to check the flow authentication if it's planned without a user
|
||||||
|
# in the context, as a user in the context can only be set via the explicit code API
|
||||||
|
# or if a flow is restarted due to `invalid_response_action` being set to
|
||||||
|
# `restart_with_context`, which can only happen if the user was already authorized
|
||||||
|
# to use the flow
|
||||||
|
self._check_authentication(request)
|
||||||
# First off, check the flow's direct policy bindings
|
# First off, check the flow's direct policy bindings
|
||||||
# to make sure the user even has access to the flow
|
# to make sure the user even has access to the flow
|
||||||
engine = PolicyEngine(self.flow, user, request)
|
engine = PolicyEngine(self.flow, user, request)
|
||||||
|
|
|
@ -561,9 +561,13 @@ class ConfigureFlowInitView(LoginRequiredMixin, View):
|
||||||
LOGGER.debug("Stage has no configure_flow set", stage=stage)
|
LOGGER.debug("Stage has no configure_flow set", stage=stage)
|
||||||
raise Http404
|
raise Http404
|
||||||
|
|
||||||
plan = FlowPlanner(stage.configure_flow).plan(
|
try:
|
||||||
request, {PLAN_CONTEXT_PENDING_USER: request.user}
|
plan = FlowPlanner(stage.configure_flow).plan(
|
||||||
)
|
request, {PLAN_CONTEXT_PENDING_USER: request.user}
|
||||||
|
)
|
||||||
|
except FlowNonApplicableException:
|
||||||
|
LOGGER.warning("Flow not applicable to user")
|
||||||
|
raise Http404
|
||||||
request.session[SESSION_KEY_PLAN] = plan
|
request.session[SESSION_KEY_PLAN] = plan
|
||||||
return redirect_with_qs(
|
return redirect_with_qs(
|
||||||
"authentik_core:if-flow",
|
"authentik_core:if-flow",
|
||||||
|
|
|
@ -24,6 +24,7 @@ from authentik.flows.challenge import (
|
||||||
ChallengeTypes,
|
ChallengeTypes,
|
||||||
HttpChallengeResponse,
|
HttpChallengeResponse,
|
||||||
)
|
)
|
||||||
|
from authentik.flows.exceptions import FlowNonApplicableException
|
||||||
from authentik.flows.models import in_memory_stage
|
from authentik.flows.models import in_memory_stage
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
|
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
|
||||||
from authentik.flows.stage import StageView
|
from authentik.flows.stage import StageView
|
||||||
|
@ -373,19 +374,22 @@ class AuthorizationFlowInitView(PolicyAccessView):
|
||||||
# Regardless, we start the planner and return to it
|
# Regardless, we start the planner and return to it
|
||||||
planner = FlowPlanner(self.provider.authorization_flow)
|
planner = FlowPlanner(self.provider.authorization_flow)
|
||||||
planner.allow_empty_flows = True
|
planner.allow_empty_flows = True
|
||||||
plan = planner.plan(
|
try:
|
||||||
self.request,
|
plan = planner.plan(
|
||||||
{
|
self.request,
|
||||||
PLAN_CONTEXT_SSO: True,
|
{
|
||||||
PLAN_CONTEXT_APPLICATION: self.application,
|
PLAN_CONTEXT_SSO: True,
|
||||||
# OAuth2 related params
|
PLAN_CONTEXT_APPLICATION: self.application,
|
||||||
PLAN_CONTEXT_PARAMS: self.params,
|
# OAuth2 related params
|
||||||
# Consent related params
|
PLAN_CONTEXT_PARAMS: self.params,
|
||||||
PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
|
# Consent related params
|
||||||
% {"application": self.application.name},
|
PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
|
||||||
PLAN_CONTEXT_CONSENT_PERMISSIONS: scope_descriptions,
|
% {"application": self.application.name},
|
||||||
},
|
PLAN_CONTEXT_CONSENT_PERMISSIONS: scope_descriptions,
|
||||||
)
|
},
|
||||||
|
)
|
||||||
|
except FlowNonApplicableException:
|
||||||
|
return self.handle_no_permission_authenticated()
|
||||||
# OpenID clients can specify a `prompt` parameter, and if its set to consent we
|
# OpenID clients can specify a `prompt` parameter, and if its set to consent we
|
||||||
# need to inject a consent stage
|
# need to inject a consent stage
|
||||||
if PROMPT_CONSENT in self.params.prompt:
|
if PROMPT_CONSENT in self.params.prompt:
|
||||||
|
|
|
@ -10,6 +10,7 @@ from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
|
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
|
||||||
|
from authentik.flows.exceptions import FlowNonApplicableException
|
||||||
from authentik.flows.models import in_memory_stage
|
from authentik.flows.models import in_memory_stage
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
|
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
|
||||||
from authentik.flows.stage import ChallengeStageView
|
from authentik.flows.stage import ChallengeStageView
|
||||||
|
@ -57,19 +58,23 @@ def validate_code(code: int, request: HttpRequest) -> Optional[HttpResponse]:
|
||||||
scope_descriptions = UserInfoView().get_scope_descriptions(token.scope)
|
scope_descriptions = UserInfoView().get_scope_descriptions(token.scope)
|
||||||
planner = FlowPlanner(token.provider.authorization_flow)
|
planner = FlowPlanner(token.provider.authorization_flow)
|
||||||
planner.allow_empty_flows = True
|
planner.allow_empty_flows = True
|
||||||
plan = planner.plan(
|
try:
|
||||||
request,
|
plan = planner.plan(
|
||||||
{
|
request,
|
||||||
PLAN_CONTEXT_SSO: True,
|
{
|
||||||
PLAN_CONTEXT_APPLICATION: app,
|
PLAN_CONTEXT_SSO: True,
|
||||||
# OAuth2 related params
|
PLAN_CONTEXT_APPLICATION: app,
|
||||||
PLAN_CONTEXT_DEVICE: token,
|
# OAuth2 related params
|
||||||
# Consent related params
|
PLAN_CONTEXT_DEVICE: token,
|
||||||
PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
|
# Consent related params
|
||||||
% {"application": app.name},
|
PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
|
||||||
PLAN_CONTEXT_CONSENT_PERMISSIONS: scope_descriptions,
|
% {"application": app.name},
|
||||||
},
|
PLAN_CONTEXT_CONSENT_PERMISSIONS: scope_descriptions,
|
||||||
)
|
},
|
||||||
|
)
|
||||||
|
except FlowNonApplicableException:
|
||||||
|
LOGGER.warning("Flow not applicable to user")
|
||||||
|
return None
|
||||||
plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage))
|
plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage))
|
||||||
request.session[SESSION_KEY_PLAN] = plan
|
request.session[SESSION_KEY_PLAN] = plan
|
||||||
return redirect_with_qs(
|
return redirect_with_qs(
|
||||||
|
@ -97,7 +102,11 @@ class DeviceEntryView(View):
|
||||||
# Regardless, we start the planner and return to it
|
# Regardless, we start the planner and return to it
|
||||||
planner = FlowPlanner(device_flow)
|
planner = FlowPlanner(device_flow)
|
||||||
planner.allow_empty_flows = True
|
planner.allow_empty_flows = True
|
||||||
plan = planner.plan(self.request)
|
try:
|
||||||
|
plan = planner.plan(self.request)
|
||||||
|
except FlowNonApplicableException:
|
||||||
|
LOGGER.warning("Flow not applicable to user")
|
||||||
|
return HttpResponse(status=404)
|
||||||
plan.append_stage(in_memory_stage(OAuthDeviceCodeStage))
|
plan.append_stage(in_memory_stage(OAuthDeviceCodeStage))
|
||||||
|
|
||||||
self.request.session[SESSION_KEY_PLAN] = plan
|
self.request.session[SESSION_KEY_PLAN] = plan
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"""authentik SAML IDP Views"""
|
"""authentik SAML IDP Views"""
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import Http404, HttpRequest, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
@ -11,6 +11,7 @@ from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
|
from authentik.flows.exceptions import FlowNonApplicableException
|
||||||
from authentik.flows.models import in_memory_stage
|
from authentik.flows.models import in_memory_stage
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
|
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
|
||||||
from authentik.flows.views.executor import SESSION_KEY_PLAN, SESSION_KEY_POST
|
from authentik.flows.views.executor import SESSION_KEY_PLAN, SESSION_KEY_POST
|
||||||
|
@ -60,16 +61,19 @@ class SAMLSSOView(PolicyAccessView):
|
||||||
# Regardless, we start the planner and return to it
|
# Regardless, we start the planner and return to it
|
||||||
planner = FlowPlanner(self.provider.authorization_flow)
|
planner = FlowPlanner(self.provider.authorization_flow)
|
||||||
planner.allow_empty_flows = True
|
planner.allow_empty_flows = True
|
||||||
plan = planner.plan(
|
try:
|
||||||
request,
|
plan = planner.plan(
|
||||||
{
|
request,
|
||||||
PLAN_CONTEXT_SSO: True,
|
{
|
||||||
PLAN_CONTEXT_APPLICATION: self.application,
|
PLAN_CONTEXT_SSO: True,
|
||||||
PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
|
PLAN_CONTEXT_APPLICATION: self.application,
|
||||||
% {"application": self.application.name},
|
PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
|
||||||
PLAN_CONTEXT_CONSENT_PERMISSIONS: [],
|
% {"application": self.application.name},
|
||||||
},
|
PLAN_CONTEXT_CONSENT_PERMISSIONS: [],
|
||||||
)
|
},
|
||||||
|
)
|
||||||
|
except FlowNonApplicableException:
|
||||||
|
raise Http404
|
||||||
plan.append_stage(in_memory_stage(SAMLFlowFinalView))
|
plan.append_stage(in_memory_stage(SAMLFlowFinalView))
|
||||||
request.session[SESSION_KEY_PLAN] = plan
|
request.session[SESSION_KEY_PLAN] = plan
|
||||||
return redirect_with_qs(
|
return redirect_with_qs(
|
||||||
|
|
|
@ -22,6 +22,7 @@ from authentik.flows.challenge import (
|
||||||
ChallengeResponse,
|
ChallengeResponse,
|
||||||
ChallengeTypes,
|
ChallengeTypes,
|
||||||
)
|
)
|
||||||
|
from authentik.flows.exceptions import FlowNonApplicableException
|
||||||
from authentik.flows.models import in_memory_stage
|
from authentik.flows.models import in_memory_stage
|
||||||
from authentik.flows.planner import (
|
from authentik.flows.planner import (
|
||||||
PLAN_CONTEXT_REDIRECT,
|
PLAN_CONTEXT_REDIRECT,
|
||||||
|
@ -87,7 +88,10 @@ class InitiateView(View):
|
||||||
# We run the Flow planner here so we can pass the Pending user in the context
|
# We run the Flow planner here so we can pass the Pending user in the context
|
||||||
planner = FlowPlanner(source.pre_authentication_flow)
|
planner = FlowPlanner(source.pre_authentication_flow)
|
||||||
planner.allow_empty_flows = True
|
planner.allow_empty_flows = True
|
||||||
plan = planner.plan(self.request, kwargs)
|
try:
|
||||||
|
plan = planner.plan(self.request, kwargs)
|
||||||
|
except FlowNonApplicableException:
|
||||||
|
raise Http404
|
||||||
for stage in stages_to_append:
|
for stage in stages_to_append:
|
||||||
plan.append_stage(stage)
|
plan.append_stage(stage)
|
||||||
self.request.session[SESSION_KEY_PLAN] = plan
|
self.request.session[SESSION_KEY_PLAN] = plan
|
||||||
|
|
|
@ -13,9 +13,9 @@ from authentik.flows.markers import StageMarker
|
||||||
from authentik.flows.models import FlowDesignation, FlowStageBinding
|
from authentik.flows.models import FlowDesignation, FlowStageBinding
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||||
from authentik.flows.tests import FlowTestCase
|
from authentik.flows.tests import FlowTestCase
|
||||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
from authentik.flows.views.executor import QS_KEY_TOKEN, SESSION_KEY_PLAN
|
||||||
from authentik.stages.email.models import EmailStage
|
from authentik.stages.email.models import EmailStage
|
||||||
from authentik.stages.email.stage import PLAN_CONTEXT_EMAIL_OVERRIDE, QS_KEY_TOKEN
|
from authentik.stages.email.stage import PLAN_CONTEXT_EMAIL_OVERRIDE
|
||||||
|
|
||||||
|
|
||||||
class TestEmailStage(FlowTestCase):
|
class TestEmailStage(FlowTestCase):
|
||||||
|
|
|
@ -2,8 +2,17 @@ version: '3.7'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
chrome:
|
chrome:
|
||||||
image: selenium/standalone-chrome:103.0-chromedriver-103.0
|
image: selenium/standalone-chrome:110.0
|
||||||
volumes:
|
volumes:
|
||||||
- /dev/shm:/dev/shm
|
- /dev/shm:/dev/shm
|
||||||
network_mode: host
|
network_mode: host
|
||||||
restart: always
|
restart: always
|
||||||
|
mailhog:
|
||||||
|
image: mailhog/mailhog:v1.0.1
|
||||||
|
ports:
|
||||||
|
- 1025:1025
|
||||||
|
- 8025:8025
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--spider", "http://localhost:8025"]
|
||||||
|
interval: 5s
|
||||||
|
start_period: 1s
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
"""test flow with otp stages"""
|
"""test flow with otp stages"""
|
||||||
from base64 import b32decode
|
from base64 import b32decode
|
||||||
from sys import platform
|
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from unittest.case import skipUnless
|
|
||||||
from urllib.parse import parse_qs, urlparse
|
from urllib.parse import parse_qs, urlparse
|
||||||
|
|
||||||
from django_otp.oath import TOTP
|
from django_otp.oath import TOTP
|
||||||
|
@ -20,7 +18,6 @@ from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage
|
||||||
from tests.e2e.utils import SeleniumTestCase, retry
|
from tests.e2e.utils import SeleniumTestCase, retry
|
||||||
|
|
||||||
|
|
||||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
|
||||||
class TestFlowsAuthenticator(SeleniumTestCase):
|
class TestFlowsAuthenticator(SeleniumTestCase):
|
||||||
"""test flow with otp stages"""
|
"""test flow with otp stages"""
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
"""Test Enroll flow"""
|
"""Test Enroll flow"""
|
||||||
from sys import platform
|
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from typing import Any, Optional
|
|
||||||
from unittest.case import skipUnless
|
|
||||||
|
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from docker.types import Healthcheck
|
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
from selenium.webdriver.support import expected_conditions as ec
|
from selenium.webdriver.support import expected_conditions as ec
|
||||||
from selenium.webdriver.support.wait import WebDriverWait
|
from selenium.webdriver.support.wait import WebDriverWait
|
||||||
|
@ -17,23 +13,9 @@ from authentik.stages.identification.models import IdentificationStage
|
||||||
from tests.e2e.utils import SeleniumTestCase, retry
|
from tests.e2e.utils import SeleniumTestCase, retry
|
||||||
|
|
||||||
|
|
||||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
|
||||||
class TestFlowsEnroll(SeleniumTestCase):
|
class TestFlowsEnroll(SeleniumTestCase):
|
||||||
"""Test Enroll flow"""
|
"""Test Enroll flow"""
|
||||||
|
|
||||||
def get_container_specs(self) -> Optional[dict[str, Any]]:
|
|
||||||
return {
|
|
||||||
"image": "mailhog/mailhog:v1.0.1",
|
|
||||||
"detach": True,
|
|
||||||
"network_mode": "host",
|
|
||||||
"auto_remove": True,
|
|
||||||
"healthcheck": Healthcheck(
|
|
||||||
test=["CMD", "wget", "--spider", "http://localhost:8025"],
|
|
||||||
interval=5 * 100 * 1000000,
|
|
||||||
start_period=1 * 100 * 1000000,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
@retry()
|
@retry()
|
||||||
@apply_blueprint(
|
@apply_blueprint(
|
||||||
"default/flow-default-authentication-flow.yaml",
|
"default/flow-default-authentication-flow.yaml",
|
||||||
|
|
|
@ -1,12 +1,8 @@
|
||||||
"""test default login flow"""
|
"""test default login flow"""
|
||||||
from sys import platform
|
|
||||||
from unittest.case import skipUnless
|
|
||||||
|
|
||||||
from authentik.blueprints.tests import apply_blueprint
|
from authentik.blueprints.tests import apply_blueprint
|
||||||
from tests.e2e.utils import SeleniumTestCase, retry
|
from tests.e2e.utils import SeleniumTestCase, retry
|
||||||
|
|
||||||
|
|
||||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
|
||||||
class TestFlowsLogin(SeleniumTestCase):
|
class TestFlowsLogin(SeleniumTestCase):
|
||||||
"""test default login flow"""
|
"""test default login flow"""
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
"""Test recovery flow"""
|
"""Test recovery flow"""
|
||||||
from sys import platform
|
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from typing import Any, Optional
|
|
||||||
from unittest.case import skipUnless
|
|
||||||
|
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from docker.types import Healthcheck
|
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
from selenium.webdriver.support import expected_conditions as ec
|
from selenium.webdriver.support import expected_conditions as ec
|
||||||
from selenium.webdriver.support.wait import WebDriverWait
|
from selenium.webdriver.support.wait import WebDriverWait
|
||||||
|
@ -19,23 +15,9 @@ from authentik.stages.identification.models import IdentificationStage
|
||||||
from tests.e2e.utils import SeleniumTestCase, retry
|
from tests.e2e.utils import SeleniumTestCase, retry
|
||||||
|
|
||||||
|
|
||||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
|
||||||
class TestFlowsRecovery(SeleniumTestCase):
|
class TestFlowsRecovery(SeleniumTestCase):
|
||||||
"""Test Recovery flow"""
|
"""Test Recovery flow"""
|
||||||
|
|
||||||
def get_container_specs(self) -> Optional[dict[str, Any]]:
|
|
||||||
return {
|
|
||||||
"image": "mailhog/mailhog:v1.0.1",
|
|
||||||
"detach": True,
|
|
||||||
"network_mode": "host",
|
|
||||||
"auto_remove": True,
|
|
||||||
"healthcheck": Healthcheck(
|
|
||||||
test=["CMD", "wget", "--spider", "http://localhost:8025"],
|
|
||||||
interval=5 * 100 * 1000000,
|
|
||||||
start_period=1 * 100 * 1000000,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
def initial_stages(self, user: User):
|
def initial_stages(self, user: User):
|
||||||
"""Fill out initial stages"""
|
"""Fill out initial stages"""
|
||||||
# Identification stage, click recovery
|
# Identification stage, click recovery
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
"""test stage setup flows (password change)"""
|
"""test stage setup flows (password change)"""
|
||||||
from sys import platform
|
|
||||||
from unittest.case import skipUnless
|
|
||||||
|
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
from selenium.webdriver.common.keys import Keys
|
from selenium.webdriver.common.keys import Keys
|
||||||
|
|
||||||
|
@ -13,7 +10,6 @@ from authentik.stages.password.models import PasswordStage
|
||||||
from tests.e2e.utils import SeleniumTestCase, retry
|
from tests.e2e.utils import SeleniumTestCase, retry
|
||||||
|
|
||||||
|
|
||||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
|
||||||
class TestFlowsStageSetup(SeleniumTestCase):
|
class TestFlowsStageSetup(SeleniumTestCase):
|
||||||
"""test stage setup flows"""
|
"""test stage setup flows"""
|
||||||
|
|
||||||
|
|
Reference in New Issue