Skip to main content

aa_auth/
scope.rs

1//! Authorization scope levels for API operations.
2
3use axum::extract::FromRequestParts;
4use axum::http::request::Parts;
5use serde::{Deserialize, Serialize};
6
7use super::{AuthError, AuthenticatedCaller};
8
9/// Authorization scope level for API operations.
10///
11/// Variants are ordered by privilege: `Read < Write < Admin`.
12/// A caller with `Admin` scope satisfies any scope requirement.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, utoipa::ToSchema)]
14#[serde(rename_all = "lowercase")]
15pub enum Scope {
16    /// Read-only access to resources.
17    Read,
18    /// Read and write access (create, update, delete).
19    Write,
20    /// Global / cross-tenant administrative access — e.g. fleet-wide halt,
21    /// retention policy, and IAM key management. Per-tenant destructive actions
22    /// (agent suspend/resume/delete, op lifecycle) are gated by Write plus
23    /// tenant ownership, not flat Admin.
24    Admin,
25}
26
27impl Scope {
28    /// Check whether the given set of scopes satisfies this required scope.
29    ///
30    /// Returns `true` if any scope in `granted` is >= `self` in the
31    /// privilege ordering.
32    pub fn is_satisfied_by(self, granted: &[Scope]) -> bool {
33        granted.iter().any(|s| *s >= self)
34    }
35}
36
37impl std::fmt::Display for Scope {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        match self {
40            Scope::Read => write!(f, "read"),
41            Scope::Write => write!(f, "write"),
42            Scope::Admin => write!(f, "admin"),
43        }
44    }
45}
46
47/// Axum extractor that enforces a minimum scope level.
48///
49/// Use as a handler parameter to gate access:
50///
51/// ```ignore
52/// async fn admin_only(_scope: RequireScope<{ Scope::Admin as u8 }>) { ... }
53/// ```
54///
55/// This extractor first resolves the [`AuthenticatedCaller`] and then
56/// checks that the caller's scopes satisfy the required level.
57pub struct RequireScope(pub AuthenticatedCaller);
58
59impl RequireScope {
60    /// Validate that the caller has at least the given scope.
61    fn check(caller: &AuthenticatedCaller, required: Scope) -> Result<(), AuthError> {
62        if required.is_satisfied_by(&caller.scopes) {
63            Ok(())
64        } else {
65            Err(AuthError::InsufficientScope { required })
66        }
67    }
68}
69
70/// Require `Scope::Read` — the caller must have at least read access.
71pub struct RequireRead(pub AuthenticatedCaller);
72
73impl<S> FromRequestParts<S> for RequireRead
74where
75    S: Send + Sync,
76{
77    type Rejection = AuthError;
78
79    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
80        let caller = AuthenticatedCaller::from_request_parts(parts, state).await?;
81        RequireScope::check(&caller, Scope::Read)?;
82        Ok(Self(caller))
83    }
84}
85
86/// Require `Scope::Write` — the caller must have at least write access.
87pub struct RequireWrite(pub AuthenticatedCaller);
88
89impl<S> FromRequestParts<S> for RequireWrite
90where
91    S: Send + Sync,
92{
93    type Rejection = AuthError;
94
95    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
96        let caller = AuthenticatedCaller::from_request_parts(parts, state).await?;
97        RequireScope::check(&caller, Scope::Write)?;
98        Ok(Self(caller))
99    }
100}
101
102/// Require `Scope::Admin` — the caller must have admin access.
103pub struct RequireAdmin(pub AuthenticatedCaller);
104
105impl<S> FromRequestParts<S> for RequireAdmin
106where
107    S: Send + Sync,
108{
109    type Rejection = AuthError;
110
111    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
112        let caller = AuthenticatedCaller::from_request_parts(parts, state).await?;
113        RequireScope::check(&caller, Scope::Admin)?;
114        Ok(Self(caller))
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn test_scope_ordering() {
124        assert!(Scope::Admin > Scope::Write);
125        assert!(Scope::Write > Scope::Read);
126        assert!(Scope::Admin > Scope::Read);
127    }
128
129    #[test]
130    fn test_scope_contains_same_level() {
131        assert!(Scope::Write.is_satisfied_by(&[Scope::Write]));
132        assert!(Scope::Read.is_satisfied_by(&[Scope::Read]));
133        assert!(Scope::Admin.is_satisfied_by(&[Scope::Admin]));
134    }
135
136    #[test]
137    fn test_scope_contains_higher_level() {
138        assert!(Scope::Write.is_satisfied_by(&[Scope::Admin]));
139        assert!(Scope::Read.is_satisfied_by(&[Scope::Admin]));
140        assert!(Scope::Read.is_satisfied_by(&[Scope::Write]));
141    }
142
143    #[test]
144    fn test_scope_rejects_lower_level() {
145        assert!(!Scope::Write.is_satisfied_by(&[Scope::Read]));
146        assert!(!Scope::Admin.is_satisfied_by(&[Scope::Write]));
147        assert!(!Scope::Admin.is_satisfied_by(&[Scope::Read]));
148    }
149
150    #[test]
151    fn test_scope_empty_grants_rejects_all() {
152        assert!(!Scope::Read.is_satisfied_by(&[]));
153        assert!(!Scope::Write.is_satisfied_by(&[]));
154        assert!(!Scope::Admin.is_satisfied_by(&[]));
155    }
156
157    #[test]
158    fn test_scope_check_with_caller() {
159        let caller = AuthenticatedCaller {
160            key_id: "test".to_string(),
161            scopes: vec![Scope::Read, Scope::Write],
162            tenant: crate::Tenant::default(),
163        };
164        assert!(RequireScope::check(&caller, Scope::Read).is_ok());
165        assert!(RequireScope::check(&caller, Scope::Write).is_ok());
166        assert!(RequireScope::check(&caller, Scope::Admin).is_err());
167    }
168
169    #[test]
170    fn scope_display_renders_each_level() {
171        assert_eq!(Scope::Read.to_string(), "read");
172        assert_eq!(Scope::Write.to_string(), "write");
173        assert_eq!(Scope::Admin.to_string(), "admin");
174    }
175}