Skip to main content

static_authn_plugin/domain/
service.rs

1//! Service implementation for the static `AuthN` resolver plugin.
2
3use std::collections::HashMap;
4
5use modkit_macros::domain_model;
6use modkit_security::SecurityContext;
7
8use crate::config::{AuthNMode, IdentityConfig, StaticAuthNPluginConfig};
9use authn_resolver_sdk::AuthenticationResult;
10
11/// Static `AuthN` resolver service.
12///
13/// Provides token-to-identity mapping based on configuration mode:
14/// - `accept_all`: Any non-empty token maps to the default identity
15/// - `static_tokens`: Specific tokens map to specific identities
16#[domain_model]
17pub struct Service {
18    mode: AuthNMode,
19    default_identity: IdentityConfig,
20    token_map: HashMap<String, IdentityConfig>,
21}
22
23impl Service {
24    /// Create a service from plugin configuration.
25    #[must_use]
26    pub fn from_config(cfg: &StaticAuthNPluginConfig) -> Self {
27        let token_map: HashMap<String, IdentityConfig> = cfg
28            .tokens
29            .iter()
30            .map(|m| (m.token.clone(), m.identity.clone()))
31            .collect();
32
33        Self {
34            mode: cfg.mode.clone(),
35            default_identity: cfg.default_identity.clone(),
36            token_map,
37        }
38    }
39
40    /// Authenticate a bearer token and return the identity.
41    ///
42    /// Returns `None` if the token is not recognized (in `static_tokens` mode)
43    /// or empty.
44    #[must_use]
45    pub fn authenticate(&self, bearer_token: &str) -> Option<AuthenticationResult> {
46        if bearer_token.is_empty() {
47            return None;
48        }
49
50        let identity = match &self.mode {
51            AuthNMode::AcceptAll => &self.default_identity,
52            AuthNMode::StaticTokens => self.token_map.get(bearer_token)?,
53        };
54
55        build_result(identity, bearer_token)
56    }
57}
58
59fn build_result(identity: &IdentityConfig, bearer_token: &str) -> Option<AuthenticationResult> {
60    let ctx = SecurityContext::builder()
61        .subject_id(identity.subject_id)
62        .subject_tenant_id(identity.subject_tenant_id)
63        .token_scopes(identity.token_scopes.clone())
64        .bearer_token(bearer_token.to_owned())
65        .build()
66        .map_err(|e| tracing::error!("Failed to build SecurityContext from config: {e}"))
67        .ok()?;
68
69    Some(AuthenticationResult {
70        security_context: ctx,
71    })
72}
73
74#[cfg(test)]
75#[cfg_attr(coverage_nightly, coverage(off))]
76mod tests {
77    use secrecy::ExposeSecret;
78
79    use super::*;
80    use crate::config::TokenMapping;
81    use uuid::Uuid;
82
83    fn default_config() -> StaticAuthNPluginConfig {
84        StaticAuthNPluginConfig::default()
85    }
86
87    #[test]
88    fn accept_all_mode_returns_default_identity() {
89        let service = Service::from_config(&default_config());
90
91        let result = service.authenticate("any-token-value");
92        assert!(result.is_some());
93
94        let auth = result.unwrap();
95        let ctx = &auth.security_context;
96        assert_eq!(
97            ctx.subject_id(),
98            modkit_security::constants::DEFAULT_SUBJECT_ID
99        );
100        assert_eq!(
101            ctx.subject_tenant_id(),
102            modkit_security::constants::DEFAULT_TENANT_ID
103        );
104        assert_eq!(ctx.token_scopes(), &["*"]);
105        assert_eq!(
106            ctx.bearer_token().map(ExposeSecret::expose_secret),
107            Some("any-token-value"),
108        );
109    }
110
111    #[test]
112    fn accept_all_mode_rejects_empty_token() {
113        let service = Service::from_config(&default_config());
114
115        let result = service.authenticate("");
116        assert!(result.is_none());
117    }
118
119    #[test]
120    fn static_tokens_mode_returns_mapped_identity() {
121        let user_a_id = Uuid::parse_str("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa").unwrap();
122        let tenant_a = Uuid::parse_str("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb").unwrap();
123
124        let cfg = StaticAuthNPluginConfig {
125            mode: AuthNMode::StaticTokens,
126            tokens: vec![TokenMapping {
127                token: "token-user-a".to_owned(),
128                identity: IdentityConfig {
129                    subject_id: user_a_id,
130                    subject_tenant_id: tenant_a,
131                    token_scopes: vec!["read:data".to_owned()],
132                },
133            }],
134            ..default_config()
135        };
136
137        let service = Service::from_config(&cfg);
138
139        let result = service.authenticate("token-user-a");
140        assert!(result.is_some());
141
142        let auth = result.unwrap();
143        let ctx = &auth.security_context;
144        assert_eq!(ctx.subject_id(), user_a_id);
145        assert_eq!(ctx.subject_tenant_id(), tenant_a);
146        assert_eq!(ctx.token_scopes(), &["read:data"]);
147        assert_eq!(
148            ctx.bearer_token().map(ExposeSecret::expose_secret),
149            Some("token-user-a"),
150        );
151    }
152
153    #[test]
154    fn static_tokens_mode_rejects_unknown_token() {
155        let cfg = StaticAuthNPluginConfig {
156            mode: AuthNMode::StaticTokens,
157            tokens: vec![TokenMapping {
158                token: "known-token".to_owned(),
159                identity: IdentityConfig::default(),
160            }],
161            ..default_config()
162        };
163
164        let service = Service::from_config(&cfg);
165
166        let result = service.authenticate("unknown-token");
167        assert!(result.is_none());
168    }
169
170    #[test]
171    fn static_tokens_mode_rejects_empty_token() {
172        let cfg = StaticAuthNPluginConfig {
173            mode: AuthNMode::StaticTokens,
174            tokens: vec![],
175            ..default_config()
176        };
177
178        let service = Service::from_config(&cfg);
179
180        let result = service.authenticate("");
181        assert!(result.is_none());
182    }
183}