zerodds_webdds_bridge/daemon/
security.rs1pub use zerodds_bridge_security::{
12 Acl, AclEntry, AclOp, AuthError, AuthMode, AuthSubject, RotatingTlsConfig, SecurityConfig,
13 SecurityCtx, SecurityError, authenticate as bs_authenticate, authorize, build_ctx,
14 extract_mtls_subject, serve_tls_handshake,
15};
16
17pub fn ctx_from_daemon_config(
23 cfg: &super::config::DaemonConfig,
24) -> Result<(SecurityCtx, Option<RotatingTlsConfig>), SecurityError> {
25 let mut sc = SecurityConfig::default();
26 if cfg.tls_enabled {
27 if cfg.tls_cert_file.is_empty() || cfg.tls_key_file.is_empty() {
28 return Err(SecurityError::Tls(
29 zerodds_bridge_security::TlsConfigError::Rustls(
30 "tls.enabled=true requires cert_file + key_file".into(),
31 ),
32 ));
33 }
34 sc.tls_cert = Some(cfg.tls_cert_file.clone().into());
35 sc.tls_key = Some(cfg.tls_key_file.clone().into());
36 if !cfg.tls_client_ca_file.is_empty() {
37 sc.client_ca = Some(cfg.tls_client_ca_file.clone().into());
38 }
39 }
40 sc.auth_mode = cfg.auth_mode.clone();
41 match cfg.auth_mode.as_str() {
42 "jwt" => sc.apply_jwt(&cfg.auth_jwt)?,
43 "mtls" if !cfg.tls_enabled || cfg.tls_client_ca_file.is_empty() => {
44 return Err(SecurityError::Config(
45 "auth.mode: mtls needs tls.enabled and tls.client_ca_file".into(),
46 ));
47 }
48 _ => {}
49 }
50 if let (Some(tok), Some(subj)) = (
51 cfg.auth_bearer_token.as_ref(),
52 cfg.auth_bearer_subject.as_ref(),
53 ) {
54 sc.bearer_tokens.insert(tok.clone(), subj.clone());
55 } else if let Some(tok) = cfg.auth_bearer_token.as_ref() {
56 sc.bearer_tokens.insert(tok.clone(), "anonymous".into());
57 }
58 for (app, (read, write)) in &cfg.app_acl {
59 sc.topic_acl.insert(
60 app.clone(),
61 AclEntry {
62 read: read.clone(),
63 write: write.clone(),
64 },
65 );
66 }
67 let ctx = build_ctx(&sc)?;
68 let rotating = match (cfg.tls_enabled, sc.tls_cert.clone(), sc.tls_key.clone()) {
69 (true, Some(c), Some(k)) => {
70 Some(RotatingTlsConfig::load(c, k, sc.client_ca.clone()).map_err(SecurityError::Tls)?)
71 }
72 _ => None,
73 };
74 Ok((ctx, rotating))
75}
76
77pub fn authenticate_http(
83 auth: &AuthMode,
84 authorization_header: Option<&str>,
85 mtls_subject: Option<AuthSubject>,
86) -> Result<AuthSubject, AuthError> {
87 bs_authenticate(auth, authorization_header, None, mtls_subject)
88}
89
90#[must_use]
95pub fn application_of_path(path: &str) -> Option<String> {
96 let marker = "/applications/";
97 let start = path.find(marker)? + marker.len();
98 let rest = &path[start..];
99 let end = rest.find('/').unwrap_or(rest.len());
100 let name = &rest[..end];
101 (!name.is_empty()).then(|| name.to_string())
102}
103
104#[must_use]
107pub fn op_of_method(method: &str) -> AclOp {
108 if method.eq_ignore_ascii_case("GET") || method.eq_ignore_ascii_case("HEAD") {
109 AclOp::Read
110 } else {
111 AclOp::Write
112 }
113}
114
115#[cfg(test)]
116#[allow(clippy::unwrap_used, clippy::expect_used)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn application_of_path_extracts_the_name() {
122 assert_eq!(
123 application_of_path("/dds/rest1/applications/app1/domain_participants"),
124 Some("app1".to_string())
125 );
126 }
127
128 #[test]
129 fn application_of_path_none_for_the_collection_itself() {
130 assert_eq!(application_of_path("/dds/rest1/applications"), None);
131 }
132
133 #[test]
134 fn op_of_method_maps_get_to_read() {
135 assert_eq!(op_of_method("GET"), AclOp::Read);
136 assert_eq!(op_of_method("HEAD"), AclOp::Read);
137 assert_eq!(op_of_method("POST"), AclOp::Write);
138 assert_eq!(op_of_method("DELETE"), AclOp::Write);
139 }
140
141 #[test]
142 fn ctx_from_config_with_bearer_tokens() {
143 let mut cfg = super::super::config::DaemonConfig::default_for_dev();
144 cfg.auth_mode = "bearer".into();
145 cfg.auth_bearer_token = Some("tok123".into());
146 cfg.auth_bearer_subject = Some("alice".into());
147 cfg.app_acl
148 .insert("app1".into(), (vec!["alice".into()], vec!["alice".into()]));
149 let (ctx, rot) = ctx_from_daemon_config(&cfg).expect("build");
150 assert!(rot.is_none());
151 assert!(matches!(*ctx.auth, AuthMode::Bearer { .. }));
152 let alice = AuthSubject::new("alice");
153 assert!(authorize(&ctx.acl, &alice, AclOp::Read, "app1"));
154 }
155
156 #[test]
157 fn ctx_from_config_tls_enabled_without_paths_rejected() {
158 let mut cfg = super::super::config::DaemonConfig::default_for_dev();
159 cfg.tls_enabled = true;
160 assert!(matches!(
161 ctx_from_daemon_config(&cfg).unwrap_err(),
162 SecurityError::Tls(_)
163 ));
164 }
165}