Skip to main content

rustio_admin/auth/
guards.rs

1//! Authority guards — server-side enforcement of the rank model.
2//!
3//! Every authority mutation (user create / update / delete, future
4//! group / permission / recovery flows) routes through one of these
5//! pure verdict functions before touching the DB. UI hiding is a
6//! courtesy, not a security boundary; the framework refuses unsafe
7//! state changes here regardless of what the form said.
8//!
9//! The guards encode five invariants:
10//!
11//! 1. **Self-demote / self-deactivate are blocked.** A signed-in user
12//!    cannot lower their own role nor flip themselves to inactive
13//!    (matches the existing self-delete rule). Self-keep-rank is fine.
14//!
15//! 2. **Cross-rank protection.** A user cannot edit another user
16//!    whose role rank is at-or-above their own. Editing one's own
17//!    record is not blocked by this guard (the self-* guards cover
18//!    that), and editing a strictly lower-rank target is allowed.
19//!
20//! 3. **Role ceiling.** A user cannot grant a role with a rank
21//!    strictly above their own — even to themselves. Equal is
22//!    allowed (an Admin keeping their own Admin role on save).
23//!
24//! 4. **Protected-role orphan prevention.** Lives in
25//!    [`super::would_orphan_protected`]; the guard wrapper here
26//!    converts the resolved orphan-role into a clear human message.
27//!
28//! 5. _(deferred to a later phase)_ Permission ceiling — a user
29//!    cannot grant permissions they themselves don't hold. Today the
30//!    group routes are gated by `Role::Administrator`, who bypasses
31//!    group checks, so this guard is unreachable; reinstate when
32//!    delegated group management lands.
33//!
34//! All five return [`Error::Forbidden`] on rejection so the HTTP
35//! layer renders a 403 with the supplied reason.
36
37use crate::error::{Error, Result};
38use crate::orm::Db;
39
40use super::role::Role;
41use super::users::{would_orphan_protected, Identity};
42
43/// Forbid a user from saving an edit to their own record that drops
44/// their role below its current rank or flips `is_active` to false.
45/// Self-keep-rank is allowed; raising one's own rank is blocked
46/// separately by [`enforce_role_ceiling`].
47pub fn enforce_self_demote_safe(
48    actor: &Identity,
49    target_id: i64,
50    new_role: Role,
51    new_active: bool,
52) -> Result<()> {
53    if actor.user_id != target_id {
54        return Ok(());
55    }
56    if !new_active {
57        return Err(Error::Forbidden(
58            "You cannot deactivate yourself.".to_string(),
59        ));
60    }
61    if new_role.rank() < actor.role.rank() {
62        return Err(Error::Forbidden(
63            "You cannot demote yourself below your current authority level.".to_string(),
64        ));
65    }
66    Ok(())
67}
68
69/// Forbid a user from modifying another user whose role is at-or-above
70/// their own. Editing one's own record is allowed; the self-* guards
71/// catch the dangerous cases.
72pub fn enforce_cross_rank_safe(actor: &Identity, target_id: i64, target_role: Role) -> Result<()> {
73    if actor.user_id == target_id {
74        return Ok(());
75    }
76    if target_role.rank() >= actor.role.rank() {
77        return Err(Error::Forbidden(
78            "You cannot modify users at or above your authority level.".to_string(),
79        ));
80    }
81    Ok(())
82}
83
84/// Forbid a user from assigning a role with rank strictly greater
85/// than their own. Same-rank is allowed so an Admin can re-save
86/// another Admin's record (already-existing target's rank ladder is
87/// covered by [`enforce_cross_rank_safe`]).
88pub fn enforce_role_ceiling(actor: &Identity, requested_role: Role) -> Result<()> {
89    if requested_role.rank() > actor.role.rank() {
90        return Err(Error::Forbidden(
91            "You cannot assign a role higher than your own authority.".to_string(),
92        ));
93    }
94    Ok(())
95}
96
97/// Reject changes that would empty the active-member set for any
98/// protected role. Wraps [`would_orphan_protected`] and returns a
99/// human-readable error naming the role that would be orphaned.
100pub async fn enforce_no_orphan_role(
101    db: &Db,
102    target_id: i64,
103    new_role: Role,
104    new_active: bool,
105) -> Result<()> {
106    if let Some(role) = would_orphan_protected(db, target_id, new_role, new_active).await? {
107        return Err(Error::Forbidden(format!(
108            "At least one active {} must remain.",
109            role.label()
110        )));
111    }
112    Ok(())
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    fn ident(role: Role, user_id: i64) -> Identity {
120        Identity {
121            user_id,
122            email: format!("u{user_id}@test"),
123            role,
124            is_active: true,
125            is_demo: false,
126            demo_label: None,
127        }
128    }
129
130    // ---- enforce_self_demote_safe ----
131
132    #[test]
133    fn self_demote_blocks_role_drop() {
134        let actor = ident(Role::Administrator, 1);
135        let err = enforce_self_demote_safe(&actor, 1, Role::Staff, true).unwrap_err();
136        assert!(matches!(err, Error::Forbidden(_)));
137    }
138
139    #[test]
140    fn self_demote_blocks_self_deactivate() {
141        let actor = ident(Role::Administrator, 1);
142        let err = enforce_self_demote_safe(&actor, 1, Role::Administrator, false).unwrap_err();
143        assert!(matches!(err, Error::Forbidden(_)));
144    }
145
146    #[test]
147    fn self_demote_allows_self_keep_rank() {
148        let actor = ident(Role::Administrator, 1);
149        assert!(enforce_self_demote_safe(&actor, 1, Role::Administrator, true).is_ok());
150    }
151
152    #[test]
153    fn self_demote_ignores_other_targets() {
154        let actor = ident(Role::Administrator, 1);
155        // Lowering another user's role is fine for THIS guard
156        // (cross-rank handles the inverse case).
157        assert!(enforce_self_demote_safe(&actor, 2, Role::User, true).is_ok());
158    }
159
160    // ---- enforce_cross_rank_safe ----
161
162    #[test]
163    fn cross_rank_blocks_editing_higher() {
164        let actor = ident(Role::Administrator, 1);
165        let err = enforce_cross_rank_safe(&actor, 2, Role::Developer).unwrap_err();
166        assert!(matches!(err, Error::Forbidden(_)));
167    }
168
169    #[test]
170    fn cross_rank_blocks_editing_equal() {
171        let actor = ident(Role::Administrator, 1);
172        let err = enforce_cross_rank_safe(&actor, 2, Role::Administrator).unwrap_err();
173        assert!(matches!(err, Error::Forbidden(_)));
174    }
175
176    #[test]
177    fn cross_rank_allows_editing_lower() {
178        let actor = ident(Role::Administrator, 1);
179        assert!(enforce_cross_rank_safe(&actor, 2, Role::Staff).is_ok());
180        assert!(enforce_cross_rank_safe(&actor, 2, Role::Supervisor).is_ok());
181    }
182
183    #[test]
184    fn cross_rank_allows_editing_self() {
185        let actor = ident(Role::Administrator, 1);
186        // Self-edit isn't the concern of this guard.
187        assert!(enforce_cross_rank_safe(&actor, 1, Role::Administrator).is_ok());
188    }
189
190    #[test]
191    fn cross_rank_developer_can_edit_administrator() {
192        let actor = ident(Role::Developer, 1);
193        assert!(enforce_cross_rank_safe(&actor, 2, Role::Administrator).is_ok());
194    }
195
196    // ---- enforce_role_ceiling ----
197
198    #[test]
199    fn ceiling_blocks_promote_above_self() {
200        let actor = ident(Role::Administrator, 1);
201        let err = enforce_role_ceiling(&actor, Role::Developer).unwrap_err();
202        assert!(matches!(err, Error::Forbidden(_)));
203    }
204
205    #[test]
206    fn ceiling_allows_assigning_equal_or_below() {
207        let actor = ident(Role::Administrator, 1);
208        assert!(enforce_role_ceiling(&actor, Role::Administrator).is_ok());
209        assert!(enforce_role_ceiling(&actor, Role::Supervisor).is_ok());
210        assert!(enforce_role_ceiling(&actor, Role::Staff).is_ok());
211        assert!(enforce_role_ceiling(&actor, Role::User).is_ok());
212    }
213
214    #[test]
215    fn ceiling_supervisor_cannot_create_administrator() {
216        let actor = ident(Role::Supervisor, 1);
217        let err = enforce_role_ceiling(&actor, Role::Administrator).unwrap_err();
218        assert!(matches!(err, Error::Forbidden(_)));
219    }
220
221    #[test]
222    fn ceiling_developer_can_assign_anything_inclusive() {
223        let actor = ident(Role::Developer, 1);
224        for r in [
225            Role::User,
226            Role::Staff,
227            Role::Supervisor,
228            Role::Administrator,
229            Role::Developer,
230        ] {
231            assert!(enforce_role_ceiling(&actor, r).is_ok());
232        }
233    }
234}