1use soaprs_core::{BoxFuture, SoapError, SoapResult};
4
5use crate::{Authentication, AuthorizationName, Principal};
6
7#[derive(Debug, Clone, PartialEq, Eq, Default)]
9pub enum AuthorizationPolicy {
10 #[default]
12 Public,
13 Optional,
15 OptionalStrategy(AuthorizationName),
17 Authenticated,
19 Strategy(AuthorizationName),
21 AnyRole(Vec<AuthorizationName>),
23 AllRoles(Vec<AuthorizationName>),
25 AnyPermission(Vec<AuthorizationName>),
27 AllPermissions(Vec<AuthorizationName>),
29 Named(AuthorizationName),
31}
32
33impl AuthorizationPolicy {
34 pub fn optional_strategy(name: impl Into<String>) -> SoapResult<Self> {
36 AuthorizationName::new(name).map(Self::OptionalStrategy)
37 }
38
39 pub fn strategy(name: impl Into<String>) -> SoapResult<Self> {
41 AuthorizationName::new(name).map(Self::Strategy)
42 }
43
44 pub fn named(name: impl Into<String>) -> SoapResult<Self> {
46 AuthorizationName::new(name).map(Self::Named)
47 }
48
49 pub fn any_role<I, S>(roles: I) -> SoapResult<Self>
51 where
52 I: IntoIterator<Item = S>,
53 S: Into<String>,
54 {
55 names("authorization roles", roles).map(Self::AnyRole)
56 }
57
58 pub fn all_roles<I, S>(roles: I) -> SoapResult<Self>
60 where
61 I: IntoIterator<Item = S>,
62 S: Into<String>,
63 {
64 names("authorization roles", roles).map(Self::AllRoles)
65 }
66
67 pub fn any_permission<I, S>(permissions: I) -> SoapResult<Self>
69 where
70 I: IntoIterator<Item = S>,
71 S: Into<String>,
72 {
73 names("authorization permissions", permissions).map(Self::AnyPermission)
74 }
75
76 pub fn all_permissions<I, S>(permissions: I) -> SoapResult<Self>
78 where
79 I: IntoIterator<Item = S>,
80 S: Into<String>,
81 {
82 names("authorization permissions", permissions).map(Self::AllPermissions)
83 }
84
85 pub fn validate(&self) -> SoapResult<()> {
87 match self {
88 Self::AnyRole(values)
89 | Self::AllRoles(values)
90 | Self::AnyPermission(values)
91 | Self::AllPermissions(values)
92 if values.is_empty() =>
93 {
94 Err(SoapError::validation(
95 "authorization name list cannot be empty",
96 ))
97 }
98 _ => Ok(()),
99 }
100 }
101
102 pub const fn requires_identity(&self) -> bool {
104 !matches!(
105 self,
106 Self::Public | Self::Optional | Self::OptionalStrategy(_)
107 )
108 }
109
110 pub const fn authenticates_when_present(&self) -> bool {
112 !matches!(self, Self::Public)
113 }
114
115 pub const fn allows_public_response_cache(&self) -> bool {
117 matches!(self, Self::Public)
118 }
119}
120
121fn names<I, S>(kind: &str, values: I) -> SoapResult<Vec<AuthorizationName>>
122where
123 I: IntoIterator<Item = S>,
124 S: Into<String>,
125{
126 let values = values
127 .into_iter()
128 .map(AuthorizationName::new)
129 .collect::<SoapResult<Vec<_>>>()?;
130 if values.is_empty() {
131 Err(SoapError::validation(format!("{kind} cannot be empty")))
132 } else {
133 Ok(values)
134 }
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
139pub enum AuthorizationFailure {
140 MissingAuthentication,
142 StrategyMismatch,
144 MissingRole,
146 MissingPermission,
148 PolicyDenied,
150}
151
152#[derive(Debug, Clone, PartialEq, Eq)]
154pub enum AuthorizationDecision {
155 Allowed,
157 Denied(AuthorizationFailure),
159 RequiresPolicy(AuthorizationName),
161}
162
163impl AuthorizationDecision {
164 pub fn enforce(self) -> SoapResult<()> {
166 match self {
167 Self::Allowed => Ok(()),
168 Self::Denied(
169 AuthorizationFailure::MissingAuthentication
170 | AuthorizationFailure::StrategyMismatch,
171 ) => Err(SoapError::unauthorized()),
172 Self::Denied(
173 AuthorizationFailure::MissingRole
174 | AuthorizationFailure::MissingPermission
175 | AuthorizationFailure::PolicyDenied,
176 ) => Err(SoapError::forbidden()),
177 Self::RequiresPolicy(_) => Err(SoapError::unsupported(
178 "named authorization policy requires an application evaluator",
179 )),
180 }
181 }
182}
183
184#[derive(Debug, Clone, Copy, Default)]
186pub struct DefaultAuthorizationEvaluator;
187
188impl DefaultAuthorizationEvaluator {
189 pub fn evaluate<P>(
191 &self,
192 authentication: Option<&Authentication<P>>,
193 policy: &AuthorizationPolicy,
194 ) -> SoapResult<AuthorizationDecision>
195 where
196 P: Principal,
197 {
198 policy.validate()?;
199 let decision = match policy {
200 AuthorizationPolicy::Public | AuthorizationPolicy::Optional => {
201 AuthorizationDecision::Allowed
202 }
203 AuthorizationPolicy::OptionalStrategy(strategy) => match authentication {
204 None => AuthorizationDecision::Allowed,
205 Some(authentication) if authentication.strategy() == strategy => {
206 AuthorizationDecision::Allowed
207 }
208 Some(_) => AuthorizationDecision::Denied(AuthorizationFailure::StrategyMismatch),
209 },
210 AuthorizationPolicy::Authenticated => require_authentication(authentication, |_| true),
211 AuthorizationPolicy::Strategy(strategy) => {
212 require_authentication(authentication, |authentication| {
213 authentication.strategy() == strategy
214 })
215 }
216 AuthorizationPolicy::AnyRole(roles) => {
217 require_authentication(authentication, |authentication| {
218 roles
219 .iter()
220 .any(|role| authentication.principal().has_role(role))
221 })
222 .map_denial(AuthorizationFailure::MissingRole)
223 }
224 AuthorizationPolicy::AllRoles(roles) => {
225 require_authentication(authentication, |authentication| {
226 roles
227 .iter()
228 .all(|role| authentication.principal().has_role(role))
229 })
230 .map_denial(AuthorizationFailure::MissingRole)
231 }
232 AuthorizationPolicy::AnyPermission(permissions) => {
233 require_authentication(authentication, |authentication| {
234 permissions
235 .iter()
236 .any(|permission| authentication.principal().has_permission(permission))
237 })
238 .map_denial(AuthorizationFailure::MissingPermission)
239 }
240 AuthorizationPolicy::AllPermissions(permissions) => {
241 require_authentication(authentication, |authentication| {
242 permissions
243 .iter()
244 .all(|permission| authentication.principal().has_permission(permission))
245 })
246 .map_denial(AuthorizationFailure::MissingPermission)
247 }
248 AuthorizationPolicy::Named(name) => AuthorizationDecision::RequiresPolicy(name.clone()),
249 };
250 Ok(decision)
251 }
252}
253
254trait MapDenial {
255 fn map_denial(self, denial: AuthorizationFailure) -> Self;
256}
257
258impl MapDenial for AuthorizationDecision {
259 fn map_denial(self, denial: AuthorizationFailure) -> Self {
260 match self {
261 Self::Denied(AuthorizationFailure::StrategyMismatch) => Self::Denied(denial),
262 decision => decision,
263 }
264 }
265}
266
267fn require_authentication<P, F>(
268 authentication: Option<&Authentication<P>>,
269 predicate: F,
270) -> AuthorizationDecision
271where
272 F: FnOnce(&Authentication<P>) -> bool,
273{
274 match authentication {
275 None => AuthorizationDecision::Denied(AuthorizationFailure::MissingAuthentication),
276 Some(authentication) if predicate(authentication) => AuthorizationDecision::Allowed,
277 Some(_) => AuthorizationDecision::Denied(AuthorizationFailure::StrategyMismatch),
278 }
279}
280
281pub trait Authorizer<P, C>: Send + Sync
283where
284 P: Send + Sync,
285 C: Send + Sync,
286{
287 fn authorize<'a>(
289 &'a self,
290 authentication: Option<&'a Authentication<P>>,
291 context: &'a C,
292 policy: &'a AuthorizationPolicy,
293 ) -> BoxFuture<'a, SoapResult<AuthorizationDecision>>;
294}
295
296#[cfg(test)]
297mod tests {
298 use soaprs_core::SoapErrorKind;
299
300 use super::{
301 AuthorizationDecision, AuthorizationFailure, AuthorizationPolicy,
302 DefaultAuthorizationEvaluator,
303 };
304 use crate::{Authentication, StandardPrincipal};
305
306 #[test]
307 fn built_in_policies_distinguish_authentication_and_grant_failures() {
308 let principal = StandardPrincipal::new("user-42")
309 .and_then(|principal| principal.role("admin"))
310 .and_then(|principal| principal.permission("users:read"));
311 let Some(principal) = principal.ok() else {
312 panic!("valid principal");
313 };
314 let Some(authentication) = Authentication::new("jwt", principal).ok() else {
315 panic!("valid authentication");
316 };
317 let evaluator = DefaultAuthorizationEvaluator;
318
319 let allowed = AuthorizationPolicy::all_permissions(["users:read"])
320 .and_then(|policy| evaluator.evaluate(Some(&authentication), &policy));
321 assert_eq!(allowed.ok(), Some(AuthorizationDecision::Allowed));
322
323 let denied = AuthorizationPolicy::any_role(["owner"])
324 .and_then(|policy| evaluator.evaluate(Some(&authentication), &policy));
325 assert_eq!(
326 denied.as_ref().ok(),
327 Some(&AuthorizationDecision::Denied(
328 AuthorizationFailure::MissingRole
329 ))
330 );
331 assert_eq!(
332 denied
333 .and_then(AuthorizationDecision::enforce)
334 .as_ref()
335 .map_err(|error| error.kind()),
336 Err(SoapErrorKind::Forbidden)
337 );
338
339 let missing = evaluator
340 .evaluate::<StandardPrincipal>(None, &AuthorizationPolicy::Authenticated)
341 .and_then(AuthorizationDecision::enforce);
342 assert_eq!(
343 missing.as_ref().map_err(|error| error.kind()),
344 Err(SoapErrorKind::Unauthorized)
345 );
346 }
347
348 #[test]
349 fn optional_and_named_policies_are_explicit() {
350 let evaluator = DefaultAuthorizationEvaluator;
351 assert_eq!(
352 evaluator
353 .evaluate::<StandardPrincipal>(None, &AuthorizationPolicy::Optional)
354 .ok(),
355 Some(AuthorizationDecision::Allowed)
356 );
357 let Some(named) = AuthorizationPolicy::named("resource.owner").ok() else {
358 panic!("valid named policy");
359 };
360 assert!(matches!(
361 evaluator.evaluate::<StandardPrincipal>(None, &named),
362 Ok(AuthorizationDecision::RequiresPolicy(_))
363 ));
364 }
365}