From db647d8b0a8ff649fb555e4549f6e1fde19f04f5 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Tue, 1 Oct 2024 00:13:11 +0200 Subject: [PATCH] flows: more tests Signed-off-by: Jens Langhammer --- authentik/flows/tests/test_executor.py | 147 +++++++++++++++++++----- authentik/flows/tests/test_inspector.py | 22 ++-- authentik/flows/tests/test_planner.py | 20 ++-- 3 files changed, 144 insertions(+), 45 deletions(-) diff --git a/authentik/flows/tests/test_executor.py b/authentik/flows/tests/test_executor.py index 894a3ae7b234..f424b2276fc1 100644 --- a/authentik/flows/tests/test_executor.py +++ b/authentik/flows/tests/test_executor.py @@ -7,8 +7,8 @@ from django.test.client import RequestFactory from django.urls import reverse -from authentik.core.models import User -from authentik.core.tests.utils import create_test_flow +from authentik.core.models import Group, User +from authentik.core.tests.utils import create_test_flow, create_test_user from authentik.flows.markers import ReevaluateMarker, StageMarker from authentik.flows.models import ( FlowDeniedAction, @@ -255,7 +255,11 @@ def test_reevaluate_remove_last(self): ) binding = FlowStageBinding.objects.create( - target=flow, stage=DummyStage.objects.create(name=generate_id()), order=0 + target=flow, + stage=DummyStage.objects.create(name=generate_id()), + order=0, + evaluate_on_plan=True, + re_evaluate_policies=False, ) binding2 = FlowStageBinding.objects.create( target=flow, @@ -278,8 +282,8 @@ def test_reevaluate_remove_last(self): self.assertEqual(plan.bindings[0], binding) self.assertEqual(plan.bindings[1], binding2) - self.assertIsInstance(plan.markers[0], StageMarker) - self.assertIsInstance(plan.markers[1], ReevaluateMarker) + self.assertEqual(plan.markers[0].__class__, StageMarker) + self.assertEqual(plan.markers[1].__class__, ReevaluateMarker) # Second request, this passes the first dummy stage response = self.client.post(exec_url) @@ -301,7 +305,11 @@ def test_reevaluate_remove_middle(self): ) binding = FlowStageBinding.objects.create( - target=flow, stage=DummyStage.objects.create(name=generate_id()), order=0 + target=flow, + stage=DummyStage.objects.create(name=generate_id()), + order=0, + evaluate_on_plan=True, + re_evaluate_policies=False, ) binding2 = FlowStageBinding.objects.create( target=flow, @@ -310,7 +318,11 @@ def test_reevaluate_remove_middle(self): re_evaluate_policies=True, ) binding3 = FlowStageBinding.objects.create( - target=flow, stage=DummyStage.objects.create(name=generate_id()), order=2 + target=flow, + stage=DummyStage.objects.create(name=generate_id()), + order=2, + evaluate_on_plan=True, + re_evaluate_policies=False, ) PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0) @@ -328,9 +340,9 @@ def test_reevaluate_remove_middle(self): self.assertEqual(plan.bindings[1], binding2) self.assertEqual(plan.bindings[2], binding3) - self.assertIsInstance(plan.markers[0], StageMarker) - self.assertIsInstance(plan.markers[1], ReevaluateMarker) - self.assertIsInstance(plan.markers[2], StageMarker) + self.assertEqual(plan.markers[0].__class__, StageMarker) + self.assertEqual(plan.markers[1].__class__, ReevaluateMarker) + self.assertEqual(plan.markers[2].__class__, StageMarker) # Second request, this passes the first dummy stage response = self.client.post(exec_url) @@ -341,8 +353,8 @@ def test_reevaluate_remove_middle(self): self.assertEqual(plan.bindings[0], binding2) self.assertEqual(plan.bindings[1], binding3) - self.assertIsInstance(plan.markers[0], StageMarker) - self.assertIsInstance(plan.markers[1], StageMarker) + self.assertEqual(plan.markers[0].__class__, ReevaluateMarker) + self.assertEqual(plan.markers[1].__class__, StageMarker) # third request, this should trigger the re-evaluate # We do this request without the patch, so the policy results in false @@ -360,7 +372,11 @@ def test_reevaluate_keep(self): ) binding = FlowStageBinding.objects.create( - target=flow, stage=DummyStage.objects.create(name=generate_id()), order=0 + target=flow, + stage=DummyStage.objects.create(name=generate_id()), + order=0, + evaluate_on_plan=True, + re_evaluate_policies=False, ) binding2 = FlowStageBinding.objects.create( target=flow, @@ -369,7 +385,11 @@ def test_reevaluate_keep(self): re_evaluate_policies=True, ) binding3 = FlowStageBinding.objects.create( - target=flow, stage=DummyStage.objects.create(name=generate_id()), order=2 + target=flow, + stage=DummyStage.objects.create(name=generate_id()), + order=2, + evaluate_on_plan=True, + re_evaluate_policies=False, ) PolicyBinding.objects.create(policy=true_policy, target=binding2, order=0) @@ -387,9 +407,9 @@ def test_reevaluate_keep(self): self.assertEqual(plan.bindings[1], binding2) self.assertEqual(plan.bindings[2], binding3) - self.assertIsInstance(plan.markers[0], StageMarker) - self.assertIsInstance(plan.markers[1], ReevaluateMarker) - self.assertIsInstance(plan.markers[2], StageMarker) + self.assertEqual(plan.markers[0].__class__, StageMarker) + self.assertEqual(plan.markers[1].__class__, ReevaluateMarker) + self.assertEqual(plan.markers[2].__class__, StageMarker) # Second request, this passes the first dummy stage response = self.client.post(exec_url) @@ -400,8 +420,8 @@ def test_reevaluate_keep(self): self.assertEqual(plan.bindings[0], binding2) self.assertEqual(plan.bindings[1], binding3) - self.assertIsInstance(plan.markers[0], StageMarker) - self.assertIsInstance(plan.markers[1], StageMarker) + self.assertEqual(plan.markers[0].__class__, ReevaluateMarker) + self.assertEqual(plan.markers[1].__class__, StageMarker) # Third request, this passes the first dummy stage response = self.client.post(exec_url) @@ -411,7 +431,7 @@ def test_reevaluate_keep(self): self.assertEqual(plan.bindings[0], binding3) - self.assertIsInstance(plan.markers[0], StageMarker) + self.assertEqual(plan.markers[0].__class__, StageMarker) # third request, this should trigger the re-evaluate # We do this request without the patch, so the policy results in false @@ -429,7 +449,11 @@ def test_reevaluate_remove_consecutive(self): ) binding = FlowStageBinding.objects.create( - target=flow, stage=DummyStage.objects.create(name=generate_id()), order=0 + target=flow, + stage=DummyStage.objects.create(name=generate_id()), + order=0, + evaluate_on_plan=True, + re_evaluate_policies=False, ) binding2 = FlowStageBinding.objects.create( target=flow, @@ -444,7 +468,11 @@ def test_reevaluate_remove_consecutive(self): re_evaluate_policies=True, ) binding4 = FlowStageBinding.objects.create( - target=flow, stage=DummyStage.objects.create(name=generate_id()), order=2 + target=flow, + stage=DummyStage.objects.create(name=generate_id()), + order=2, + evaluate_on_plan=True, + re_evaluate_policies=False, ) PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0) @@ -465,10 +493,10 @@ def test_reevaluate_remove_consecutive(self): self.assertEqual(plan.bindings[2], binding3) self.assertEqual(plan.bindings[3], binding4) - self.assertIsInstance(plan.markers[0], StageMarker) - self.assertIsInstance(plan.markers[1], ReevaluateMarker) - self.assertIsInstance(plan.markers[2], ReevaluateMarker) - self.assertIsInstance(plan.markers[3], StageMarker) + self.assertEqual(plan.markers[0].__class__, StageMarker) + self.assertEqual(plan.markers[1].__class__, ReevaluateMarker) + self.assertEqual(plan.markers[2].__class__, ReevaluateMarker) + self.assertEqual(plan.markers[3].__class__, StageMarker) # Second request, this passes the first dummy stage response = self.client.post(exec_url) @@ -519,9 +547,9 @@ def test_invalid_restart(self): ) # Stage 0 is a deny stage that is added dynamically # when the reputation policy says so - deny_stage = DenyStage.objects.create(name="deny") + deny_stage = DenyStage.objects.create(name=generate_id()) reputation_policy = ReputationPolicy.objects.create( - name="reputation", threshold=-1, check_ip=False + name=generate_id(), threshold=-1, check_ip=False ) deny_binding = FlowStageBinding.objects.create( target=flow, @@ -534,7 +562,7 @@ def test_invalid_restart(self): # Stage 1 is an identification stage ident_stage = IdentificationStage.objects.create( - name="ident", + name=generate_id(), user_fields=[UserFields.E_MAIL], pretend_user_exists=False, ) @@ -559,3 +587,64 @@ def test_invalid_restart(self): ) response = self.client.post(exec_url, {"uid_field": "invalid-string"}, follow=True) self.assertStageResponse(response, flow, component="ak-stage-access-denied") + + def test_re_evaluate_group_binding(self): + """Test re-evaluate stage binding that has a policy binding to a group""" + flow = create_test_flow() + + user_group_membership = create_test_user() + user_direct_binding = create_test_user() + user_other = create_test_user() + + group_a = Group.objects.create(name=generate_id()) + user_group_membership.ak_groups.add(group_a) + + # Stage 0 is an identification stage + ident_stage = IdentificationStage.objects.create( + name=generate_id(), + user_fields=[UserFields.USERNAME], + pretend_user_exists=False, + ) + FlowStageBinding.objects.create( + target=flow, + stage=ident_stage, + order=0, + ) + + # Stage 1 is a dummy stage that is only shown for users in group_a + dummy_stage = DummyStage.objects.create(name=generate_id()) + dummy_binding = FlowStageBinding.objects.create(target=flow, stage=dummy_stage, order=1) + PolicyBinding.objects.create(group=group_a, target=dummy_binding, order=0) + PolicyBinding.objects.create(user=user_direct_binding, target=dummy_binding, order=0) + + # Stage 2 is a deny stage that (in this case) only user_b will see + deny_stage = DenyStage.objects.create(name=generate_id()) + FlowStageBinding.objects.create(target=flow, stage=deny_stage, order=2) + + exec_url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}) + + with self.subTest(f"Test user access through group: {user_group_membership}"): + self.client.logout() + # First request, run the planner + response = self.client.get(exec_url) + self.assertStageResponse(response, flow, component="ak-stage-identification") + response = self.client.post( + exec_url, {"uid_field": user_group_membership.username}, follow=True + ) + self.assertStageResponse(response, flow, component="ak-stage-dummy") + with self.subTest(f"Test user access through user: {user_direct_binding}"): + self.client.logout() + # First request, run the planner + response = self.client.get(exec_url) + self.assertStageResponse(response, flow, component="ak-stage-identification") + response = self.client.post( + exec_url, {"uid_field": user_direct_binding.username}, follow=True + ) + self.assertStageResponse(response, flow, component="ak-stage-dummy") + with self.subTest(f"Test user has no access: {user_other}"): + self.client.logout() + # First request, run the planner + response = self.client.get(exec_url) + self.assertStageResponse(response, flow, component="ak-stage-identification") + response = self.client.post(exec_url, {"uid_field": user_other.username}, follow=True) + self.assertStageResponse(response, flow, component="ak-stage-access-denied") diff --git a/authentik/flows/tests/test_inspector.py b/authentik/flows/tests/test_inspector.py index 2a01ea370c7a..c6bd83c58bdd 100644 --- a/authentik/flows/tests/test_inspector.py +++ b/authentik/flows/tests/test_inspector.py @@ -8,6 +8,7 @@ from authentik.core.tests.utils import create_test_admin_user, create_test_flow from authentik.flows.models import FlowDesignation, FlowStageBinding, InvalidResponseAction +from authentik.lib.generators import generate_id from authentik.stages.dummy.models import DummyStage from authentik.stages.identification.models import IdentificationStage, UserFields @@ -26,7 +27,7 @@ def test(self): # Stage 1 is an identification stage ident_stage = IdentificationStage.objects.create( - name="ident", + name=generate_id(), user_fields=[UserFields.USERNAME], ) FlowStageBinding.objects.create( @@ -35,9 +36,8 @@ def test(self): order=1, invalid_response_action=InvalidResponseAction.RESTART_WITH_CONTEXT, ) - FlowStageBinding.objects.create( - target=flow, stage=DummyStage.objects.create(name="dummy2"), order=1 - ) + dummy_stage = DummyStage.objects.create(name=generate_id()) + FlowStageBinding.objects.create(target=flow, stage=dummy_stage, order=1) res = self.client.get( reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), @@ -67,9 +67,11 @@ def test(self): ) content = loads(ins.content) self.assertEqual(content["is_completed"], False) - self.assertEqual(content["current_plan"]["current_stage"]["stage_obj"]["name"], "ident") self.assertEqual( - content["current_plan"]["next_planned_stage"]["stage_obj"]["name"], "dummy2" + content["current_plan"]["current_stage"]["stage_obj"]["name"], ident_stage.name + ) + self.assertEqual( + content["current_plan"]["next_planned_stage"]["stage_obj"]["name"], dummy_stage.name ) self.client.post( @@ -83,8 +85,12 @@ def test(self): ) content = loads(ins.content) self.assertEqual(content["is_completed"], False) - self.assertEqual(content["plans"][0]["current_stage"]["stage_obj"]["name"], "ident") - self.assertEqual(content["current_plan"]["current_stage"]["stage_obj"]["name"], "dummy2") + self.assertEqual( + content["plans"][0]["current_stage"]["stage_obj"]["name"], ident_stage.name + ) + self.assertEqual( + content["current_plan"]["current_stage"]["stage_obj"]["name"], dummy_stage.name + ) self.assertEqual( content["current_plan"]["plan_context"]["pending_user"]["username"], self.admin.username ) diff --git a/authentik/flows/tests/test_planner.py b/authentik/flows/tests/test_planner.py index f7352a7cf5d3..f69f1eb46cca 100644 --- a/authentik/flows/tests/test_planner.py +++ b/authentik/flows/tests/test_planner.py @@ -16,6 +16,7 @@ from authentik.flows.markers import ReevaluateMarker, StageMarker from authentik.flows.models import FlowAuthenticationRequirement, FlowDesignation, FlowStageBinding from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key +from authentik.lib.generators import generate_id from authentik.lib.tests.utils import dummy_get_response from authentik.outposts.apps import MANAGED_OUTPOST from authentik.outposts.models import Outpost @@ -122,7 +123,7 @@ def test_planner_cache(self): """Test planner cache""" flow = create_test_flow(FlowDesignation.AUTHENTICATION) FlowStageBinding.objects.create( - target=flow, stage=DummyStage.objects.create(name="dummy"), order=0 + target=flow, stage=DummyStage.objects.create(name=generate_id()), order=0 ) request = self.request_factory.get( reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), @@ -141,7 +142,7 @@ def test_planner_default_context(self): """Test planner with default_context""" flow = create_test_flow() FlowStageBinding.objects.create( - target=flow, stage=DummyStage.objects.create(name="dummy"), order=0 + target=flow, stage=DummyStage.objects.create(name=generate_id()), order=0 ) user = User.objects.create(username="test-user") @@ -160,7 +161,7 @@ def test_planner_marker_reevaluate(self): FlowStageBinding.objects.create( target=flow, - stage=DummyStage.objects.create(name="dummy1"), + stage=DummyStage.objects.create(name=generate_id()), order=0, re_evaluate_policies=True, ) @@ -173,7 +174,7 @@ def test_planner_marker_reevaluate(self): planner = FlowPlanner(flow) plan = planner.plan(request) - self.assertIsInstance(plan.markers[0], ReevaluateMarker) + self.assertEqual(plan.markers[0].__class__, ReevaluateMarker) def test_planner_reevaluate_actual(self): """Test planner with re-evaluate""" @@ -181,11 +182,14 @@ def test_planner_reevaluate_actual(self): false_policy = DummyPolicy.objects.create(result=False, wait_min=1, wait_max=2) binding = FlowStageBinding.objects.create( - target=flow, stage=DummyStage.objects.create(name="dummy1"), order=0 + target=flow, + stage=DummyStage.objects.create(name=generate_id()), + order=0, + re_evaluate_policies=False, ) binding2 = FlowStageBinding.objects.create( target=flow, - stage=DummyStage.objects.create(name="dummy2"), + stage=DummyStage.objects.create(name=generate_id()), order=1, re_evaluate_policies=True, ) @@ -209,5 +213,5 @@ def test_planner_reevaluate_actual(self): self.assertEqual(plan.bindings[0], binding) self.assertEqual(plan.bindings[1], binding2) - self.assertIsInstance(plan.markers[0], StageMarker) - self.assertIsInstance(plan.markers[1], ReevaluateMarker) + self.assertEqual(plan.markers[0].__class__, StageMarker) + self.assertEqual(plan.markers[1].__class__, ReevaluateMarker)