AI News Hub Logo

AI News Hub

Building Pre-Execution Gates: Three Architectural Patterns

DEV Community
Fuzentry™

So you've decided pre-execution gates belong in your architecture. Good choice. Now you need to actually build one. The question isn't whether you need a gate, it's what shape should it take in your codebase. There are three main patterns engineers use, and each has a different profile of complexity, flexibility, and maintainability. The right one depends on how dynamic your rules are and how much they're likely to change. This is the pattern to start with. Your rules are explicit in code, organized in a table structure, and evaluated deterministically. The idea: define your rules as data, then write an evaluator that walks through them in order. # Rules are data, not scattered logic AUTHORIZATION_RULES = [ { "condition": lambda action, user: ( action["operation"] == "delete_data" and user.role != "admin" ), "allowed": False, "reason": "Only admins can delete data" }, { "condition": lambda action, user: ( action["operation"] == "export_data" and user.department != action["target_department"] ), "allowed": False, "reason": "Cannot export data across departments" }, { "condition": lambda action, user: ( action["operation"] == "transfer" and action["amount"] > 100000 and user.approval_level Dict[str, Any]: # Walk through policies in order for policy in self.policies: if self._matches_conditions(policy["conditions"], action, user): return { "allowed": policy["effect"] == "Allow", "policy_id": policy["id"], "reason": policy["reason"] } # Default: allow if no deny policy matched return {"allowed": True, "reason": "No restrictions"} def _matches_conditions(self, conditions: Dict, action: Dict, user: Any) -> bool: # Each condition must match for the policy to apply for key, value in conditions.items(): if not self._evaluate_condition(key, value, action, user): return False return True def _evaluate_condition(self, condition_type: str, condition_value: Any, action: Dict, user: Any) -> bool: # Helper to evaluate individual conditions if condition_type == "operation_matches": import re return bool(re.match(condition_value, action.get("operation", ""))) elif condition_type == "user_role_not_in": return user.role not in condition_value elif condition_type == "user_department_not_equals": # Handle reference to resource properties target = condition_value.replace("resource.", "") return user.department != action.get(target) elif condition_type == "resource_amount_gt": return action.get("amount", 0) > condition_value elif condition_type == "user_approval_level_lt": return user.approval_level Dict: """ Evaluate all policies against the request context. Returns the first matching Deny, or Allow if no Deny matched. """ context = self._build_context(action, user, resource) for policy in self.policies: if self._evaluate_statement(policy.get("statement"), context): effect = policy.get("effect", "Allow") return { "allowed": effect == "Allow", "policy_id": policy.get("id"), "reason": policy.get("reason"), "matched_conditions": policy.get("statement", []) } return {"allowed": True, "reason": "No applicable policies"} def _build_context(self, action: Dict, user: Any, resource: Dict) -> Dict: """ Build evaluation context from action, user, and resource. This is what the policy expressions evaluate against. """ return { "action": action, "user": { "id": user.id, "role": user.role, "department": user.department, "approval_level": user.approval_level, "groups": user.groups }, "resource": resource or {}, "time": self._get_current_time() } def _evaluate_statement(self, statement: List[Dict], context: Dict) -> bool: """ Evaluate a statement (list of conditions). All conditions must be true for the statement to match. """ if not statement: return False for condition in statement: if not self._evaluate_condition(condition, context): return False return True def _evaluate_condition(self, condition: Dict, context: Dict) -> bool: """Evaluate a single condition against the context""" operator = condition.get("operator", "equals") attribute = condition.get("attribute") value = condition.get("value") # Navigate nested attributes (e.g., "user.role") context_value = self._get_attribute(attribute, context) if operator == "equals": return context_value == value elif operator == "not_equals": return context_value != value elif operator == "in": return context_value in value elif operator == "not_in": return context_value not in value elif operator == "greater_than": return context_value > value elif operator == "less_than": return context_value Any: """Navigate dot-notation attributes (e.g., 'user.role')""" parts = attribute_path.split(".") current = context for part in parts: if isinstance(current, dict): current = current.get(part) else: current = getattr(current, part, None) return current def _get_current_time(self) -> str: from datetime import datetime return datetime.utcnow().isoformat() # Define policies as structured data policies = [ { "id": "deny_delete_non_admin", "effect": "Deny", "reason": "Only admins can delete", "statement": [ {"attribute": "action.operation", "operator": "equals", "value": "delete"}, {"attribute": "user.role", "operator": "not_in", "value": ["admin", "superuser"]} ] }, { "id": "deny_large_transfer_low_approval", "effect": "Deny", "reason": "Large transfers require approval level 3+", "statement": [ {"attribute": "action.operation", "operator": "equals", "value": "transfer"}, {"attribute": "resource.amount", "operator": "greater_than", "value": 100000}, {"attribute": "user.approval_level", "operator": "less_than", "value": 3} ] } ] # Usage engine = PolicyEngine() for policy in policies: engine.add_policy(policy) result = engine.evaluate(action, user, resource) if not result["allowed"]: audit_log.record_refusal(action, result["reason"], policy_id=result["policy_id"]) raise PermissionDenied(result["reason"]) Strengths: Handles complex boolean logic cleanly Policies are data, not code or YAML magic Easy to test (just pass in different contexts) Scales to enterprise complexity (thousands of policies) Caching support for performance Weaknesses: Most complex of the three patterns Requires careful design of context and attributes Testing policies is its own discipline Overkill for simple scenarios Use this when: You have complex permission models, role hierarchies, or hundreds of policies that interact with each other. When policy evaluation is a core part of your business logic. Here's how to think about it: Start with Pattern 1 (Decision Table) if you have fewer than 20 rules, they're unlikely to change, and the logic is straightforward. Move to Pattern 2 (Policy Language) when rules change frequently enough that redeploys become annoying, or when non-engineers need to manage rules. Consider Pattern 3 (Policy Engine) when you have role hierarchies, attribute-based access control, or policies that need to interact with each other in complex ways. The pattern you choose is an investment decision. A policy engine is more powerful but requires more infrastructure. A decision table is simpler but brittle at scale. Most teams start with a decision table and graduate to a policy language as complexity grows. The specific pattern matters less than the principle: separate your rules from your logic. Whether you use YAML files, a DSL, or a full policy engine, the goal is the same. Make it possible to change authorization rules without redeploying your application. Make it possible to see all your rules in one place. Make it possible to reason about whether a given action is allowed. The team at Tailored Techworks builds these patterns at scale, often helping organizations graduate from scattered authorization logic to a unified gate architecture. If you're wrestling with how to structure this in your systems, it's worth learning from how production systems do it. Want to dive deeper into policy architecture and governance patterns? Connect with Tailored Techworks on LinkedIn: https://www.linkedin.com/company/tailored-techworks/ - they share detailed breakdowns of architecture decisions and their tradeoffs in real systems.