flows: add FlowStageBinding to flow plan instead of just stage

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2021-06-27 18:47:04 +02:00
parent 5431e7fe9d
commit 3b2b3262d7
18 changed files with 151 additions and 108 deletions

View file

@ -6,7 +6,7 @@ from django.http.request import HttpRequest
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.models import User from authentik.core.models import User
from authentik.flows.models import Stage from authentik.flows.models import FlowStageBinding
from authentik.policies.engine import PolicyEngine from authentik.policies.engine import PolicyEngine
from authentik.policies.models import PolicyBinding from authentik.policies.models import PolicyBinding
@ -22,11 +22,14 @@ class StageMarker:
# pylint: disable=unused-argument # pylint: disable=unused-argument
def process( def process(
self, plan: "FlowPlan", stage: Stage, http_request: Optional[HttpRequest] self,
) -> Optional[Stage]: plan: "FlowPlan",
binding: FlowStageBinding,
http_request: Optional[HttpRequest],
) -> Optional[FlowStageBinding]:
"""Process callback for this marker. This should be overridden by sub-classes. """Process callback for this marker. This should be overridden by sub-classes.
If a stage should be removed, return None.""" If a stage should be removed, return None."""
return stage return binding
@dataclass @dataclass
@ -37,13 +40,16 @@ class ReevaluateMarker(StageMarker):
user: User user: User
def process( def process(
self, plan: "FlowPlan", stage: Stage, http_request: Optional[HttpRequest] self,
) -> Optional[Stage]: plan: "FlowPlan",
binding: FlowStageBinding,
http_request: Optional[HttpRequest],
) -> Optional[FlowStageBinding]:
"""Re-evaluate policies bound to stage, and if they fail, remove from plan""" """Re-evaluate policies bound to stage, and if they fail, remove from plan"""
LOGGER.debug( LOGGER.debug(
"f(plan_inst)[re-eval marker]: running re-evaluation", "f(plan_inst)[re-eval marker]: running re-evaluation",
stage=stage, binding=binding,
binding=self.binding, policy_binding=self.binding,
) )
engine = PolicyEngine(self.binding, self.user) engine = PolicyEngine(self.binding, self.user)
engine.use_cache = False engine.use_cache = False
@ -53,10 +59,10 @@ class ReevaluateMarker(StageMarker):
engine.build() engine.build()
result = engine.result result = engine.result
if result.passing: if result.passing:
return stage return binding
LOGGER.warning( LOGGER.warning(
"f(plan_inst)[re-eval marker]: stage failed re-evaluation", "f(plan_inst)[re-eval marker]: binding failed re-evaluation",
stage=stage, binding=binding,
messages=result.messages, messages=result.messages,
) )
return None return None

View file

