Skip to main content

apl_cpex/
register.rs

1// Location: ./crates/apl-cpex/src/register.rs
2// Copyright 2025
3// SPDX-License-Identifier: Apache-2.0
4// Authors: Teryl Taylor
5//
6// `register_apl` — sugar function that bundles "construct
7// `AplConfigVisitor` + register it with the manager" into one call.
8//
9// Hosts that just want APL governance with sensible defaults call this
10// instead of building the visitor by hand. The lower-level
11// `PluginManager::register_visitor` API stays available for custom
12// orchestrators (future Rego, Cedar-direct, hand-rolled audit visitors)
13// that don't fit the APL setup.
14//
15// # Two ways to supply PDPs
16//
17// PDP resolvers can reach the visitor's internal `PdpRouter` via two
18// channels, and `AplOptions` exposes both:
19//
20//   * `pdps`           — code-supplied resolvers. The host built them
21//                        in Rust (e.g. a hand-rolled audit resolver,
22//                        a test fake) and hands them in directly.
23//   * `pdp_factories`  — factories the visitor consults when it sees a
24//                        `global.apl.pdp[]` entry in the unified
25//                        config. Each factory advertises a `kind()`
26//                        string that matches the YAML block's `kind:`
27//                        field.
28//
29// Both channels feed the same `PdpRouter` inside the visitor, so a
30// host can mix the two freely — code-supplied Cedar for tests plus a
31// config-declared OPA in prod, say.
32
33use std::collections::HashSet;
34use std::sync::Arc;
35
36use cpex_core::manager::PluginManager;
37use cpex_core::visitor::ConfigVisitor;
38
39use apl_core::step::{PdpFactory, PdpResolver};
40
41use crate::dispatch_plan::DispatchCache;
42use crate::session_store::{SessionStore, SessionStoreFactory};
43use crate::visitor::AplConfigVisitor;
44
45/// Configuration for [`register_apl`]. All runtime collaborators APL
46/// needs to do its work are funneled through here so the call site
47/// reads as a single block instead of a multi-step builder.
48pub struct AplOptions {
49    /// Shared dispatch-plan cache. One `Arc<DispatchCache>` per host
50    /// instance — clones are cheap (refcount bump) and the cache is
51    /// internally synchronized.
52    pub dispatch_cache: Arc<DispatchCache>,
53
54    /// Pluggable session-scoped state. `MemorySessionStore` is the
55    /// default in-process backend; production hosts swap in Redis /
56    /// DynamoDB-backed impls.
57    pub session_store: Arc<dyn SessionStore>,
58
59    /// Zero or more code-supplied PDP resolvers. Each is registered
60    /// into the visitor's internal `PdpRouter`, so `pdp(...)` steps
61    /// dispatch by dialect across this list **and** any resolvers the
62    /// visitor builds from `global.apl.pdp[]` config entries. An empty
63    /// list combined with empty `pdp_factories` means no PDP is wired
64    /// — routes that call `pdp(...)` surface `PdpError::NoResolver` at
65    /// evaluation time, which is the correct behavior for "you forgot
66    /// to configure your policy decision point."
67    pub pdps: Vec<Arc<dyn PdpResolver>>,
68
69    /// PDP factories the visitor consults when it encounters a
70    /// `global.apl.pdp[]` entry. Each factory advertises a `kind()`
71    /// string that matches the YAML block's `kind:` field — e.g.
72    /// `cedar-direct`, `opa`. An empty list disables
73    /// config-driven PDP wiring; hosts can still supply resolvers via
74    /// `pdps`.
75    pub pdp_factories: Vec<Arc<dyn PdpFactory>>,
76
77    /// Session-store factories the visitor consults when it encounters a
78    /// `global.apl.session_store` block. Each factory advertises a
79    /// `kind()` string matching the block's `kind:` field — e.g.
80    /// `valkey`. An empty list keeps the constructor-supplied
81    /// `session_store` (the `MemorySessionStore` default) active, so
82    /// existing deployments are unaffected.
83    pub session_store_factories: Vec<Arc<dyn SessionStoreFactory>>,
84
85    /// Override the visitor's baseline capabilities for installed
86    /// `AplRouteHandler`s. `None` uses the visitor's default
87    /// (read-only across the common attribute namespaces); `Some(set)`
88    /// replaces it entirely. The per-route plugin capability union is
89    /// added on top regardless — this only controls the baseline.
90    ///
91    /// Set to `Some(HashSet::new())` for strict deployments where
92    /// only plugin-declared caps should be granted.
93    pub base_capabilities: Option<HashSet<String>>,
94}
95
96impl AplOptions {
97    /// Minimal options — in-process dispatch cache + memory session
98    /// store, no PDP, default baseline capabilities. Useful for tests
99    /// and single-process demos.
100    pub fn in_process() -> Self {
101        Self {
102            dispatch_cache: Arc::new(DispatchCache::new()),
103            session_store: Arc::new(crate::session_store::MemorySessionStore::new()),
104            pdps: Vec::new(),
105            pdp_factories: Vec::new(),
106            session_store_factories: Vec::new(),
107            base_capabilities: None,
108        }
109    }
110}
111
112/// Build an [`AplConfigVisitor`] from the supplied options and register
113/// it on the manager. Returns the `Arc<AplConfigVisitor>` so the caller
114/// can stash it for later inspection (or call `register_pdp` on it
115/// after the fact for late-bound resolvers) — but in the typical case
116/// the return value is dropped and the visitor lives inside the
117/// manager's visitor list.
118///
119/// After this call, the next `mgr.load_config_yaml(yaml)` invocation
120/// will walk the visitor: cpex-core's [`visit_plugins`][vp] populates
121/// the APL plugin registry from `&[PluginConfig]`; `visit_global`
122/// processes any `global.apl.pdp[]` entries by dispatching to the
123/// registered `pdp_factories`; the hierarchy walk stacks `global.apl`
124/// / `defaults.<entity>.apl` / `policies.<tag>.apl` / route-level
125/// `apl:` into compiled routes; one `AplRouteHandler` is installed
126/// per route per phase via [`PluginManager::annotate_route`][ar].
127///
128/// [vp]: cpex_core::visitor::ConfigVisitor::visit_plugins
129/// [ar]: cpex_core::manager::PluginManager::annotate_route
130///
131/// # Example
132///
133/// ```ignore
134/// use std::sync::Arc;
135/// use cpex_core::manager::PluginManager;
136/// use apl_cpex::{register_apl, AplOptions};
137/// use cpex_pdp_cedar_direct::CedarDirectPdpFactory;
138///
139/// let mgr = Arc::new(PluginManager::default());
140/// mgr.register_factory("scope-gate", Box::new(ScopeGateFactory));
141///
142/// apl_cpex::register_apl(&mgr, AplOptions {
143///     dispatch_cache: dispatch_cache.clone(),
144///     session_store: session_store.clone(),
145///     pdps: vec![],                                       // none code-supplied
146///     pdp_factories: vec![Arc::new(CedarDirectPdpFactory::new())],
147///     base_capabilities: None,
148/// });
149///
150/// mgr.load_config_yaml(&yaml_string)?;
151/// mgr.initialize().await?;
152/// ```
153pub fn register_apl(mgr: &Arc<PluginManager>, opts: AplOptions) -> Arc<AplConfigVisitor> {
154    let AplOptions {
155        dispatch_cache,
156        session_store,
157        pdps,
158        pdp_factories,
159        session_store_factories,
160        base_capabilities,
161    } = opts;
162
163    // Build the visitor and apply consuming builders first (these take
164    // `self` by value), then mutating registrations (`&mut self` for
165    // factories), and finally wrap in `Arc` so we can hand the shared
166    // handle to the manager. Code-supplied PDPs go through
167    // `register_pdp(&self, ...)` which uses interior mutability, so
168    // they're registered after the `Arc` wrap.
169    let mut visitor = AplConfigVisitor::new(dispatch_cache, session_store, Arc::downgrade(mgr));
170
171    if let Some(caps) = base_capabilities {
172        visitor = visitor.with_base_capabilities(caps);
173    }
174
175    for factory in pdp_factories {
176        visitor.register_pdp_factory(factory);
177    }
178
179    for factory in session_store_factories {
180        visitor.register_session_store_factory(factory);
181    }
182
183    let arc = Arc::new(visitor);
184
185    for pdp in pdps {
186        arc.register_pdp(pdp);
187    }
188
189    mgr.register_visitor(Arc::clone(&arc) as Arc<dyn ConfigVisitor>);
190    arc
191}