Chapter 3: Setting Up Your Authorization Engine: Choosing Between Embedded RBAC and External Policy Engines (OPA)

The Policy Enforcement Point (PEP) intercepts actions inline, while the Policy Decision Point (PDP) serves as the authoritative evaluation brain. Once identity is propagated through RunnableConfig, the PEP queries the PDP using a standardized evaluation tuple: subject, action, resource, and context.

This chapter examines how to configure the two PDP deployment models—an embedded in-process engine and a resilient remote OPA adapter—and highlights how to register the centralized engine and manage its decision cache lifecycle across your enterprise application.


Option 1: In-Process EmbeddedPolicyEngine

For applications requiring ultra-low latency without external network dependencies, the embedded engine runs in-memory with sub-millisecond evaluation. It resolves hierarchical role Directed Acyclic Graphs (DAGs), CEL expressions, and numeric clearance tiers (0 ≤ PUBLIC < 1 ≤ INTERNAL < 2 ≤ CONFIDENTIAL < 3 ≤ RESTRICTED).

When hardening your application, you can establish an authorization bootstrap module. In our reference assistant, this is implemented as a new module: app/security/security_integration.py.

1. Defining the Role Graph and Clearance Hierarchy

First, configure the role inheritance relationships and clearance tier mappings required by your organization:

New File: app/security/security_integration.py

DEMO APP (NEW FILE) app/security/security_integration.py
PYTHON
# Hierarchical role inheritance: ADMIN inherits DEVOPS, DEVOPS inherits USER
def create_role_graph() -> RoleGraph:
    graph = RoleGraph.create()
    graph.add_inheritance(ROLE_ADMIN, ROLE_DEVOPS_ENGINEER)
    graph.add_inheritance(ROLE_ADMIN, ROLE_SUPPORT_AGENT)
    graph.add_inheritance(ROLE_DEVOPS_ENGINEER, ROLE_USER)
    graph.add_inheritance(ROLE_SUPPORT_AGENT, ROLE_USER)
    return graph

CLEARANCE_RANK_MAP: Dict[str, int] = {
    "TOP_SECRET": 3,
    "SECRET": 2,
    "STANDARD": 1,
    "UNAUTHENTICATED": 0,
    ROLE_ADMIN: 3,
    ROLE_DEVOPS_ENGINEER: 2,
    ROLE_SUPPORT_AGENT: 2,
    ROLE_USER: 1,
}

Explanation: The RoleGraph defines a Directed Acyclic Graph (DAG) of role inheritance so higher privileges automatically satisfy lower requirements (e.g. ROLE_ADMIN satisfies ROLE_DEVOPS_ENGINEER). CLEARANCE_RANK_MAP normalizes arbitrary enterprise roles into monotonic integer clearance tiers for numeric range comparisons.

2. Bootstrapping the Policy Engine

Next, assemble the EmbeddedPolicyEngine with CEL evaluation, audit logging, and two-tier decision caching:

New File: app/security/security_integration.py

DEMO APP (NEW FILE) app/security/security_integration.py
PYTHON
def get_pdp_engine() -> EmbeddedPolicyEngine:
    global _PDP_ENGINE_INSTANCE
    if _PDP_ENGINE_INSTANCE is None:
        role_graph = create_role_graph()
        cel_evaluator = CELEvaluator.create(entity_resolver=entity_resolver_instance)
        audit_manager = AuditManager.create(
            publishers=[JsonFileAuditLogger.create(DEFAULT_AUDIT_LOG_FILE)],
            sanitizer=AuditSanitizer.create(masked_fields={"password", "secret", "token"}),
        )
        decision_cache = TwoTierDecisionCache.create(
            l1_enabled=config.cache_l1_enabled,
            l2_enabled=config.cache_l2_enabled,
            l2_ttl_seconds=config.cache_l2_ttl_seconds,
            l2_max_entries=config.cache_l2_max_entries,
        )
        engine = EmbeddedPolicyEngine.with_components(
            role_graph=role_graph,
            cel_evaluator=cel_evaluator,
            audit_manager=audit_manager,
            decision_cache=decision_cache,
        )
        engine.enforce_strict_tenant_isolation = True
        _PDP_ENGINE_INSTANCE = engine
    return _PDP_ENGINE_INSTANCE