@ -13,7 +13,7 @@ from authentik.core.models import User
from authentik.events.models import cleanse_dict from authentik.events.models import cleanse_dict
from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException
from authentik.flows.markers import ReevaluateMarker, StageMarker from authentik.flows.markers import ReevaluateMarker, StageMarker
from authentik.flows.models import Flow, FlowStageBinding, Stage from authentik.flows.models import Flow, FlowStageBinding
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.policies.engine import PolicyEngine from authentik.policies.engine import PolicyEngine
from authentik.root.monitoring import UpdatingGauge from authentik.root.monitoring import UpdatingGauge
@ -52,33 +52,37 @@ class FlowPlan:
flow_pk: str flow_pk: str
stages: list[Stage] = field(default_factory=list) bindings: list[FlowStageBinding] = field(default_factory=list)
context: dict[str, Any] = field(default_factory=dict) context: dict[str, Any] = field(default_factory=dict)
markers: list[StageMarker] = field(default_factory=list) markers: list[StageMarker] = field(default_factory=list)
def append(self, stage: Stage, marker: Optional[StageMarker] = None): def append(self, binding: FlowStageBinding, marker: Optional[StageMarker] = None):
"""Append `stage` to all stages, optionall with stage marker""" """Append `stage` to all stages, optionall with stage marker"""
self.stages.append(stage) self.bindings.append(binding)
self.markers.append(marker or StageMarker()) self.markers.append(marker or StageMarker())
def insert(self, stage: Stage, marker: Optional[StageMarker] = None): def insert(self, binding: FlowStageBinding, marker: Optional[StageMarker] = None):
"""Insert stage into plan, as immediate next stage""" """Insert stage into plan, as immediate next stage"""
self.stages.insert(1, stage) self.bindings.insert(1, binding)
self.markers.insert(1, marker or StageMarker()) self.markers.insert(1, marker or StageMarker())
def next(self, http_request: Optional[HttpRequest]) -> Optional[Stage]: def next(self, http_request: Optional[HttpRequest]) -> Optional[FlowStageBinding]:
"""Return next pending stage from the bottom of the list""" """Return next pending stage from the bottom of the list"""
if not self.has_stages: if not self.has_stages:
return None return None
stage = self.stages[0] binding = self.bindings[0]
marker = self.markers[0] marker = self.markers[0]
if marker.__class__ is not StageMarker: if marker.__class__ is not StageMarker:
LOGGER.debug("f(plan_inst): stage has marker", stage=stage, marker=marker) LOGGER.debug(
marked_stage = marker.process(self, stage, http_request) "f(plan_inst): stage has marker", binding=binding, marker=marker
)
marked_stage = marker.process(self, binding, http_request)
if not marked_stage: if not marked_stage:
LOGGER.debug("f(plan_inst): marker returned none, next stage", stage=stage) LOGGER.debug(
self.stages.remove(stage) "f(plan_inst): marker returned none, next stage", binding=binding
)
self.bindings.remove(binding)
self.markers.remove(marker) self.markers.remove(marker)
if not self.has_stages: if not self.has_stages:
return None return None
@ -89,12 +93,12 @@ class FlowPlan:
def pop(self): def pop(self):
"""Pop next pending stage from bottom of list""" """Pop next pending stage from bottom of list"""
self.markers.pop(0) self.markers.pop(0)
self.stages.pop(0) self.bindings.pop(0)
@property @property
def has_stages(self) -> bool: def has_stages(self) -> bool:
"""Check if there are any stages left in this plan""" """Check if there are any stages left in this plan"""
return len(self.markers) + len(self.stages) > 0 return len(self.markers) + len(self.bindings) > 0
class FlowPlanner: class FlowPlanner:
@ -161,7 +165,7 @@ class FlowPlanner:
plan = self._build_plan(user, request, default_context) plan = self._build_plan(user, request, default_context)
cache.set(cache_key(self.flow, user), plan, CACHE_TIMEOUT) cache.set(cache_key(self.flow, user), plan, CACHE_TIMEOUT)
GAUGE_FLOWS_CACHED.update() GAUGE_FLOWS_CACHED.update()
if not plan.stages and not self.allow_empty_flows: if not plan.bindings and not self.allow_empty_flows:
raise EmptyFlowException() raise EmptyFlowException()
return plan return plan
@ -218,7 +222,7 @@ class FlowPlanner:
) )
marker = ReevaluateMarker(binding=binding, user=user) marker = ReevaluateMarker(binding=binding, user=user)
if stage: if stage:
plan.append(stage, marker) plan.append(binding, marker)
HIST_FLOWS_PLAN_TIME.labels(flow_slug=self.flow.slug) HIST_FLOWS_PLAN_TIME.labels(flow_slug=self.flow.slug)
self._logger.debug( self._logger.debug(
"f(plan): finished building", "f(plan): finished building",

View file

@ -182,8 +182,8 @@ class TestFlowPlanner(TestCase):
planner = FlowPlanner(flow) planner = FlowPlanner(flow)
plan = planner.plan(request) plan = planner.plan(request)
self.assertEqual(plan.stages[0], binding.stage) self.assertEqual(plan.bindings[0], binding)
self.assertEqual(plan.stages[1], binding2.stage) self.assertEqual(plan.bindings[1], binding2)
self.assertIsInstance(plan.markers[0], StageMarker) self.assertIsInstance(plan.markers[0], StageMarker)
self.assertIsInstance(plan.markers[1], ReevaluateMarker) self.assertIsInstance(plan.markers[1], ReevaluateMarker)

View file

@ -52,8 +52,9 @@ class TestFlowExecutor(TestCase):
designation=FlowDesignation.AUTHENTICATION, designation=FlowDesignation.AUTHENTICATION,
) )
stage = DummyStage.objects.create(name="dummy") stage = DummyStage.objects.create(name="dummy")
binding = FlowStageBinding.objects.create(target=flow, stage=stage)
plan = FlowPlan( plan = FlowPlan(
flow_pk=flow.pk.hex + "a", stages=[stage], markers=[StageMarker()] flow_pk=flow.pk.hex + "a", bindings=[binding], markers=[StageMarker()]
) )
session = self.client.session session = self.client.session
session[SESSION_KEY_PLAN] = plan session[SESSION_KEY_PLAN] = plan
@ -163,7 +164,7 @@ class TestFlowExecutor(TestCase):
# Check that two stages are in plan # Check that two stages are in plan
session = self.client.session session = self.client.session
plan: FlowPlan = session[SESSION_KEY_PLAN] plan: FlowPlan = session[SESSION_KEY_PLAN]
self.assertEqual(len(plan.stages), 2) self.assertEqual(len(plan.bindings), 2)
# Second request, submit form, one stage left # Second request, submit form, one stage left
response = self.client.post(exec_url) response = self.client.post(exec_url)
# Second request redirects to the same URL # Second request redirects to the same URL
@ -172,7 +173,7 @@ class TestFlowExecutor(TestCase):
# Check that two stages are in plan # Check that two stages are in plan
session = self.client.session session = self.client.session
plan: FlowPlan = session[SESSION_KEY_PLAN] plan: FlowPlan = session[SESSION_KEY_PLAN]
self.assertEqual(len(plan.stages), 1) self.assertEqual(len(plan.bindings), 1)
@patch( @patch(
"authentik.flows.views.to_stage_response", "authentik.flows.views.to_stage_response",
@ -213,8 +214,8 @@ class TestFlowExecutor(TestCase):
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN] plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
self.assertEqual(plan.stages[0], binding.stage) self.assertEqual(plan.bindings[0], binding)
self.assertEqual(plan.stages[1], binding2.stage) self.assertEqual(plan.bindings[1], binding2)
self.assertIsInstance(plan.markers[0], StageMarker) self.assertIsInstance(plan.markers[0], StageMarker)
self.assertIsInstance(plan.markers[1], ReevaluateMarker) self.assertIsInstance(plan.markers[1], ReevaluateMarker)
@ -267,9 +268,9 @@ class TestFlowExecutor(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN] plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
self.assertEqual(plan.stages[0], binding.stage) self.assertEqual(plan.bindings[0], binding)
self.assertEqual(plan.stages[1], binding2.stage) self.assertEqual(plan.bindings[1], binding2)
self.assertEqual(plan.stages[2], binding3.stage) self.assertEqual(plan.bindings[2], binding3)
self.assertIsInstance(plan.markers[0], StageMarker) self.assertIsInstance(plan.markers[0], StageMarker)
self.assertIsInstance(plan.markers[1], ReevaluateMarker) self.assertIsInstance(plan.markers[1], ReevaluateMarker)
@ -281,8 +282,8 @@ class TestFlowExecutor(TestCase):
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN] plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
self.assertEqual(plan.stages[0], binding2.stage) self.assertEqual(plan.bindings[0], binding2)
self.assertEqual(plan.stages[1], binding3.stage) self.assertEqual(plan.bindings[1], binding3)
self.assertIsInstance(plan.markers[0], StageMarker) self.assertIsInstance(plan.markers[0], StageMarker)
self.assertIsInstance(plan.markers[1], StageMarker) self.assertIsInstance(plan.markers[1], StageMarker)
@ -338,9 +339,9 @@ class TestFlowExecutor(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN] plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
self.assertEqual(plan.stages[0], binding.stage) self.assertEqual(plan.bindings[0], binding)
self.assertEqual(plan.stages[1], binding2.stage) self.assertEqual(plan.bindings[1], binding2)
self.assertEqual(plan.stages[2], binding3.stage) self.assertEqual(plan.bindings[2], binding3)
self.assertIsInstance(plan.markers[0], StageMarker) self.assertIsInstance(plan.markers[0], StageMarker)
self.assertIsInstance(plan.markers[1], ReevaluateMarker) self.assertIsInstance(plan.markers[1], ReevaluateMarker)
@ -352,8 +353,8 @@ class TestFlowExecutor(TestCase):
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN] plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
self.assertEqual(plan.stages[0], binding2.stage) self.assertEqual(plan.bindings[0], binding2)
self.assertEqual(plan.stages[1], binding3.stage) self.assertEqual(plan.bindings[1], binding3)
self.assertIsInstance(plan.markers[0], StageMarker) self.assertIsInstance(plan.markers[0], StageMarker)
self.assertIsInstance(plan.markers[1], StageMarker) self.assertIsInstance(plan.markers[1], StageMarker)
@ -364,7 +365,7 @@ class TestFlowExecutor(TestCase):
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN] plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
self.assertEqual(plan.stages[0], binding3.stage) self.assertEqual(plan.bindings[0], binding3)
self.assertIsInstance(plan.markers[0], StageMarker) self.assertIsInstance(plan.markers[0], StageMarker)
@ -438,10 +439,10 @@ class TestFlowExecutor(TestCase):
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN] plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
self.assertEqual(plan.stages[0], binding.stage) self.assertEqual(plan.bindings[0], binding)
self.assertEqual(plan.stages[1], binding2.stage) self.assertEqual(plan.bindings[1], binding2)
self.assertEqual(plan.stages[2], binding3.stage) self.assertEqual(plan.bindings[2], binding3)
self.assertEqual(plan.stages[3], binding4.stage) self.assertEqual(plan.bindings[3], binding4)
self.assertIsInstance(plan.markers[0], StageMarker) self.assertIsInstance(plan.markers[0], StageMarker)
self.assertIsInstance(plan.markers[1], ReevaluateMarker) self.assertIsInstance(plan.markers[1], ReevaluateMarker)

