Skip to main content

fraiseql_server/middleware/
admin_scope.rs

1//! Admin scope guard for JWT-authenticated admin requests.
2//!
3//! When admin routes receive a JWT (rather than a raw `admin_token`), the guard
4//! verifies that the JWT's `scope` claim contains `fraiseql:admin`. Bearer-token
5//! authenticated requests (using `admin_token`) bypass this check entirely for
6//! backwards compatibility.
7//!
8//! The guard is additive: it does not replace bearer-token auth. Routes that use
9//! the guard should apply it **after** the bearer-auth middleware.
10
11/// The required scope claim value for admin API access via JWT.
12pub const ADMIN_SCOPE: &str = "fraiseql:admin";
13
14/// Check whether a space-delimited scope string contains the `fraiseql:admin` scope.
15///
16/// Scope claims in JWTs are typically a space-separated list of scope values
17/// (RFC 8693 / `OpenID` Connect).
18///
19/// # Examples
20///
21/// ```
22/// use fraiseql_server::middleware::admin_scope::has_admin_scope;
23///
24/// assert!(has_admin_scope("fraiseql:admin"));
25/// assert!(has_admin_scope("read write fraiseql:admin"));
26/// assert!(!has_admin_scope("read write"));
27/// assert!(!has_admin_scope(""));
28/// ```
29#[must_use]
30pub fn has_admin_scope(scope_claim: &str) -> bool {
31    scope_claim.split_whitespace().any(|s| s == ADMIN_SCOPE)
32}
33
34/// Validate that a JWT scope claim authorizes admin access.
35///
36/// Returns `Ok(())` if the scope claim contains `fraiseql:admin`,
37/// `Err` with a 403 message otherwise.
38///
39/// # Errors
40///
41/// Returns `FraiseQLError::Authorization` if the scope claim does not
42/// contain `fraiseql:admin`.
43pub fn require_admin_scope(scope_claim: &str) -> fraiseql_error::Result<()> {
44    if has_admin_scope(scope_claim) {
45        Ok(())
46    } else {
47        Err(fraiseql_error::FraiseQLError::unauthorized(format!(
48            "Admin API requires '{ADMIN_SCOPE}' scope. \
49             Found: '{scope_claim}'"
50        )))
51    }
52}