Explanation: EmbeddedPolicyEngine.with_components() combines the role DAG, CEL evaluator, audit manager, and two-tier cache into a cohesive, in-process Policy Decision Point (PDP). See app/security/security_integration.py in the assistant for the complete implementation.

Two-Tier Decision Cache Architecture

To balance sub-millisecond latency with multi-turn agent workloads, TwoTierDecisionCache splits evaluation caching across two complementary layers:


Global Engine Registration and Discovery

Rather than manually injecting the PDP instance into every tool and agent boundary across your codebase, your application registers the configured PDP engine and entity resolver globally via SecurityRegistry during initialization. Downstream Policy Enforcement Points (such as @secured_tool decorators and SecureRunnable wrappers) automatically resolve the active evaluation engine and entity resolver from this central registry.

New File: app/security/security_integration.py

DEMO APP (NEW FILE) app/security/security_integration.py
PYTHON
from langchain_security_extension.security_core.registry import SecurityRegistry

# Register configured PDP engine and resolver for global PEP resolution
SecurityRegistry.configure(
    pdp_engine=engine,
    entity_resolver=entity_resolver_instance,
)

Option 2: Remote OPA Engine with Fail-Closed Circuit Breaking

When offloading policy logic to Open Policy Agent (OPA) clusters, network latency and outages introduce critical failure modes. Enterprise applications set up the remote adapter through a factory that wraps HTTP dispatches inside a fail-closed circuit breaker with strict timeouts and recovery probe thresholds.

In app/config.py, the application selects the active PDP mode using the property security.pdp.mode (defaulting to EMBEDDED, with alternative OPA), allowing deployment environments to toggle between in-process evaluation and centralized OPA policy bundles dynamically.

If the remote PDP becomes unreachable or times out, the library can be configured either to enforce strict fail-closed rejection (fallback_engine=None) or to seamlessly use a local PDP fallback (fallback_engine=base_engine) to maintain system availability without sacrificing authorization enforcement:

New File: app/security/security_integration.py

DEMO APP (NEW FILE) app/security/security_integration.py
PYTHON
# Creating resilient remote OPA adapter with fail-closed circuit breaker
adapter = RemotePDPAdapter.create(
    endpoint_url=config.pdp_opa_url,
    timeout_seconds=config.pdp_opa_timeout_seconds,
    failure_threshold=config.pdp_circuit_breaker_threshold,
    recovery_seconds=config.pdp_circuit_breaker_recovery_seconds,
    fallback_engine=base_engine if use_fallback else None,
    audit_manager=base_engine.audit_manager,
)

Explanation: If network calls to OPA fail consecutively beyond failure_threshold, the circuit breaker opens, immediately falling back to fallback_engine or failing closed with a denial, protecting model agents from hanging on network timeouts.


Cache Invalidation and Identity Lifecycle

Decision cache entries must be evicted when subject permissions change or external policy bundles reload. When user roles, clearances, or account statuses are modified in your user registry or IAM sync job (e.g., upon privilege revocation or role updates), cache invalidation triggers notify OPACacheInvalidationHandler.on_invalidation_event({"subject_id": email}) to flush cached decisions and guarantee immediate policy consistency.

app/security/user_registry.py

DEMO APP (DIFF) app/security/user_registry.py
DIFF
@@ -195,3 +195,6 @@ def evict_subject_from_pdp_cache(self, email: Optional[str]) -> int:
     if not email:
         return 0
+    from app.security.security_integration import get_opa_cache_invalidation_handler
+    return get_opa_cache_invalidation_handler().on_invalidation_event({"subject_id": email})

Explanation: Instead of a no-op return, the method notifies the cache invalidation handler to flush both L1 and L2 cache entries for that subject. Future evaluations for that user re-query the PDP immediately, preventing stale privilege retention.


Architectural Comparison

Dimension EmbeddedPolicyEngine OPAPolicyEngine (Remote SPI)
Deployment Footprint Zero dependencies; runs in-process. Requires OPA sidecar or central cluster.
Policy Updates Requires service redeploy or config reload. Centralized, dynamic bundle distribution.
Resilience Mode Process memory bound; zero network risk. Requires fail-closed circuit breakers and bounded timeouts.

Verification Tests (Guardrail Tests)

Validate PDP resilience, state transitions, and fail-closed behaviors using the standardized test suite: