Skip to main content

authz_resolver/domain/
service.rs

1//! Domain service for the `AuthZ` resolver.
2
3use std::sync::Arc;
4use std::time::Duration;
5
6use authz_resolver_sdk::{
7    AuthZResolverPluginClient, AuthZResolverPluginSpecV1, EvaluationRequest, EvaluationResponse,
8};
9use modkit::client_hub::{ClientHub, ClientScope};
10use modkit::plugins::{GtsPluginSelector, choose_plugin_instance};
11use modkit::telemetry::ThrottledLog;
12use modkit_macros::domain_model;
13use tracing::info;
14use types_registry_sdk::{ListQuery, TypesRegistryClient};
15
16use super::error::DomainError;
17
18/// Throttle interval for unavailable plugin warnings.
19const UNAVAILABLE_LOG_THROTTLE: Duration = Duration::from_secs(10);
20
21/// `AuthZ` resolver service.
22#[domain_model]
23pub struct Service {
24    hub: Arc<ClientHub>,
25    vendor: String,
26    selector: GtsPluginSelector,
27    unavailable_log_throttle: ThrottledLog,
28}
29
30impl Service {
31    #[must_use]
32    pub fn new(hub: Arc<ClientHub>, vendor: String) -> Self {
33        Self {
34            hub,
35            vendor,
36            selector: GtsPluginSelector::new(),
37            unavailable_log_throttle: ThrottledLog::new(UNAVAILABLE_LOG_THROTTLE),
38        }
39    }
40
41    async fn get_plugin(&self) -> Result<Arc<dyn AuthZResolverPluginClient>, DomainError> {
42        let instance_id = self.selector.get_or_init(|| self.resolve_plugin()).await?;
43        let scope = ClientScope::gts_id(instance_id.as_ref());
44
45        if let Some(client) = self
46            .hub
47            .try_get_scoped::<dyn AuthZResolverPluginClient>(&scope)
48        {
49            Ok(client)
50        } else {
51            if self.unavailable_log_throttle.should_log() {
52                tracing::warn!(
53                    plugin_gts_id = %instance_id,
54                    vendor = %self.vendor,
55                    "Plugin client not registered yet"
56                );
57            }
58            Err(DomainError::PluginUnavailable {
59                gts_id: instance_id.to_string(),
60                reason: "client not registered yet".into(),
61            })
62        }
63    }
64
65    #[tracing::instrument(skip_all, fields(vendor = %self.vendor))]
66    async fn resolve_plugin(&self) -> Result<String, DomainError> {
67        info!("Resolving authz_resolver plugin");
68
69        let registry = self
70            .hub
71            .get::<dyn TypesRegistryClient>()
72            .map_err(|e| DomainError::TypesRegistryUnavailable(e.to_string()))?;
73
74        let plugin_type_id = AuthZResolverPluginSpecV1::gts_schema_id().clone();
75
76        let instances = registry
77            .list(
78                ListQuery::new()
79                    .with_pattern(format!("{plugin_type_id}*"))
80                    .with_is_type(false),
81            )
82            .await?;
83
84        let gts_id = choose_plugin_instance::<AuthZResolverPluginSpecV1>(
85            &self.vendor,
86            instances.iter().map(|e| (e.gts_id.as_str(), &e.content)),
87        )?;
88        info!(plugin_gts_id = %gts_id, "Selected authz_resolver plugin instance");
89
90        Ok(gts_id)
91    }
92
93    /// Evaluate an authorization request via the selected plugin.
94    ///
95    /// # Errors
96    ///
97    /// - Plugin resolution errors
98    /// - Plugin evaluation errors
99    #[tracing::instrument(skip_all)]
100    pub async fn evaluate(
101        &self,
102        request: EvaluationRequest,
103    ) -> Result<EvaluationResponse, DomainError> {
104        let plugin = self.get_plugin().await?;
105        plugin.evaluate(request).await.map_err(DomainError::from)
106    }
107}