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 GrantSource::Operator => {
72 format!("subject `{subject}` is the operator and already holds the deploy grant")
73 }
74 };
75 ServerError::deploy_denied(format!(
76 "subject `{subject}` is not authorized to deploy; {hint}"
77 ))
78}
79
80#[cfg(test)]
81mod tests {
82 use aion_proto::WireErrorCode;
83
84 use super::DeployGuard;
85 use crate::config::NamespaceMode;
86 use crate::namespace::{
87 CallerIdentity, NamespaceResolver, StaticScheduleNamespaces, StaticWorkflowNamespaces,
88 };
89
90 fn guard() -> DeployGuard {
91 DeployGuard::new(NamespaceResolver::authorization_only(
92 NamespaceMode::SharedEngine,
93 StaticWorkflowNamespaces::default(),
94 StaticScheduleNamespaces::default(),
95 ))
96 }
97
98 #[test]
99 fn granted_caller_is_authorized() -> Result<(), Box<dyn std::error::Error>> {
100 let header_caller = CallerIdentity::new("ci", [String::from("tenant-a")]).with_deploy(true);
101 let token_caller =
102 CallerIdentity::from_token_claims("ci", [String::from("tenant-a")]).with_deploy(true);
103
104 guard().authorize(&header_caller)?;
105 guard().authorize(&token_caller)?;
106 Ok(())
107 }
108
109 #[test]
115 fn denial_hint_names_the_grant_source() -> Result<(), Box<dyn std::error::Error>> {
116 let header_caller = CallerIdentity::new("ci", [String::from("tenant-a")]);
117 let header_denial = guard()
118 .authorize(&header_caller)
119 .err()
120 .map(|error| error.to_wire_error())
121 .ok_or("expected header-sourced caller to be denied")?;
122 assert_eq!(header_denial.code, WireErrorCode::DeployDenied);
123 assert!(
124 header_denial
125 .message
126 .contains("subject `ci` is not authorized to deploy"),
127 "denial must name the subject: {}",
128 header_denial.message
129 );
130 assert!(
131 header_denial.message.contains("x-aion-deploy"),
132 "header-path denial must hint the dev header: {}",
133 header_denial.message
134 );
135 assert!(
136 !header_denial.message.contains("deploy claim"),
137 "header-path denial must not hint the token claim: {}",
138 header_denial.message
139 );
140
141 let token_caller = CallerIdentity::from_token_claims("ci", [String::from("tenant-a")]);
142 let token_denial = guard()
143 .authorize(&token_caller)
144 .err()
145 .map(|error| error.to_wire_error())
146 .ok_or("expected token-sourced caller to be denied")?;
147 assert_eq!(token_denial.code, WireErrorCode::DeployDenied);
148 assert!(
149 token_denial.message.contains("deploy claim"),
150 "JWT-path denial must hint the token's deploy claim: {}",
151 token_denial.message
152 );
153 assert!(
154 !token_denial.message.contains("x-aion-deploy"),
155 "JWT-path denial must not hint the dev header: {}",
156 token_denial.message
157 );
158 Ok(())
159 }
160
161 #[test]
164 fn transport_denied_caller_is_deploy_denied_with_reason()
165 -> Result<(), Box<dyn std::error::Error>> {
166 let denied = CallerIdentity::denied("ci", "invalid bearer token").with_deploy(true);
167
168 let error = guard()
169 .authorize(&denied)
170 .err()
171 .map(|error| error.to_wire_error())
172 .ok_or("expected transport-denied caller to be refused")?;
173 assert_eq!(error.code, WireErrorCode::DeployDenied);
174 assert!(
175 error.message.contains("invalid bearer token"),
176 "denial must carry the transport reason: {}",
177 error.message
178 );
179 Ok(())
180 }
181
182 #[test]
185 fn namespace_grants_do_not_imply_deploy() {
186 let caller = CallerIdentity::new("ci", [String::from("tenant-a"), String::from("b")]);
187
188 let result = guard().authorize(&caller);
189 assert_eq!(
190 result.err().map(|error| error.to_wire_error().code),
191 Some(WireErrorCode::DeployDenied)
192 );
193 }
194}