static_authn_plugin/domain/
service.rs1use 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#[domain_model]
17pub struct Service {
18 mode: AuthNMode,
19 default_identity: IdentityConfig,
20 token_map: HashMap<String, IdentityConfig>,
21}
22
23impl Service {
24 #[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 #[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}