View file

@ -37,7 +37,13 @@ from authentik.flows.challenge import (
WithUserInfoChallenge, WithUserInfoChallenge,
) )
from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException
from authentik.flows.models import ConfigurableStage, Flow, FlowDesignation, Stage from authentik.flows.models import (
ConfigurableStage,
Flow,
FlowDesignation,
FlowStageBinding,
Stage,
)
from authentik.flows.planner import ( from authentik.flows.planner import (
PLAN_CONTEXT_PENDING_USER, PLAN_CONTEXT_PENDING_USER,
PLAN_CONTEXT_REDIRECT, PLAN_CONTEXT_REDIRECT,
@ -107,6 +113,7 @@ class FlowExecutorView(APIView):
flow: Flow flow: Flow
plan: Optional[FlowPlan] = None plan: Optional[FlowPlan] = None
current_binding: FlowStageBinding
current_stage: Stage current_stage: Stage
current_stage_view: View current_stage_view: View
@ -159,11 +166,12 @@ class FlowExecutorView(APIView):
request.session[SESSION_KEY_GET] = QueryDict(request.GET.get("query", "")) request.session[SESSION_KEY_GET] = QueryDict(request.GET.get("query", ""))
# We don't save the Plan after getting the next stage # We don't save the Plan after getting the next stage
# as it hasn't been successfully passed yet # as it hasn't been successfully passed yet
next_stage = self.plan.next(self.request) next_binding = self.plan.next(self.request)
if not next_stage: if not next_binding:
self._logger.debug("f(exec): no more stages, flow is done.") self._logger.debug("f(exec): no more stages, flow is done.")
return self._flow_done() return self._flow_done()
self.current_stage = next_stage self.current_binding = next_binding
self.current_stage = next_binding.stage
self._logger.debug( self._logger.debug(
"f(exec): Current stage", "f(exec): Current stage",
current_stage=self.current_stage, current_stage=self.current_stage,
@ -293,10 +301,10 @@ class FlowExecutorView(APIView):
) )
self.plan.pop() self.plan.pop()
self.request.session[SESSION_KEY_PLAN] = self.plan self.request.session[SESSION_KEY_PLAN] = self.plan
if self.plan.stages: if self.plan.bindings:
self._logger.debug( self._logger.debug(
"f(exec): Continuing with next stage", "f(exec): Continuing with next stage",
remaining=len(self.plan.stages), remaining=len(self.plan.bindings),
) )
kwargs = self.kwargs kwargs = self.kwargs
kwargs.update({"flow_slug": self.flow.slug}) kwargs.update({"flow_slug": self.flow.slug})

View file

@ -468,7 +468,7 @@ class AuthorizationFlowInitView(PolicyAccessView):
# 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_CONSNET in self.params.prompt: if PROMPT_CONSNET in self.params.prompt:
if not any(isinstance(x, ConsentStageView) for x in plan.stages): if not any(isinstance(x.stage, ConsentStageView) for x in plan.bindings):
# Plan does not have any consent stage, so we add an in-memory one # Plan does not have any consent stage, so we add an in-memory one
stage = ConsentStage( stage = ConsentStage(
name="OAuth2 Provider In-memory consent stage", name="OAuth2 Provider In-memory consent stage",

View file

@ -36,12 +36,14 @@ class TestCaptchaStage(TestCase):
public_key=RECAPTCHA_PUBLIC_KEY, public_key=RECAPTCHA_PUBLIC_KEY,
private_key=RECAPTCHA_PRIVATE_KEY, private_key=RECAPTCHA_PRIVATE_KEY,
) )
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) self.binding = FlowStageBinding.objects.create(
target=self.flow, stage=self.stage, order=2
)
def test_valid(self): def test_valid(self):
"""Test valid captcha""" """Test valid captcha"""
plan = FlowPlan( plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
) )
session = self.client.session session = self.client.session
session[SESSION_KEY_PLAN] = plan session[SESSION_KEY_PLAN] = plan

