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            must_change_password: false,
128        }
129    }
130
131    // ---- enforce_self_demote_safe ----
132
133    #[test]
134    fn self_demote_blocks_role_drop() {
135        let actor = ident(Role::Administrator, 1);
136        let err = enforce_self_demote_safe(&actor, 1, Role::Staff, true).unwrap_err();
137        assert!(matches!(err, Error::Forbidden(_)));
138    }
139
140    #[test]
141    fn self_demote_blocks_self_deactivate() {
142        let actor = ident(Role::Administrator, 1);
143        let err = enforce_self_demote_safe(&actor, 1, Role::Administrator, false).unwrap_err();
144        assert!(matches!(err, Error::Forbidden(_)));
145    }
146
147    #[test]
148    fn self_demote_allows_self_keep_rank() {
149        let actor = ident(Role::Administrator, 1);
150        assert!(enforce_self_demote_safe(&actor, 1, Role::Administrator, true).is_ok());
151    }
152
153    #[test]
154    fn self_demote_ignores_other_targets() {
155        let actor = ident(Role::Administrator, 1);
156        // Lowering another user's role is fine for THIS guard
157        // (cross-rank handles the inverse case).
158        assert!(enforce_self_demote_safe(&actor, 2, Role::User, true).is_ok());
159    }
160
161    // ---- enforce_cross_rank_safe ----
162
163    #[test]
164    fn cross_rank_blocks_editing_higher() {
165        let actor = ident(Role::Administrator, 1);
166        let err = enforce_cross_rank_safe(&actor, 2, Role::Developer).unwrap_err();
167        assert!(matches!(err, Error::Forbidden(_)));
168    }
169
170    #[test]
171    fn cross_rank_blocks_editing_equal() {
172        let actor = ident(Role::Administrator, 1);
173        let err = enforce_cross_rank_safe(&actor, 2, Role::Administrator).unwrap_err();
174        assert!(matches!(err, Error::Forbidden(_)));
175    }
176
177    #[test]
178    fn cross_rank_allows_editing_lower() {
179        let actor = ident(Role::Administrator, 1);
180        assert!(enforce_cross_rank_safe(&actor, 2, Role::Staff).is_ok());
181        assert!(enforce_cross_rank_safe(&actor, 2, Role::Supervisor).is_ok());
182    }
183
184    #[test]
185    fn cross_rank_allows_editing_self() {
186        let actor = ident(Role::Administrator, 1);
187        // Self-edit isn't the concern of this guard.
188        assert!(enforce_cross_rank_safe(&actor, 1, Role::Administrator).is_ok());
189    }
190
191    #[test]
192    fn cross_rank_developer_can_edit_administrator() {
193        let actor = ident(Role::Developer, 1);
194        assert!(enforce_cross_rank_safe(&actor, 2, Role::Administrator).is_ok());
195    }
196
197    // ---- enforce_role_ceiling ----
198
199    #[test]
200    fn ceiling_blocks_promote_above_self() {
201        let actor = ident(Role::Administrator, 1);
202        let err = enforce_role_ceiling(&actor, Role::Developer).unwrap_err();
203        assert!(matches!(err, Error::Forbidden(_)));
204    }
205
206    #[test]
207    fn ceiling_allows_assigning_equal_or_below() {
208        let actor = ident(Role::Administrator, 1);
209        assert!(enforce_role_ceiling(&actor, Role::Administrator).is_ok());
210        assert!(enforce_role_ceiling(&actor, Role::Supervisor).is_ok());
211        assert!(enforce_role_ceiling(&actor, Role::Staff).is_ok());
212        assert!(enforce_role_ceiling(&actor, Role::User).is_ok());
213    }
214
215    #[test]
216    fn ceiling_supervisor_cannot_create_administrator() {
217        let actor = ident(Role::Supervisor, 1);
218        let err = enforce_role_ceiling(&actor, Role::Administrator).unwrap_err();
219        assert!(matches!(err, Error::Forbidden(_)));
220    }
221
222    #[test]
223    fn ceiling_developer_can_assign_anything_inclusive() {
224        let actor = ident(Role::Developer, 1);
225        for r in [
226            Role::User,
227            Role::Staff,
228            Role::Supervisor,
229            Role::Administrator,
230            Role::Developer,
231        ] {
232            assert!(enforce_role_ceiling(&actor, r).is_ok());
233        }
234    }
235}