1use std::collections::HashMap;
10use std::path::PathBuf;
11use std::sync::Arc;
12
13use crate::acl::{Acl, AclEntry, AclOp};
14use crate::auth::{AuthError, AuthInput, AuthMode, AuthSubject};
15use crate::tls::{TlsConfigError, load_server_config, load_server_config_with_client_auth};
16
17#[derive(Clone)]
19pub struct SecurityCtx {
20 pub tls: Option<Arc<rustls::ServerConfig>>,
22 pub auth: Arc<AuthMode>,
24 pub acl: Arc<Acl>,
26}
27
28impl core::fmt::Debug for SecurityCtx {
29 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
30 f.debug_struct("SecurityCtx")
31 .field("tls_enabled", &self.tls.is_some())
32 .field("auth", &self.auth)
33 .finish_non_exhaustive()
34 }
35}
36
37#[derive(Debug, Clone, Default)]
39pub struct SecurityConfig {
40 pub tls_cert: Option<PathBuf>,
42 pub tls_key: Option<PathBuf>,
44 pub client_ca: Option<PathBuf>,
46 pub auth_mode: String,
48 pub bearer_tokens: HashMap<String, String>,
50 pub jwt_pubkey_der: Option<Vec<u8>>,
52 pub jwt_expected_iss: Option<String>,
54 pub sasl_users: HashMap<String, String>,
56 pub topic_acl: HashMap<String, AclEntry>,
58 pub topic_acl_default: Option<AclEntry>,
60}
61
62#[derive(Debug)]
64pub enum SecurityError {
65 Tls(TlsConfigError),
67 UnknownAuthMode(String),
69 MissingAuthInput(&'static str),
71}
72
73impl core::fmt::Display for SecurityError {
74 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
75 match self {
76 Self::Tls(e) => write!(f, "tls: {e}"),
77 Self::UnknownAuthMode(m) => write!(f, "unknown auth-mode: {m}"),
78 Self::MissingAuthInput(s) => write!(f, "missing auth input: {s}"),
79 }
80 }
81}
82
83impl std::error::Error for SecurityError {}
84
85impl From<TlsConfigError> for SecurityError {
86 fn from(e: TlsConfigError) -> Self {
87 Self::Tls(e)
88 }
89}
90
91pub fn build_ctx(cfg: &SecurityConfig) -> Result<SecurityCtx, SecurityError> {
97 let tls = match (&cfg.tls_cert, &cfg.tls_key) {
98 (Some(c), Some(k)) => match &cfg.client_ca {
99 Some(ca) => Some(load_server_config_with_client_auth(c, k, ca)?),
100 None => Some(load_server_config(c, k)?),
101 },
102 (None, None) => None,
103 _ => {
104 return Err(SecurityError::Tls(TlsConfigError::Rustls(
105 "tls_cert and tls_key must be set together".to_string(),
106 )));
107 }
108 };
109
110 let auth = match cfg.auth_mode.as_str() {
111 "" | "none" => AuthMode::None,
112 "bearer" => {
113 let mut tokens: HashMap<String, AuthSubject> = HashMap::new();
114 for (tok, name) in &cfg.bearer_tokens {
115 tokens.insert(tok.clone(), AuthSubject::new(name.clone()));
116 }
117 AuthMode::Bearer { tokens }
118 }
119 "jwt" => {
120 let der = cfg
121 .jwt_pubkey_der
122 .clone()
123 .ok_or(SecurityError::MissingAuthInput("jwt_pubkey_der"))?;
124 AuthMode::Jwt {
125 pkcs1_pubkey_der: der,
126 expected_issuer: cfg.jwt_expected_iss.clone(),
127 }
128 }
129 "mtls" => AuthMode::Mtls,
130 "sasl" => {
131 if cfg.sasl_users.is_empty() {
132 return Err(SecurityError::MissingAuthInput("sasl_users"));
133 }
134 AuthMode::SaslPlain {
135 users: cfg.sasl_users.clone(),
136 }
137 }
138 other => return Err(SecurityError::UnknownAuthMode(other.to_string())),
139 };
140
141 let mut acl = if cfg.topic_acl.is_empty() && cfg.topic_acl_default.is_none() {
142 if matches!(auth, AuthMode::None) {
143 Acl::allow_all()
144 } else {
145 Acl::deny_all()
146 }
147 } else {
148 Acl::deny_all()
149 };
150 for (topic, entry) in &cfg.topic_acl {
151 acl.set(topic.clone(), entry.clone());
152 }
153 if let Some(d) = &cfg.topic_acl_default {
154 acl.set_default(d.clone());
155 }
156
157 Ok(SecurityCtx {
158 tls,
159 auth: Arc::new(auth),
160 acl: Arc::new(acl),
161 })
162}
163
164pub fn authenticate(
172 auth: &AuthMode,
173 authorization_header: Option<&str>,
174 sasl_plain_blob: Option<&[u8]>,
175 mtls_subject: Option<AuthSubject>,
176) -> Result<AuthSubject, AuthError> {
177 let input = AuthInput {
178 authorization_header,
179 sasl_plain_blob,
180 mtls_subject,
181 };
182 auth.validate(&input)
183}
184
185#[must_use]
187pub fn authorize(acl: &Acl, subject: &AuthSubject, op: AclOp, topic: &str) -> bool {
188 acl.check(subject, op, topic)
189}
190
191#[must_use]
197pub fn extract_mtls_subject(conn: &rustls::ServerConnection) -> Option<AuthSubject> {
198 let certs = conn.peer_certificates()?;
199 let leaf = certs.first()?;
200 let hash = sha256_hex(leaf.as_ref());
204 Some(AuthSubject::new(format!("mtls:{hash}")))
205}
206
207fn sha256_hex(data: &[u8]) -> String {
208 use crate::backend::digest::{Context, SHA256};
209 let mut ctx = Context::new(&SHA256);
210 ctx.update(data);
211 let d = ctx.finish();
212 let mut s = String::with_capacity(64);
213 for b in d.as_ref() {
214 s.push_str(&format!("{b:02x}"));
215 }
216 s
217}
218
219#[cfg(test)]
220#[allow(clippy::expect_used, clippy::unwrap_used)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn build_default_yields_none_auth_allow_all_acl() {
226 let cfg = SecurityConfig::default();
227 let ctx = build_ctx(&cfg).unwrap();
228 assert!(ctx.tls.is_none());
229 assert!(matches!(*ctx.auth, AuthMode::None));
230 assert!(ctx.acl.check(&AuthSubject::anonymous(), AclOp::Read, "X"));
231 }
232
233 #[test]
234 fn build_bearer_with_tokens() {
235 let mut cfg = SecurityConfig {
236 auth_mode: "bearer".into(),
237 ..Default::default()
238 };
239 cfg.bearer_tokens.insert("t".into(), "alice".into());
240 let ctx = build_ctx(&cfg).unwrap();
241 let s = authenticate(&ctx.auth, Some("Bearer t"), None, None).unwrap();
242 assert_eq!(s.name, "alice");
243 }
244
245 #[test]
246 fn build_sasl_with_users() {
247 let mut cfg = SecurityConfig {
248 auth_mode: "sasl".into(),
249 ..Default::default()
250 };
251 cfg.sasl_users.insert("u".into(), "p".into());
252 let ctx = build_ctx(&cfg).unwrap();
253 let s = authenticate(&ctx.auth, None, Some(b"\0u\0p"), None).unwrap();
254 assert_eq!(s.name, "u");
255 }
256
257 #[test]
258 fn build_sasl_without_users_rejected() {
259 let cfg = SecurityConfig {
260 auth_mode: "sasl".into(),
261 ..Default::default()
262 };
263 let err = build_ctx(&cfg).unwrap_err();
264 assert!(matches!(err, SecurityError::MissingAuthInput(_)));
265 }
266
267 #[test]
268 fn unknown_auth_mode_rejected() {
269 let cfg = SecurityConfig {
270 auth_mode: "weird".into(),
271 ..Default::default()
272 };
273 let err = build_ctx(&cfg).unwrap_err();
274 assert!(matches!(err, SecurityError::UnknownAuthMode(_)));
275 }
276
277 #[test]
278 fn jwt_without_key_rejected() {
279 let cfg = SecurityConfig {
280 auth_mode: "jwt".into(),
281 ..Default::default()
282 };
283 let err = build_ctx(&cfg).unwrap_err();
284 assert!(matches!(err, SecurityError::MissingAuthInput(_)));
285 }
286
287 #[test]
288 fn explicit_acl_overrides_open_default() {
289 let mut cfg = SecurityConfig::default();
290 cfg.topic_acl.insert(
291 "T".into(),
292 AclEntry {
293 read: vec!["alice".into()],
294 write: vec!["alice".into()],
295 },
296 );
297 let ctx = build_ctx(&cfg).unwrap();
298 let alice = AuthSubject::new("alice");
299 let bob = AuthSubject::new("bob");
300 assert!(authorize(&ctx.acl, &alice, AclOp::Read, "T"));
301 assert!(!authorize(&ctx.acl, &bob, AclOp::Read, "T"));
302 assert!(!authorize(&ctx.acl, &alice, AclOp::Read, "Other"));
303 }
304
305 #[test]
306 fn explicit_acl_default_used_for_unknown() {
307 let cfg = SecurityConfig {
308 topic_acl_default: Some(AclEntry {
309 read: vec!["*".into()],
310 write: vec![],
311 }),
312 ..SecurityConfig::default()
313 };
314 let ctx = build_ctx(&cfg).unwrap();
315 let bob = AuthSubject::new("bob");
316 assert!(authorize(&ctx.acl, &bob, AclOp::Read, "Anything"));
317 assert!(!authorize(&ctx.acl, &bob, AclOp::Write, "Anything"));
318 }
319
320 #[test]
321 fn partial_tls_paths_rejected() {
322 let cfg = SecurityConfig {
323 tls_cert: Some("/x".into()),
324 tls_key: None,
325 ..Default::default()
326 };
327 let err = build_ctx(&cfg).unwrap_err();
328 assert!(matches!(err, SecurityError::Tls(_)));
329 }
330}