View file

@ -39,9 +39,11 @@ class TestConsentStage(TestCase):
stage = ConsentStage.objects.create( stage = ConsentStage.objects.create(
name="consent", mode=ConsentMode.ALWAYS_REQUIRE name="consent", mode=ConsentMode.ALWAYS_REQUIRE
) )
FlowStageBinding.objects.create(target=flow, stage=stage, order=2) binding = FlowStageBinding.objects.create(target=flow, stage=stage, order=2)
plan = FlowPlan(flow_pk=flow.pk.hex, stages=[stage], markers=[StageMarker()]) plan = FlowPlan(
flow_pk=flow.pk.hex, bindings=[binding], markers=[StageMarker()]
)
session = self.client.session session = self.client.session
session[SESSION_KEY_PLAN] = plan session[SESSION_KEY_PLAN] = plan
session.save() session.save()
@ -69,11 +71,11 @@ class TestConsentStage(TestCase):
designation=FlowDesignation.AUTHENTICATION, designation=FlowDesignation.AUTHENTICATION,
) )
stage = ConsentStage.objects.create(name="consent", mode=ConsentMode.PERMANENT) stage = ConsentStage.objects.create(name="consent", mode=ConsentMode.PERMANENT)
FlowStageBinding.objects.create(target=flow, stage=stage, order=2) binding = FlowStageBinding.objects.create(target=flow, stage=stage, order=2)
plan = FlowPlan( plan = FlowPlan(
flow_pk=flow.pk.hex, flow_pk=flow.pk.hex,
stages=[stage], bindings=[binding],
markers=[StageMarker()], markers=[StageMarker()],
context={PLAN_CONTEXT_APPLICATION: self.application}, context={PLAN_CONTEXT_APPLICATION: self.application},
) )
@ -110,11 +112,11 @@ class TestConsentStage(TestCase):
stage = ConsentStage.objects.create( stage = ConsentStage.objects.create(
name="consent", mode=ConsentMode.EXPIRING, consent_expire_in="seconds=1" name="consent", mode=ConsentMode.EXPIRING, consent_expire_in="seconds=1"
) )
FlowStageBinding.objects.create(target=flow, stage=stage, order=2) binding = FlowStageBinding.objects.create(target=flow, stage=stage, order=2)
plan = FlowPlan( plan = FlowPlan(
flow_pk=flow.pk.hex, flow_pk=flow.pk.hex,
stages=[stage], bindings=[binding],
markers=[StageMarker()], markers=[StageMarker()],
context={PLAN_CONTEXT_APPLICATION: self.application}, context={PLAN_CONTEXT_APPLICATION: self.application},
) )

