Files
el/ui/vessels/el-aop/src/registry.rs
T

285 lines
9.4 KiB
Rust

//! Aspect registry — register built-in and custom aspects by name.
//!
//! The compiler's AOP codegen uses the registry to look up aspect implementations
//! by their decorator name (e.g., `"authenticate"` → `AuthenticateAspect`).
//!
//! ## Security-by-default
//!
//! `"public"` is a special bypass marker — NOT an aspect. When the compiler sees
//! `@public` it calls `registry.is_public_bypass(name)` and skips default auth.
//!
//! `set_default_auth_guard()` installs the global default `AuthenticateAspect`
//! that is prepended to every non-`@public` chain.
use crate::Aspect;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
type AspectFactory = Box<dyn Fn(&HashMap<String, String>) -> Arc<dyn Aspect> + Send + Sync>;
/// Registry of aspect factories, indexed by decorator name.
pub struct AspectRegistry {
factories: RwLock<HashMap<String, AspectFactory>>,
/// When `true`, the registry is configured to prepend AuthenticateAspect
/// to every non-`@public` chain via `AspectChain::with_default_auth()`.
default_auth_enabled: RwLock<bool>,
/// Names that are bypass markers (not real aspects). Currently just "public".
bypass_markers: RwLock<std::collections::HashSet<String>>,
}
impl AspectRegistry {
pub fn new() -> Self {
let mut markers = std::collections::HashSet::new();
markers.insert("public".to_string());
Self {
factories: RwLock::new(HashMap::new()),
default_auth_enabled: RwLock::new(false),
bypass_markers: RwLock::new(markers),
}
}
/// Create a registry with all built-in aspects registered.
pub fn with_builtins() -> Self {
let registry = Self::new();
registry.register_builtins();
registry
}
/// Enable security-by-default: every non-`@public` chain will have
/// `AuthenticateAspect` prepended automatically.
///
/// Call at application startup. After this, use `AspectChain::with_default_auth()`
/// when building chains for protected functions.
pub fn set_default_auth_enabled(&self, enabled: bool) {
*self.default_auth_enabled.write().expect("registry lock poisoned") = enabled;
}
/// Returns `true` if security-by-default auth is active.
pub fn is_default_auth_enabled(&self) -> bool {
*self.default_auth_enabled.read().expect("registry lock poisoned")
}
/// Returns `true` if `name` is a public bypass marker (e.g., `"public"`).
///
/// Bypass markers are NOT aspects — they signal that default auth should
/// be skipped for the decorated function.
pub fn is_public_bypass(&self, name: &str) -> bool {
self.bypass_markers
.read()
.expect("registry lock poisoned")
.contains(name)
}
/// Register a custom bypass marker name.
///
/// By default only `"public"` is registered. Use this to add custom
/// bypass annotations (e.g., `"internal_only"` that uses a different guard).
pub fn register_bypass_marker(&self, name: &str) {
self.bypass_markers
.write()
.expect("registry lock poisoned")
.insert(name.to_string());
}
/// Build an `AspectChain` for a function with the given decorators.
///
/// This is the primary chain-building entry point used by the AOP codegen.
///
/// - If any decorator is a bypass marker (`@public`), returns a plain chain
/// with no default auth.
/// - Otherwise, if `default_auth_enabled`, prepends `AuthenticateAspect`.
/// - Unknown decorator names are silently skipped (forward-compatible).
pub fn build_chain(&self, decorator_names: &[(&str, HashMap<String, String>)]) -> crate::AspectChain {
let is_public = decorator_names.iter().any(|(name, _)| self.is_public_bypass(name));
let mut chain = crate::AspectChain::new();
for (name, params) in decorator_names {
if self.is_public_bypass(name) {
continue; // bypass markers are not aspects
}
if let Some(aspect) = self.create(name, params) {
chain = chain.add(aspect);
}
}
if !is_public && self.is_default_auth_enabled() {
chain = chain.with_default_auth();
}
chain
}
/// Register all built-in aspects.
pub fn register_builtins(&self) {
use crate::aspects::*;
self.register("authenticate", |_params| {
Arc::new(AuthenticateAspect)
});
self.register("authorize", |params| {
let role = params
.get("role")
.cloned()
.unwrap_or_else(|| "user".to_string());
Arc::new(AuthorizeAspect::new(role))
});
self.register("cache", |params| {
let ttl: u64 = params
.get("ttl")
.and_then(|s| s.parse().ok())
.unwrap_or(300);
Arc::new(CacheAspect::new(ttl))
});
self.register("rate_limit", |params| {
let requests: u32 = params
.get("requests")
.and_then(|s| s.parse().ok())
.unwrap_or(100);
let per: u64 = params
.get("per")
.and_then(|s| s.parse().ok())
.unwrap_or(60);
Arc::new(RateLimitAspect::new(requests, per))
});
self.register("log", |params| {
let level = params
.get("level")
.cloned()
.unwrap_or_else(|| "info".to_string());
Arc::new(LogAspect::new(level))
});
self.register("validate", |_params| Arc::new(ValidateAspect::new()));
self.register("retry", |params| {
let attempts: u32 = params
.get("attempts")
.and_then(|s| s.parse().ok())
.unwrap_or(3);
let backoff = params
.get("backoff")
.map(|s| s.as_str())
.unwrap_or("none");
let aspect = RetryAspect::new(attempts);
let aspect = match backoff {
"exponential" => aspect.with_exponential_backoff(100),
"fixed" => aspect.with_fixed_backoff(500),
_ => aspect,
};
Arc::new(aspect)
});
self.register("trace", |params| {
let service = params
.get("service")
.cloned()
.unwrap_or_else(|| "el-ui".to_string());
Arc::new(TraceAspect::new(service))
});
}
/// Register a custom aspect factory.
pub fn register(
&self,
name: &str,
factory: impl Fn(&HashMap<String, String>) -> Arc<dyn Aspect> + Send + Sync + 'static,
) {
self.factories
.write()
.expect("registry lock poisoned")
.insert(name.to_string(), Box::new(factory));
}
/// Instantiate an aspect by decorator name with the given params.
pub fn create(
&self,
name: &str,
params: &HashMap<String, String>,
) -> Option<Arc<dyn Aspect>> {
let factories = self.factories.read().expect("registry lock poisoned");
factories.get(name).map(|f| f(params))
}
/// List all registered aspect names.
pub fn aspect_names(&self) -> Vec<String> {
self.factories
.read()
.expect("registry lock poisoned")
.keys()
.cloned()
.collect()
}
/// Check if an aspect name is registered.
pub fn contains(&self, name: &str) -> bool {
self.factories
.read()
.expect("registry lock poisoned")
.contains_key(name)
}
}
impl Default for AspectRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod registry_default_auth_tests {
use super::*;
#[test]
fn test_default_auth_disabled_by_default() {
let reg = AspectRegistry::new();
assert!(!reg.is_default_auth_enabled());
}
#[test]
fn test_set_default_auth_enabled() {
let reg = AspectRegistry::new();
reg.set_default_auth_enabled(true);
assert!(reg.is_default_auth_enabled());
}
#[test]
fn test_public_is_bypass_marker() {
let reg = AspectRegistry::new();
assert!(reg.is_public_bypass("public"));
assert!(!reg.is_public_bypass("authenticate"));
}
#[test]
fn test_build_chain_public_skips_default_auth() {
let reg = AspectRegistry::with_builtins();
reg.set_default_auth_enabled(true);
let decorators = vec![("public", HashMap::new())];
let chain = reg.build_chain(&decorators);
assert!(!chain.has_auth(), "public chain should not have default auth");
}
#[test]
fn test_build_chain_non_public_gets_default_auth() {
let reg = AspectRegistry::with_builtins();
reg.set_default_auth_enabled(true);
let decorators = vec![("log", HashMap::new())];
let chain = reg.build_chain(&decorators);
assert!(chain.has_auth(), "non-public chain should have default auth prepended");
}
#[test]
fn test_build_chain_auth_is_first_aspect() {
let reg = AspectRegistry::with_builtins();
reg.set_default_auth_enabled(true);
let decorators = vec![("log", HashMap::new())];
let chain = reg.build_chain(&decorators);
let names = chain.aspect_names();
assert_eq!(names[0], "authenticate", "authenticate must be first in the chain");
}
}