Skip to main content

authz_resolver/
module.rs

1//! `AuthZ` resolver module.
2
3use std::sync::{Arc, OnceLock};
4
5use async_trait::async_trait;
6use authz_resolver_sdk::{AuthZResolverClient, AuthZResolverPluginSpecV1};
7use modkit::Module;
8use modkit::context::ModuleCtx;
9use modkit::contracts::SystemCapability;
10use tracing::info;
11use types_registry_sdk::{RegisterResult, TypesRegistryClient};
12
13use crate::config::AuthZResolverConfig;
14use crate::domain::{AuthZResolverLocalClient, Service};
15
16/// `AuthZ` Resolver module.
17///
18/// This module:
19/// 1. Registers the plugin schema in types-registry
20/// 2. Discovers plugin instances via types-registry
21/// 3. Routes requests to the selected plugin based on vendor configuration
22///
23/// Plugin discovery is lazy: happens on first API call after types-registry
24/// is ready.
25#[modkit::module(
26    name = "authz-resolver",
27    deps = ["types-registry"],
28    capabilities = [system]
29)]
30pub(crate) struct AuthZResolver {
31    service: OnceLock<Arc<Service>>,
32}
33
34impl Default for AuthZResolver {
35    fn default() -> Self {
36        Self {
37            service: OnceLock::new(),
38        }
39    }
40}
41
42// Marked as `system` so that init() runs in the system-module phase.
43// This ensures the AuthZResolver client is available in ClientHub before
44// other system modules that depend on it.
45impl SystemCapability for AuthZResolver {}
46
47#[async_trait]
48impl Module for AuthZResolver {
49    #[tracing::instrument(skip_all, fields(vendor))]
50    async fn init(&self, ctx: &ModuleCtx) -> anyhow::Result<()> {
51        let cfg: AuthZResolverConfig = ctx.config()?;
52        tracing::Span::current().record("vendor", cfg.vendor.as_str());
53        info!(vendor = %cfg.vendor);
54
55        // Register plugin schema in types-registry
56        let registry = ctx.client_hub().get::<dyn TypesRegistryClient>()?;
57        let schema_str = AuthZResolverPluginSpecV1::gts_schema_with_refs_as_string();
58        let mut schema_json: serde_json::Value = serde_json::from_str(&schema_str)?;
59        // Workaround for a bug in gts-macros: derived (child) schemas generated via
60        // gts_schema_with_refs_allof() omit "additionalProperties": false at the top level,
61        // even when the base schema declares it. The types-registry rejects this as loosening
62        // the base constraint. Patch it here until gts-macros is fixed upstream.
63        if let Some(obj) = schema_json.as_object_mut() {
64            obj.insert(
65                "additionalProperties".to_owned(),
66                serde_json::Value::Bool(false),
67            );
68        }
69        let results = registry.register(vec![schema_json]).await?;
70        RegisterResult::ensure_all_ok(&results)?;
71        info!(
72            schema_id = %AuthZResolverPluginSpecV1::gts_schema_id(),
73            "Registered plugin schema in types-registry"
74        );
75
76        // Create service
77        let hub = ctx.client_hub();
78        let svc = Arc::new(Service::new(hub, cfg.vendor));
79        self.service
80            .set(svc.clone())
81            .map_err(|_| anyhow::anyhow!("{} module already initialized", Self::MODULE_NAME))?;
82
83        // Register client in ClientHub
84        let api: Arc<dyn AuthZResolverClient> = Arc::new(AuthZResolverLocalClient::new(svc));
85        ctx.client_hub().register::<dyn AuthZResolverClient>(api);
86
87        Ok(())
88    }
89}