View file

@ -26,12 +26,14 @@ class TestUserDenyStage(TestCase):
designation=FlowDesignation.AUTHENTICATION, designation=FlowDesignation.AUTHENTICATION,
) )
self.stage = DenyStage.objects.create(name="logout") self.stage = DenyStage.objects.create(name="logout")
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) self.binding = FlowStageBinding.objects.create(
target=self.flow, stage=self.stage, order=2
)
def test_valid_password(self): def test_valid_password(self):
"""Test with a valid pending user and backend""" """Test with a valid pending user and backend"""
plan = FlowPlan( plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
) )
session = self.client.session session = self.client.session
session[SESSION_KEY_PLAN] = plan session[SESSION_KEY_PLAN] = plan

View file

@ -34,12 +34,14 @@ class TestEmailStageSending(TestCase):
self.stage = EmailStage.objects.create( self.stage = EmailStage.objects.create(
name="email", name="email",
) )
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) self.binding = FlowStageBinding.objects.create(
target=self.flow, stage=self.stage, order=2
)
def test_pending_user(self): def test_pending_user(self):
"""Test with pending user""" """Test with pending user"""
plan = FlowPlan( plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
) )
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session session = self.client.session
@ -67,7 +69,7 @@ class TestEmailStageSending(TestCase):
def test_send_error(self): def test_send_error(self):
"""Test error during sending (sending will be retried)""" """Test error during sending (sending will be retried)"""
plan = FlowPlan( plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
) )
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session session = self.client.session

View file

