2020-05-08 12:33:14 +00:00
|
|
|
"""Flows Planner"""
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
from time import time
|
2020-05-08 14:10:27 +00:00
|
|
|
from typing import Any, Dict, List, Tuple
|
2020-05-08 12:33:14 +00:00
|
|
|
|
|
|
|
from django.http import HttpRequest
|
|
|
|
from structlog import get_logger
|
|
|
|
|
2020-05-09 18:54:56 +00:00
|
|
|
from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException
|
2020-05-08 17:46:39 +00:00
|
|
|
from passbook.flows.models import Flow, Stage
|
2020-05-08 12:33:14 +00:00
|
|
|
from passbook.policies.engine import PolicyEngine
|
|
|
|
|
|
|
|
LOGGER = get_logger()
|
|
|
|
|
2020-05-08 14:10:27 +00:00
|
|
|
PLAN_CONTEXT_PENDING_USER = "pending_user"
|
|
|
|
PLAN_CONTEXT_SSO = "is_sso"
|
|
|
|
|
2020-05-08 12:33:14 +00:00
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class FlowPlan:
|
|
|
|
"""This data-class is the output of a FlowPlanner. It holds a flat list
|
2020-05-08 17:46:39 +00:00
|
|
|
of all Stages that should be run."""
|
2020-05-08 12:33:14 +00:00
|
|
|
|
2020-05-08 17:46:39 +00:00
|
|
|
stages: List[Stage] = field(default_factory=list)
|
2020-05-08 14:10:27 +00:00
|
|
|
context: Dict[str, Any] = field(default_factory=dict)
|
2020-05-08 12:33:14 +00:00
|
|
|
|
2020-05-08 17:46:39 +00:00
|
|
|
def next(self) -> Stage:
|
|
|
|
"""Return next pending stage from the bottom of the list"""
|
2020-05-09 18:54:56 +00:00
|
|
|
return self.stages[0]
|
2020-05-08 12:33:14 +00:00
|
|
|
|
|
|
|
|
|
|
|
class FlowPlanner:
|
2020-05-08 17:46:39 +00:00
|
|
|
"""Execute all policies to plan out a flat list of all Stages
|
2020-05-08 12:33:14 +00:00
|
|
|
that should be applied."""
|
|
|
|
|
|
|
|
flow: Flow
|
|
|
|
|
|
|
|
def __init__(self, flow: Flow):
|
|
|
|
self.flow = flow
|
|
|
|
|
|
|
|
def _check_flow_root_policies(self, request: HttpRequest) -> Tuple[bool, List[str]]:
|
|
|
|
engine = PolicyEngine(self.flow.policies.all(), request.user, request)
|
|
|
|
engine.build()
|
|
|
|
return engine.result
|
|
|
|
|
|
|
|
def plan(self, request: HttpRequest) -> FlowPlan:
|
2020-05-08 17:46:39 +00:00
|
|
|
"""Check each of the flows' policies, check policies for each stage with PolicyBinding
|
2020-05-08 12:33:14 +00:00
|
|
|
and return ordered list"""
|
|
|
|
LOGGER.debug("Starting planning process", flow=self.flow)
|
|
|
|
start_time = time()
|
|
|
|
plan = FlowPlan()
|
|
|
|
# First off, check the flow's direct policy bindings
|
|
|
|
# to make sure the user even has access to the flow
|
|
|
|
root_passing, root_passing_messages = self._check_flow_root_policies(request)
|
|
|
|
if not root_passing:
|
2020-05-09 18:54:56 +00:00
|
|
|
raise FlowNonApplicableException(root_passing_messages)
|
2020-05-08 12:33:14 +00:00
|
|
|
# Check Flow policies
|
2020-05-08 17:46:39 +00:00
|
|
|
for stage in (
|
|
|
|
self.flow.stages.order_by("flowstagebinding__order")
|
|
|
|
.select_subclasses()
|
|
|
|
.select_related()
|
|
|
|
):
|
|
|
|
binding = stage.flowstagebinding_set.get(flow__pk=self.flow.pk)
|
|
|
|
engine = PolicyEngine(binding.policies.all(), request.user, request)
|
2020-05-08 12:33:14 +00:00
|
|
|
engine.build()
|
|
|
|
passing, _ = engine.result
|
|
|
|
if passing:
|
2020-05-08 17:46:39 +00:00
|
|
|
LOGGER.debug("Stage passing", stage=stage)
|
|
|
|
plan.stages.append(stage)
|
2020-05-08 12:33:14 +00:00
|
|
|
end_time = time()
|
|
|
|
LOGGER.debug(
|
|
|
|
"Finished planning", flow=self.flow, duration_s=end_time - start_time
|
|
|
|
)
|
2020-05-09 18:54:56 +00:00
|
|
|
if not plan.stages:
|
|
|
|
raise EmptyFlowException()
|
2020-05-08 12:33:14 +00:00
|
|
|
return plan
|