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}