@ -35,12 +35,14 @@ class TestEmailStage(TestCase):
self.stage = EmailStage.objects.create( self.stage = EmailStage.objects.create(
name="email", name="email",
) )
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) self.binding = FlowStageBinding.objects.create(
target=self.flow, stage=self.stage, order=2
)
def test_rendering(self): def test_rendering(self):
"""Test with pending user""" """Test with pending user"""
plan = FlowPlan( plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
) )
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session session = self.client.session
@ -56,7 +58,7 @@ class TestEmailStage(TestCase):
def test_without_user(self): def test_without_user(self):
"""Test without pending user""" """Test without pending user"""
plan = FlowPlan( plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
) )
session = self.client.session session = self.client.session
session[SESSION_KEY_PLAN] = plan session[SESSION_KEY_PLAN] = plan
@ -71,7 +73,7 @@ class TestEmailStage(TestCase):
def test_pending_user(self): def test_pending_user(self):
"""Test with pending user""" """Test with pending user"""
plan = FlowPlan( plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
) )
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session session = self.client.session
@ -102,7 +104,7 @@ class TestEmailStage(TestCase):
# Make sure token exists # Make sure token exists
self.test_pending_user() self.test_pending_user()
plan = FlowPlan( plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
) )
session = self.client.session session = self.client.session
session[SESSION_KEY_PLAN] = plan session[SESSION_KEY_PLAN] = plan

View file

@ -35,7 +35,9 @@ class TestUserLoginStage(TestCase):
designation=FlowDesignation.AUTHENTICATION, designation=FlowDesignation.AUTHENTICATION,
) )
self.stage = InvitationStage.objects.create(name="invitation") self.stage = InvitationStage.objects.create(name="invitation")
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) self.binding = FlowStageBinding.objects.create(
target=self.flow, stage=self.stage, order=2
)
@patch( @patch(
"authentik.flows.views.to_stage_response", "authentik.flows.views.to_stage_response",
@ -44,7 +46,7 @@ class TestUserLoginStage(TestCase):
def test_without_invitation_fail(self): def test_without_invitation_fail(self):
"""Test without any invitation, continue_flow_without_invitation not set.""" """Test without any invitation, continue_flow_without_invitation not set."""
plan = FlowPlan( plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
) )
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_DJANGO plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_DJANGO
@ -75,7 +77,7 @@ class TestUserLoginStage(TestCase):
self.stage.continue_flow_without_invitation = True self.stage.continue_flow_without_invitation = True
self.stage.save() self.stage.save()
plan = FlowPlan( plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
) )
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_DJANGO plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_DJANGO
@ -103,7 +105,7 @@ class TestUserLoginStage(TestCase):
def test_with_invitation_get(self): def test_with_invitation_get(self):
"""Test with invitation, check data in session""" """Test with invitation, check data in session"""
plan = FlowPlan( plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
) )
session = self.client.session session = self.client.session
session[SESSION_KEY_PLAN] = plan session[SESSION_KEY_PLAN] = plan
@ -143,7 +145,7 @@ class TestUserLoginStage(TestCase):
) )
plan = FlowPlan( plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
) )
plan.context[PLAN_CONTEXT_PROMPT] = {INVITATION_TOKEN_KEY: invite.pk.hex} plan.context[PLAN_CONTEXT_PROMPT] = {INVITATION_TOKEN_KEY: invite.pk.hex}
session = self.client.session session = self.client.session

View file

@ -39,7 +39,9 @@ class TestPasswordStage(TestCase):
self.stage = PasswordStage.objects.create( self.stage = PasswordStage.objects.create(
name="password", backends=[BACKEND_DJANGO] name="password", backends=[BACKEND_DJANGO]
) )
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) self.binding = FlowStageBinding.objects.create(
target=self.flow, stage=self.stage, order=2
)
@patch( @patch(
"authentik.flows.views.to_stage_response", "authentik.flows.views.to_stage_response",
@ -48,7 +50,7 @@ class TestPasswordStage(TestCase):
def test_without_user(self): def test_without_user(self):
"""Test without user""" """Test without user"""
plan = FlowPlan( plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
) )
session = self.client.session session = self.client.session
session[SESSION_KEY_PLAN] = plan session[SESSION_KEY_PLAN] = plan
@ -84,7 +86,7 @@ class TestPasswordStage(TestCase):
) )
plan = FlowPlan( plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
) )
session = self.client.session session = self.client.session
session[SESSION_KEY_PLAN] = plan session[SESSION_KEY_PLAN] = plan
@ -101,7 +103,7 @@ class TestPasswordStage(TestCase):
def test_valid_password(self): def test_valid_password(self):
"""Test with a valid pending user and valid password""" """Test with a valid pending user and valid password"""
plan = FlowPlan( plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
) )
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session session = self.client.session
@ -129,7 +131,7 @@ class TestPasswordStage(TestCase):
def test_invalid_password(self): def test_invalid_password(self):
"""Test with a valid pending user and invalid password""" """Test with a valid pending user and invalid password"""
plan = FlowPlan( plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
) )
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session session = self.client.session
@ -148,7 +150,7 @@ class TestPasswordStage(TestCase):
def test_invalid_password_lockout(self): def test_invalid_password_lockout(self):
"""Test with a valid pending user and invalid password (trigger logout counter)""" """Test with a valid pending user and invalid password (trigger logout counter)"""
plan = FlowPlan( plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
) )
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session session = self.client.session
@ -189,7 +191,7 @@ class TestPasswordStage(TestCase):
"""Test with a valid pending user and valid password. """Test with a valid pending user and valid password.
Backend is patched to return PermissionError""" Backend is patched to return PermissionError"""
plan = FlowPlan( plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
) )
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session session = self.client.session

