aion_server/deploy/
guard.rs1use std::sync::Arc;
11
12use aion::Engine;
13
14use crate::error::ServerError;
15use crate::namespace::resolver::{CallerIdentity, GrantSource, NamespaceResolver};
16
17#[derive(Clone)]
20pub struct DeployGuard {
21 resolver: NamespaceResolver,
22}
23
24impl DeployGuard {
25 #[must_use]
27 pub const fn new(resolver: NamespaceResolver) -> Self {
28 Self { resolver }
29 }
30
31 pub fn authorize(&self, caller: &CallerIdentity) -> Result<(), ServerError> {
40 if let Some(reason) = caller.denial_reason() {
41 return Err(ServerError::deploy_denied(reason));
42 }
43 if caller.deploy_granted() {
44 return Ok(());
45 }
46 Err(deploy_denied(caller))
47 }
48
49 pub fn engine(&self) -> Result<&Arc<Engine>, ServerError> {
56 self.resolver.engine()
57 }
58}
59
60fn deploy_denied(caller: &CallerIdentity) -> ServerError {
61 let subject = caller.subject();
62 let hint = match caller.grant_source() {
63 GrantSource::NamespacesHeader => {
64 format!("set x-aion-deploy: true for subject `{subject}`")
65 }
66 GrantSource::TokenClaim => {
67 format!("mint a token whose deploy claim is true for subject `{subject}`")
68 }
69 };
70 ServerError::deploy_denied(format!(
71 "subject `{subject}` is not authorized to deploy; {hint}"
72 ))
73}
74
75#[cfg(test)]
76mod tests {
77 use aion_proto::WireErrorCode;
78
79 use super::DeployGuard;
80 use crate::config::NamespaceMode;
81 use crate::namespace::{
82 CallerIdentity, NamespaceResolver, StaticScheduleNamespaces, StaticWorkflowNamespaces,
83 };
84
85 fn guard() -> DeployGuard {
86 DeployGuard::new(NamespaceResolver::authorization_only(
87 NamespaceMode::SharedEngine,
88 StaticWorkflowNamespaces::default(),
89 StaticScheduleNamespaces::default(),
90 ))
91 }
92
93 #[test]
94 fn granted_caller_is_authorized() -> Result<(), Box<dyn std::error::Error>> {
95 let header_caller = CallerIdentity::new("ci", [String::from("tenant-a")]).with_deploy(true);
96 let token_caller =
97 CallerIdentity::from_token_claims("ci", [String::from("tenant-a")]).with_deploy(true);
98
99 guard().authorize(&header_caller)?;
100 guard().authorize(&token_caller)?;
101 Ok(())
102 }
103
104 #[test]
110 fn denial_hint_names_the_grant_source() -> Result<(), Box<dyn std::error::Error>> {
111 let header_caller = CallerIdentity::new("ci", [String::from("tenant-a")]);
112 let header_denial = guard()
113 .authorize(&header_caller)
114 .err()
115 .map(|error| error.to_wire_error())
116 .ok_or("expected header-sourced caller to be denied")?;
117 assert_eq!(header_denial.code, WireErrorCode::DeployDenied);
118 assert!(
119 header_denial
120 .message
121 .contains("subject `ci` is not authorized to deploy"),
122 "denial must name the subject: {}",
123 header_denial.message
124 );
125 assert!(
126 header_denial.message.contains("x-aion-deploy"),
127 "header-path denial must hint the dev header: {}",
128 header_denial.message
129 );
130 assert!(
131 !header_denial.message.contains("deploy claim"),
132 "header-path denial must not hint the token claim: {}",
133 header_denial.message
134 );
135
136 let token_caller = CallerIdentity::from_token_claims("ci", [String::from("tenant-a")]);
137 let token_denial = guard()
138 .authorize(&token_caller)
139 .err()
140 .map(|error| error.to_wire_error())
141 .ok_or("expected token-sourced caller to be denied")?;
142 assert_eq!(token_denial.code, WireErrorCode::DeployDenied);
143 assert!(
144 token_denial.message.contains("deploy claim"),
145 "JWT-path denial must hint the token's deploy claim: {}",
146 token_denial.message
147 );
148 assert!(
149 !token_denial.message.contains("x-aion-deploy"),
150 "JWT-path denial must not hint the dev header: {}",
151 token_denial.message
152 );
153 Ok(())
154 }
155
156 #[test]
159 fn transport_denied_caller_is_deploy_denied_with_reason()
160 -> Result<(), Box<dyn std::error::Error>> {
161 let denied = CallerIdentity::denied("ci", "invalid bearer token").with_deploy(true);
162
163 let error = guard()
164 .authorize(&denied)
165 .err()
166 .map(|error| error.to_wire_error())
167 .ok_or("expected transport-denied caller to be refused")?;
168 assert_eq!(error.code, WireErrorCode::DeployDenied);
169 assert!(
170 error.message.contains("invalid bearer token"),
171 "denial must carry the transport reason: {}",
172 error.message
173 );
174 Ok(())
175 }
176
177 #[test]
180 fn namespace_grants_do_not_imply_deploy() {
181 let caller = CallerIdentity::new("ci", [String::from("tenant-a"), String::from("b")]);
182
183 let result = guard().authorize(&caller);
184 assert_eq!(
185 result.err().map(|error| error.to_wire_error().code),
186 Some(WireErrorCode::DeployDenied)
187 );
188 }
189}