Skip to main content

authn_resolver/domain/
service.rs

1//! Domain service for the `AuthN` resolver.
2//!
3//! Plugin discovery is lazy: resolved on first API call after
4//! types-registry is ready.
5
6use std::sync::Arc;
7use std::time::Duration;
8
9use authn_resolver_sdk::{
10    AuthNResolverPluginClient, AuthNResolverPluginSpecV1, AuthenticationResult,
11};
12use modkit::client_hub::{ClientHub, ClientScope};
13use modkit::plugins::{GtsPluginSelector, choose_plugin_instance};
14use modkit::telemetry::ThrottledLog;
15use modkit_macros::domain_model;
16use tracing::info;
17use types_registry_sdk::{ListQuery, TypesRegistryClient};
18
19use super::error::DomainError;
20
21/// Throttle interval for unavailable plugin warnings.
22const UNAVAILABLE_LOG_THROTTLE: Duration = Duration::from_secs(10);
23
24/// `AuthN` resolver service.
25///
26/// Discovers plugins via types-registry and delegates authentication calls.
27#[domain_model]
28pub struct Service {
29    hub: Arc<ClientHub>,
30    vendor: String,
31    selector: GtsPluginSelector,
32    unavailable_log_throttle: ThrottledLog,
33}
34
35impl Service {
36    /// Creates a new service with lazy plugin resolution.
37    #[must_use]
38    pub fn new(hub: Arc<ClientHub>, vendor: String) -> Self {
39        Self {
40            hub,
41            vendor,
42            selector: GtsPluginSelector::new(),
43            unavailable_log_throttle: ThrottledLog::new(UNAVAILABLE_LOG_THROTTLE),
44        }
45    }
46
47    /// Lazily resolves and returns the plugin client.
48    async fn get_plugin(&self) -> Result<Arc<dyn AuthNResolverPluginClient>, DomainError> {
49        let instance_id = self.selector.get_or_init(|| self.resolve_plugin()).await?;
50        let scope = ClientScope::gts_id(instance_id.as_ref());
51
52        if let Some(client) = self
53            .hub
54            .try_get_scoped::<dyn AuthNResolverPluginClient>(&scope)
55        {
56            Ok(client)
57        } else {
58            if self.unavailable_log_throttle.should_log() {
59                tracing::warn!(
60                    plugin_gts_id = %instance_id,
61                    vendor = %self.vendor,
62                    "Plugin client not registered yet"
63                );
64            }
65            Err(DomainError::PluginUnavailable {
66                gts_id: instance_id.to_string(),
67                reason: "client not registered yet".into(),
68            })
69        }
70    }
71
72    /// Resolves the plugin instance from types-registry.
73    #[tracing::instrument(skip_all, fields(vendor = %self.vendor))]
74    async fn resolve_plugin(&self) -> Result<String, DomainError> {
75        info!("Resolving authn_resolver plugin");
76
77        let registry = self
78            .hub
79            .get::<dyn TypesRegistryClient>()
80            .map_err(|e| DomainError::TypesRegistryUnavailable(e.to_string()))?;
81
82        let plugin_type_id = AuthNResolverPluginSpecV1::gts_schema_id().clone();
83
84        let instances = registry
85            .list(
86                ListQuery::new()
87                    .with_pattern(format!("{plugin_type_id}*"))
88                    .with_is_type(false),
89            )
90            .await?;
91
92        let gts_id = choose_plugin_instance::<AuthNResolverPluginSpecV1>(
93            &self.vendor,
94            instances.iter().map(|e| (e.gts_id.as_str(), &e.content)),
95        )?;
96        info!(plugin_gts_id = %gts_id, "Selected authn_resolver plugin instance");
97
98        Ok(gts_id)
99    }
100
101    /// Authenticate a bearer token via the selected plugin.
102    ///
103    /// # Errors
104    ///
105    /// - `Unauthorized` if the token is invalid
106    /// - Plugin resolution errors
107    #[tracing::instrument(skip_all)]
108    pub async fn authenticate(
109        &self,
110        bearer_token: &str,
111    ) -> Result<AuthenticationResult, DomainError> {
112        let plugin = self.get_plugin().await?;
113        plugin
114            .authenticate(bearer_token)
115            .await
116            .map_err(DomainError::from)
117    }
118}