View file

@ -102,12 +102,14 @@ class TestPromptStage(TestCase):
hidden_prompt.field_key: hidden_prompt.placeholder, hidden_prompt.field_key: hidden_prompt.placeholder,
} }
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) self.binding = FlowStageBinding.objects.create(
target=self.flow, stage=self.stage, order=2
)
def test_render(self): def test_render(self):
"""Test render of form, check if all prompts are rendered correctly""" """Test render of form, check if all prompts are rendered correctly"""
plan = FlowPlan( plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
) )
session = self.client.session session = self.client.session
session[SESSION_KEY_PLAN] = plan session[SESSION_KEY_PLAN] = plan
@ -125,7 +127,7 @@ class TestPromptStage(TestCase):
def test_valid_challenge_with_policy(self) -> PromptChallengeResponse: def test_valid_challenge_with_policy(self) -> PromptChallengeResponse:
"""Test challenge_response validation""" """Test challenge_response validation"""
plan = FlowPlan( plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
) )
expr = "return request.context['password_prompt'] == request.context['password2_prompt']" expr = "return request.context['password_prompt'] == request.context['password2_prompt']"
expr_policy = ExpressionPolicy.objects.create( expr_policy = ExpressionPolicy.objects.create(
@ -142,7 +144,7 @@ class TestPromptStage(TestCase):
def test_invalid_challenge(self) -> PromptChallengeResponse: def test_invalid_challenge(self) -> PromptChallengeResponse:
"""Test challenge_response validation""" """Test challenge_response validation"""
plan = FlowPlan( plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
) )
expr = "False" expr = "False"
expr_policy = ExpressionPolicy.objects.create( expr_policy = ExpressionPolicy.objects.create(
@ -159,7 +161,7 @@ class TestPromptStage(TestCase):
def test_valid_challenge_request(self): def test_valid_challenge_request(self):
"""Test a request with valid challenge_response data""" """Test a request with valid challenge_response data"""
plan = FlowPlan( plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
) )
session = self.client.session session = self.client.session
session[SESSION_KEY_PLAN] = plan session[SESSION_KEY_PLAN] = plan
@ -196,7 +198,7 @@ class TestPromptStage(TestCase):
def test_invalid_password(self): def test_invalid_password(self):
"""Test challenge_response validation""" """Test challenge_response validation"""
plan = FlowPlan( plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
) )
self.prompt_data["password2_prompt"] = "qwerqwerqr" self.prompt_data["password2_prompt"] = "qwerqwerqr"
challenge_response = PromptChallengeResponse( challenge_response = PromptChallengeResponse(
@ -215,7 +217,7 @@ class TestPromptStage(TestCase):
def test_invalid_username(self): def test_invalid_username(self):
"""Test challenge_response validation""" """Test challenge_response validation"""
plan = FlowPlan( plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
) )
self.prompt_data["username_prompt"] = "akadmin" self.prompt_data["username_prompt"] = "akadmin"
challenge_response = PromptChallengeResponse( challenge_response = PromptChallengeResponse(

View file

@ -30,7 +30,9 @@ class TestUserDeleteStage(TestCase):
designation=FlowDesignation.AUTHENTICATION, designation=FlowDesignation.AUTHENTICATION,
) )
self.stage = UserDeleteStage.objects.create(name="delete") self.stage = UserDeleteStage.objects.create(name="delete")
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) self.binding = FlowStageBinding.objects.create(
target=self.flow, stage=self.stage, order=2
)
@patch( @patch(
"authentik.flows.views.to_stage_response", "authentik.flows.views.to_stage_response",
@ -39,7 +41,7 @@ class TestUserDeleteStage(TestCase):
def test_no_user(self): def test_no_user(self):
"""Test without user set""" """Test without user set"""
plan = FlowPlan( plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
) )
session = self.client.session session = self.client.session
session[SESSION_KEY_PLAN] = plan session[SESSION_KEY_PLAN] = plan
@ -66,7 +68,7 @@ class TestUserDeleteStage(TestCase):
def test_user_delete_get(self): def test_user_delete_get(self):
"""Test Form render""" """Test Form render"""
plan = FlowPlan( plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
) )
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session session = self.client.session

View file

@ -30,12 +30,14 @@ class TestUserLoginStage(TestCase):
designation=FlowDesignation.AUTHENTICATION, designation=FlowDesignation.AUTHENTICATION,
) )
self.stage = UserLoginStage.objects.create(name="login") self.stage = UserLoginStage.objects.create(name="login")
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) self.binding = FlowStageBinding.objects.create(
target=self.flow, stage=self.stage, order=2
)
def test_valid_password(self): def test_valid_password(self):
"""Test with a valid pending user and backend""" """Test with a valid pending user and backend"""
plan = FlowPlan( plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
) )
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session session = self.client.session
@ -61,7 +63,7 @@ class TestUserLoginStage(TestCase):
self.stage.session_duration = "seconds=2" self.stage.session_duration = "seconds=2"
self.stage.save() self.stage.save()
plan = FlowPlan( plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
) )
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session session = self.client.session
@ -92,7 +94,7 @@ class TestUserLoginStage(TestCase):
def test_without_user(self): def test_without_user(self):
"""Test a plan without any pending user, resulting in a denied""" """Test a plan without any pending user, resulting in a denied"""
plan = FlowPlan( plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
) )
session = self.client.session session = self.client.session
session[SESSION_KEY_PLAN] = plan session[SESSION_KEY_PLAN] = plan

