def process( self, plan: "FlowPlan", binding: FlowStageBinding, http_request: HttpRequest, ) -> Optional[FlowStageBinding]: """Re-evaluate policies bound to stage, and if they fail, remove from plan""" from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER LOGGER.debug( "f(plan_inst)[re-eval marker]: running re-evaluation", binding=binding, policy_binding=self.binding, ) engine = PolicyEngine( self.binding, plan.context.get(PLAN_CONTEXT_PENDING_USER, http_request.user) ) engine.use_cache = False engine.request.set_http_request(http_request) engine.request.context = plan.context engine.build() result = engine.result if result.passing: return binding LOGGER.warning( "f(plan_inst)[re-eval marker]: binding failed re-evaluation", binding=binding, messages=result.messages, ) return None
def _build_plan( self, user: User, request: HttpRequest, default_context: Optional[dict[str, Any]], ) -> FlowPlan: """Build flow plan by checking each stage in their respective order and checking the applied policies""" with Hub.current.start_span( op="authentik.flow.planner.build_plan", description=self.flow.slug, ) as span, HIST_FLOWS_PLAN_TIME.labels(flow_slug=self.flow.slug).time(): span: Span span.set_data("flow", self.flow) span.set_data("user", user) span.set_data("request", request) plan = FlowPlan(flow_pk=self.flow.pk.hex) if default_context: plan.context = default_context # Check Flow policies for binding in FlowStageBinding.objects.filter(target__pk=self.flow.pk).order_by( "order" ): binding: FlowStageBinding stage = binding.stage marker = StageMarker() if binding.evaluate_on_plan: self._logger.debug( "f(plan): evaluating on plan", stage=binding.stage, ) engine = PolicyEngine(binding, user, request) engine.request.context = plan.context engine.build() if engine.passing: self._logger.debug( "f(plan): stage passing", stage=binding.stage, ) else: stage = None else: self._logger.debug( "f(plan): not evaluating on plan", stage=binding.stage, ) if binding.re_evaluate_policies and stage: self._logger.debug( "f(plan): stage has re-evaluate marker", stage=binding.stage, ) marker = ReevaluateMarker(binding=binding) if stage: plan.append(binding, marker) HIST_FLOWS_PLAN_TIME.labels(flow_slug=self.flow.slug) self._logger.debug( "f(plan): finished building", ) return plan
def check_access(self, request: Request, slug: str) -> Response: """Check access to a single application by slug""" application = self.get_object() engine = PolicyEngine(application, self.request.user, self.request) engine.build() if engine.passing: return Response(status=204) return Response(status=403)
def event_trigger_handler(event_uuid: str, trigger_name: str): """Check if policies attached to NotificationRule match event""" events = Event.objects.filter(event_uuid=event_uuid) if not events.exists(): LOGGER.warning("event doesn't exist yet or anymore", event_uuid=event_uuid) return event: Event = events.first() triggers: NotificationRule = NotificationRule.objects.filter(name=trigger_name) if not triggers.exists(): return trigger = triggers.first() if "policy_uuid" in event.context: policy_uuid = event.context["policy_uuid"] if PolicyBinding.objects.filter( target__in=NotificationRule.objects.all().values_list("pbm_uuid", flat=True), policy=policy_uuid, ).exists(): # If policy that caused this event to be created is attached # to *any* NotificationRule, we return early. # This is the most effective way to prevent infinite loops. LOGGER.debug("e(trigger): attempting to prevent infinite loop", trigger=trigger) return if not trigger.group: LOGGER.debug("e(trigger): trigger has no group", trigger=trigger) return LOGGER.debug("e(trigger): checking if trigger applies", trigger=trigger) try: user = User.objects.filter(pk=event.user.get("pk")).first() or get_anonymous_user() except User.DoesNotExist: LOGGER.warning("e(trigger): failed to get user", trigger=trigger) return policy_engine = PolicyEngine(trigger, user) policy_engine.mode = PolicyEngineMode.MODE_ANY policy_engine.empty_result = False policy_engine.use_cache = False policy_engine.request.context["event"] = event policy_engine.build() result = policy_engine.result if not result.passing: return LOGGER.debug("e(trigger): event trigger matched", trigger=trigger) # Create the notification objects for transport in trigger.transports.all(): for user in trigger.group.users.all(): LOGGER.debug("created notification") notification = Notification.objects.create( severity=trigger.severity, body=event.summary, event=event, user=user ) notification_transport.apply_async( args=[notification.pk, transport.pk], queue="authentik_events" ) if transport.send_once: break
def test_engine_policy_type(self): """Test invalid policy type""" pbm = PolicyBindingModel.objects.create() PolicyBinding.objects.create(target=pbm, policy=self.policy_wrong_type, order=0) with self.assertRaises(TypeError): engine = PolicyEngine(pbm, self.user) engine.build()
def _get_allowed_applications(self, queryset: QuerySet) -> list[Application]: applications = [] for application in queryset: engine = PolicyEngine(application, self.request.user, self.request) engine.build() if engine.passing: applications.append(application) return applications
def check_access(self, request: Request, slug: str) -> Response: """Check access to a single application by slug""" # Don't use self.get_object as that checks for view_application permission # which the user might not have, even if they have access application = get_object_or_404(Application, slug=slug) engine = PolicyEngine(application, self.request.user, self.request) engine.build() if engine.passing: return Response(status=204) return Response(status=403)
def plan( self, request: HttpRequest, default_context: Optional[dict[str, Any]] = None ) -> FlowPlan: """Check each of the flows' policies, check policies for each stage with PolicyBinding and return ordered list""" with Hub.current.start_span( op="authentik.flow.planner.plan", description=self.flow.slug ) as span: span: Span span.set_data("flow", self.flow) span.set_data("request", request) self._logger.debug( "f(plan): starting planning process", ) # Bit of a workaround here, if there is a pending user set in the default context # we use that user for our cache key # to make sure they don't get the generic response if default_context and PLAN_CONTEXT_PENDING_USER in default_context: user = default_context[PLAN_CONTEXT_PENDING_USER] else: user = request.user # First off, check the flow's direct policy bindings # to make sure the user even has access to the flow engine = PolicyEngine(self.flow, user, request) if default_context: span.set_data("default_context", cleanse_dict(default_context)) engine.request.context = default_context engine.build() result = engine.result if not result.passing: exc = FlowNonApplicableException(",".join(result.messages)) exc.policy_result = result raise exc # User is passing so far, check if we have a cached plan cached_plan_key = cache_key(self.flow, user) cached_plan = cache.get(cached_plan_key, None) if cached_plan and self.use_cache: self._logger.debug( "f(plan): taking plan from cache", key=cached_plan_key, ) # Reset the context as this isn't factored into caching cached_plan.context = default_context or {} return cached_plan self._logger.debug( "f(plan): building plan", ) plan = self._build_plan(user, request, default_context) cache.set(cache_key(self.flow, user), plan, CACHE_TIMEOUT) if not plan.bindings and not self.allow_empty_flows: raise EmptyFlowException() return plan
def test_engine_cache(self): """Ensure empty policy list passes""" pbm = PolicyBindingModel.objects.create() binding = PolicyBinding.objects.create(target=pbm, policy=self.policy_false, order=0) engine = PolicyEngine(pbm, self.user) self.assertEqual( len(cache.keys(f"policy_{binding.policy_binding_uuid.hex}*")), 0) self.assertEqual(engine.build().passing, False) self.assertEqual( len(cache.keys(f"policy_{binding.policy_binding_uuid.hex}*")), 1) self.assertEqual(engine.build().passing, False) self.assertEqual( len(cache.keys(f"policy_{binding.policy_binding_uuid.hex}*")), 1)
def test_engine_empty(self): """Ensure empty policy list passes""" pbm = PolicyBindingModel.objects.create() engine = PolicyEngine(pbm, self.user) result = engine.build().result self.assertEqual(result.passing, True) self.assertEqual(result.messages, ())
def with_policy(request: HttpRequest, **flow_filter) -> Optional["Flow"]: """Get a Flow by `**flow_filter` and check if the request from `request` can access it.""" from authentik.policies.engine import PolicyEngine flows = Flow.objects.filter(**flow_filter).order_by("slug") for flow in flows: engine = PolicyEngine(flow, request.user, request) engine.build() result = engine.result if result.passing: LOGGER.debug("with_policy: flow passing", flow=flow) return flow LOGGER.warning("with_policy: flow not passing", flow=flow, messages=result.messages) LOGGER.debug("with_policy: no flow found", filters=flow_filter) return None
def user_has_access(self, user: Optional[User] = None) -> PolicyResult: """Check if user has access to application.""" user = user or self.request.user policy_engine = PolicyEngine(self.application, user or self.request.user, self.request) policy_engine.build() result = policy_engine.result LOGGER.debug( "PolicyAccessView user_has_access", user=user, app=self.application, result=result, ) if not result.passing: for message in result.messages: messages.error(self.request, _(message)) return result
def process(self, plan: "FlowPlan", stage: Stage, http_request: Optional[HttpRequest]) -> Optional[Stage]: """Re-evaluate policies bound to stage, and if they fail, remove from plan""" engine = PolicyEngine(self.binding, self.user) engine.use_cache = False if http_request: engine.request.set_http_request(http_request) engine.request.context = plan.context engine.build() result = engine.result if result.passing: return stage LOGGER.warning( "f(plan_inst)[re-eval marker]: stage failed re-evaluation", stage=stage, messages=result.messages, ) return None
def check_access(self, request: Request, slug: str) -> Response: """Check access to a single application by slug""" # Don't use self.get_object as that checks for view_application permission # which the user might not have, even if they have access application = get_object_or_404(Application, slug=slug) # If the current user is superuser, they can set `for_user` for_user = self.request.user if self.request.user.is_superuser and "for_user" in request.data: for_user = get_object_or_404(User, pk=request.data.get("for_user")) engine = PolicyEngine(application, for_user, self.request) engine.build() result = engine.result response = PolicyTestResultSerializer(PolicyResult(False)) if result.passing: response = PolicyTestResultSerializer(PolicyResult(True)) if self.request.user.is_superuser: response = PolicyTestResultSerializer(result) return Response(response.data)
def test_engine_policy_error(self): """Test policy raising an error flag""" pbm = PolicyBindingModel.objects.create() PolicyBinding.objects.create(target=pbm, policy=self.policy_raises, order=0) engine = PolicyEngine(pbm, self.user) result = engine.build().result self.assertEqual(result.passing, False) self.assertEqual(result.messages, ("division by zero", ))
def test_engine_simple(self): """Ensure simplest use-case""" pbm = PolicyBindingModel.objects.create() PolicyBinding.objects.create(target=pbm, policy=self.policy_true, order=0) engine = PolicyEngine(pbm, self.user) result = engine.build().result self.assertEqual(result.passing, True) self.assertEqual(result.messages, ("dummy", ))
def user_settings(self, request: Request) -> Response: """Get all sources the user can configure""" _all_sources: Iterable[Source] = Source.objects.filter( enabled=True).select_subclasses() matching_sources: list[UserSettingSerializer] = [] for source in _all_sources: user_settings = source.ui_user_settings if not user_settings: continue policy_engine = PolicyEngine(source, request.user, request) policy_engine.build() if not policy_engine.passing: continue source_settings = source.ui_user_settings source_settings.initial_data["object_uid"] = source.slug if not source_settings.is_valid(): LOGGER.warning(source_settings.errors) matching_sources.append(source_settings.validated_data) return Response(matching_sources)
def test_engine_negate(self): """Test negate flag""" pbm = PolicyBindingModel.objects.create() PolicyBinding.objects.create(target=pbm, policy=self.policy_true, negate=True, order=0) engine = PolicyEngine(pbm, self.user) result = engine.build().result self.assertEqual(result.passing, False) self.assertEqual(result.messages, ("dummy", ))
def test_engine_mode_any(self): """Ensure all policies passes with OR mode (false and true -> true)""" pbm = PolicyBindingModel.objects.create( policy_engine_mode=PolicyEngineMode.MODE_ANY) PolicyBinding.objects.create(target=pbm, policy=self.policy_false, order=0) PolicyBinding.objects.create(target=pbm, policy=self.policy_true, order=1) engine = PolicyEngine(pbm, self.user) result = engine.build().result self.assertEqual(result.passing, True) self.assertEqual( result.messages, ( "dummy", "dummy", ), )