View file

@ -28,12 +28,14 @@ class TestUserLogoutStage(TestCase):
designation=FlowDesignation.AUTHENTICATION, designation=FlowDesignation.AUTHENTICATION,
) )
self.stage = UserLogoutStage.objects.create(name="logout") self.stage = UserLogoutStage.objects.create(name="logout")
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) self.binding = FlowStageBinding.objects.create(
target=self.flow, stage=self.stage, order=2
)
def test_valid_password(self): def test_valid_password(self):
"""Test with a valid pending user and backend""" """Test with a valid pending user and backend"""
plan = FlowPlan( plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
) )
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_DJANGO plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_DJANGO

View file

@ -37,7 +37,9 @@ class TestUserWriteStage(TestCase):
designation=FlowDesignation.AUTHENTICATION, designation=FlowDesignation.AUTHENTICATION,
) )
self.stage = UserWriteStage.objects.create(name="write") self.stage = UserWriteStage.objects.create(name="write")
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) self.binding = FlowStageBinding.objects.create(
target=self.flow, stage=self.stage, order=2
)
self.source = Source.objects.create(name="fake_source") self.source = Source.objects.create(name="fake_source")
def test_user_create(self): def test_user_create(self):
@ -48,7 +50,7 @@ class TestUserWriteStage(TestCase):
) )
plan = FlowPlan( plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
) )
plan.context[PLAN_CONTEXT_PROMPT] = { plan.context[PLAN_CONTEXT_PROMPT] = {
"username": "test-user", "username": "test-user",
@ -92,7 +94,7 @@ class TestUserWriteStage(TestCase):
for _ in range(8) for _ in range(8)
) )
plan = FlowPlan( plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
) )
plan.context[PLAN_CONTEXT_PENDING_USER] = User.objects.create( plan.context[PLAN_CONTEXT_PENDING_USER] = User.objects.create(
username="unittest", email="test@beryju.org" username="unittest", email="test@beryju.org"
@ -135,7 +137,7 @@ class TestUserWriteStage(TestCase):
def test_without_data(self): def test_without_data(self):
"""Test without data results in error""" """Test without data results in error"""
plan = FlowPlan( plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
) )
session = self.client.session session = self.client.session
session[SESSION_KEY_PLAN] = plan session[SESSION_KEY_PLAN] = plan
@ -167,7 +169,7 @@ class TestUserWriteStage(TestCase):
def test_blank_username(self): def test_blank_username(self):
"""Test with blank username results in error""" """Test with blank username results in error"""
plan = FlowPlan( plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
) )
session = self.client.session session = self.client.session
plan.context[PLAN_CONTEXT_PROMPT] = { plan.context[PLAN_CONTEXT_PROMPT] = {
@ -204,7 +206,7 @@ class TestUserWriteStage(TestCase):
def test_duplicate_data(self): def test_duplicate_data(self):
"""Test with duplicate data, should trigger error""" """Test with duplicate data, should trigger error"""
plan = FlowPlan( plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
) )
session = self.client.session session = self.client.session
plan.context[PLAN_CONTEXT_PROMPT] = { plan.context[PLAN_CONTEXT_PROMPT] = {