Skip to main content

renox_core/auth/
permissions.rs

1//! Roles and permissions, opt in with `App::module(Permissions)`.
2//!
3//! A role (`editor`) grants permissions (`posts.publish`); users have roles.
4//! Define roles once (a seeder or a command), assign them to users, then
5//! check in handlers, routes and templates:
6//!
7//! ```
8//! use renox::prelude::*;
9//! use renox::auth::{Permissions, permissions};
10//!
11//! fn app() -> App {
12//!     App::new()
13//!         .module(Auth::new())
14//!         .module(Permissions)
15//!         .gate_before(|user, _ability| {
16//!             // Super-admins may do everything (a column the app added).
17//!             (user.get::<bool>("is_super_admin") == Some(true)).then_some(true)
18//!         })
19//! }
20//!
21//! async fn setup(db: &Db, user: &User) -> Result {
22//!     permissions::define_role(db, "editor", &["posts.create", "posts.publish"]).await?;
23//!     user.assign_role(db, "editor").await?;
24//!     Ok(())
25//! }
26//!
27//! async fn publish(user: AuthUser) -> Result<&'static str> {
28//!     if !user.allows("posts.publish") { // gate_before, a gate, then permissions
29//!         return Err(Error::Forbidden);
30//!     }
31//!     let _editor = user.has_role("editor");
32//!     Ok("published")
33//! }
34//!
35//! fn routes() -> Routes {
36//!     Routes::new()
37//!         .post("/posts/{id}/publish", publish)
38//!         .require_permission("posts.publish") // 403 otherwise, shown in route:list
39//! }
40//! # let _ = (app, routes);
41//! ```
42//!
43//! In templates, `can('posts.publish')` and `auth.roles` work the same way.
44//! A user's roles and permissions are loaded once per request.
45//!
46//! # Roles in one record
47//!
48//! A role can also be given in one record only (a store, a branch, a team)
49//! and for a period: [`User::assign_role_in`] with a [`Scope`]. A role
50//! means the same everywhere (its permissions are global); only who has it
51//! where changes. Each request picks its scope with [`set_scope`] (an app
52//! middleware, like the current team of a multi-tenant app); then every
53//! check above counts the global roles plus the roles in that scope that
54//! are within their dates:
55//!
56//! ```
57//! use renox::prelude::*;
58//! use renox::auth::permissions::{self, Scope};
59//! use renox::axum::{extract::Request, middleware::{Next, from_fn}};
60//!
61//! #[derive(Model, serde::Serialize, Default)]
62//! #[model(table = "stores")]
63//! struct Store { id: i64, name: String }
64//!
65//! async fn setup(db: &Db, user: &User, store: &Store) -> Result {
66//!     permissions::define_role(db, "manager", &["orders.refund"]).await?;
67//!     let until = renox::db::now() + renox::chrono::Duration::days(30);
68//!     user.assign_role_in(db, "manager", &Scope::of(store)).until(until).await?;
69//!     Ok(())
70//! }
71//!
72//! /// Works in the store the session says (checked against the user's stores).
73//! async fn pick_store(session: Session, req: Request, next: Next) -> Response {
74//!     if let Some(store) = session.get::<i64>("store_id") {
75//!         permissions::set_scope(Scope::of_id::<Store>(store));
76//!     }
77//!     next.run(req).await
78//! }
79//!
80//! fn app() -> App {
81//!     App::new()
82//!         .module(Auth::new())
83//!         .module(permissions::Permissions)
84//!         .layer(from_fn(pick_store))
85//! }
86//! # let _ = app;
87//! ```
88
89use std::collections::{HashMap, HashSet};
90use std::fmt;
91use std::future::{Future, IntoFuture};
92use std::pin::Pin;
93
94use super::User;
95use crate::db::{DateTime, Db, Migration, Model, Query, ToDbValue, now, sql};
96use crate::{Module, Registry, Result, Routes};
97
98const MIGRATIONS: &[Migration] = &[
99    crate::db::framework_migration!(
100        "permissions",
101        "00010101000500_create_roles_and_permissions_tables"
102    ),
103    Migration::new(
104        "00010101000510_add_scope_to_role_user",
105        include_str!("../../migrations/permissions/00010101000510_add_scope_to_role_user.up.sql"),
106        Some(include_str!(
107            "../../migrations/permissions/00010101000510_add_scope_to_role_user.down.sql"
108        )),
109    )
110    .postgres(
111        include_str!(
112            "../../migrations/permissions/00010101000510_add_scope_to_role_user.postgres.up.sql"
113        ),
114        Some(include_str!(
115            "../../migrations/permissions/00010101000510_add_scope_to_role_user.postgres.down.sql"
116        )),
117    ),
118];
119
120/// Adds roles and permissions to the app: their tables, loading the
121/// current user's grants for `AuthUser::has_role` / `has_permission`,
122/// `allows`, `Routes::require_role` / `require_permission` and `can()` in
123/// templates, and the `permissions:prune` command. Needs the `Auth`
124/// module's `users` table.
125pub struct Permissions;
126
127impl Module for Permissions {
128    fn name(&self) -> &'static str {
129        "permissions"
130    }
131
132    fn migrations(&self) -> &'static [Migration] {
133        MIGRATIONS
134    }
135
136    fn routes(&self) -> Routes {
137        Routes::new()
138    }
139
140    fn register(&self, app: &mut Registry) {
141        app.permissions = true;
142        // Ended assignments are already ignored; this keeps the table small.
143        app.command(
144            "permissions:prune",
145            "Delete role assignments that ended more than --days ago (default 30)",
146            |args, state| async move {
147                let days: u64 = args
148                    .value("--days")
149                    .unwrap_or("30")
150                    .parse()
151                    .map_err(|_| anyhow::anyhow!("--days takes a number of days"))?;
152                let age = std::time::Duration::from_secs(days * 24 * 60 * 60);
153                let pruned = prune_ended_assignments(&state.db, age).await?;
154                println!("Deleted {pruned} ended role assignments.");
155                Ok(())
156            },
157        );
158    }
159}
160
161/// Where a role is given: everywhere ([`Scope::global`]) or in one record,
162/// named by its model's table and its key (`Scope::of(&store)`,
163/// `Scope::of_id::<Store>(7)`). It is stored as two text columns of
164/// `role_user`, `scope_type` (the table) and `scope_id` (the key).
165#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, serde::Serialize)]
166pub struct Scope {
167    kind: String,
168    id: String,
169}
170
171impl Scope {
172    /// Everywhere: a role given this way counts in every scope, as
173    /// `assign_role` does.
174    pub fn global() -> Self {
175        Self::default()
176    }
177
178    /// The record `model` (its table and its key).
179    pub fn of<M: Model>(model: &M) -> Self {
180        Self::of_id::<M>(model.id())
181    }
182
183    /// The record of model `M` with the key `id`.
184    pub fn of_id<M: Model>(id: M::Key) -> Self {
185        Self::new(M::TABLE, id)
186    }
187
188    /// A scope by name, for records that aren't models: `kind` is usually
189    /// a table name, `id` the record's key.
190    pub fn new(kind: &str, id: impl fmt::Display) -> Self {
191        Self {
192            kind: kind.to_owned(),
193            id: id.to_string(),
194        }
195    }
196
197    /// Whether this is [`Scope::global`].
198    pub fn is_global(&self) -> bool {
199        self.kind.is_empty() && self.id.is_empty()
200    }
201
202    /// The record's kind: its model's table (`""` when global).
203    pub fn kind(&self) -> &str {
204        &self.kind
205    }
206
207    /// The record's key as text (`""` when global).
208    pub fn id(&self) -> &str {
209        &self.id
210    }
211
212    /// The record's key, when this scope is a record of model `M`.
213    pub fn key<M: Model>(&self) -> Option<M::Key> {
214        (self.kind == M::TABLE)
215            .then(|| self.id.parse().ok())
216            .flatten()
217    }
218}
219
220impl fmt::Display for Scope {
221    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222        if self.is_global() {
223            f.write_str("global")
224        } else {
225            write!(f, "{}:{}", self.kind, self.id)
226        }
227    }
228}
229
230/// The records in which a permission is granted, from
231/// [`User::scopes_with`] / [`scopes_with`]: every one (a global role grants
232/// it) or only these keys. Narrow a list with [`Scopes::apply`], e.g. in a
233/// `#[model(default_scope)]`.
234#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
235pub enum Scopes<K> {
236    /// A global role grants the permission: every record.
237    All,
238    /// Only the records with these keys (none when empty).
239    Only(Vec<K>),
240}
241
242impl<K> Scopes<K> {
243    /// Whether the record with key `key` is among them.
244    pub fn contains(&self, key: &K) -> bool
245    where
246        K: PartialEq,
247    {
248        match self {
249            Scopes::All => true,
250            Scopes::Only(keys) => keys.contains(key),
251        }
252    }
253
254    /// Whether no record at all is granted.
255    pub fn is_empty(&self) -> bool {
256        matches!(self, Scopes::Only(keys) if keys.is_empty())
257    }
258
259    /// Keeps the rows of `query` whose value in any of `columns` is one of
260    /// the keys (`owner_store_id IN (…) OR location_store_id IN (…)`);
261    /// [`Scopes::All`] keeps every row, and no key (or no column) none.
262    ///
263    /// ```
264    /// use renox::prelude::*;
265    /// use renox::auth::permissions;
266    ///
267    /// #[derive(Model, serde::Serialize, Default)]
268    /// #[model(table = "stores")]
269    /// struct Store { id: i64 }
270    ///
271    /// /// A transfer is seen from the store it leaves and the one it goes to.
272    /// #[derive(Model, serde::Serialize, Default)]
273    /// #[model(table = "transfers", default_scope = "my_stores")]
274    /// struct Transfer { id: i64, from_store_id: i64, to_store_id: i64 }
275    ///
276    /// fn my_stores(query: renox::db::Query<Transfer>) -> renox::db::Query<Transfer> {
277    ///     permissions::scopes_with::<Store>("transfers.view")
278    ///         .apply(query, &["from_store_id", "to_store_id"])
279    /// }
280    /// ```
281    pub fn apply<M: Model>(&self, query: Query<M>, columns: &[&str]) -> Query<M>
282    where
283        K: ToDbValue + Clone,
284    {
285        match self {
286            Scopes::All => query,
287            Scopes::Only(keys) if keys.is_empty() || columns.is_empty() => query.none(),
288            Scopes::Only(keys) => query.where_any(|mut group| {
289                for column in columns {
290                    group = group.where_in(column, keys.iter().cloned());
291                }
292                group
293            }),
294        }
295    }
296}
297
298/// One role given to a user, from [`User::assignments`]: where and when.
299#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
300#[non_exhaustive]
301pub struct Assignment {
302    /// The role's name.
303    pub role: String,
304    /// Where it counts ([`Scope::global`] for everywhere).
305    pub scope: Scope,
306    /// When it starts counting; `None` from the start.
307    pub starts_at: Option<DateTime>,
308    /// When it stops counting; `None` for good.
309    pub ends_at: Option<DateTime>,
310}
311
312impl Assignment {
313    /// Whether it counts now (`starts_at <= now < ends_at`).
314    pub fn is_active(&self) -> bool {
315        self.is_active_at(now())
316    }
317
318    /// Whether it counts at `at`.
319    pub fn is_active_at(&self, at: DateTime) -> bool {
320        self.starts_at.is_none_or(|start| start <= at) && self.ends_at.is_none_or(|end| at < end)
321    }
322}
323
324/// The scope a request works in (`set_scope`), in `renox::context`.
325#[derive(Clone)]
326struct ActiveScope(Scope);
327
328/// Makes `scope` the one this request (job, command) works in: from now on,
329/// `has_role`, `has_permission`, `allows`, the route guards and `can()` in
330/// templates count the user's global roles plus the roles given in
331/// `scope`. Call it from a middleware after checking the user may work
332/// there, like the current team in docs/authorization.md. Route guards
333/// see it when the middleware runs before them: an `App::layer`, or a
334/// route layer added after the guard.
335pub fn set_scope(scope: Scope) {
336    crate::context::set(ActiveScope(scope));
337}
338
339/// The scope set with [`set_scope`] for this request, if any.
340pub fn active_scope() -> Option<Scope> {
341    crate::context::get::<ActiveScope>().map(|active| active.0)
342}
343
344/// Back to global roles only for the rest of this request.
345pub fn clear_scope() {
346    crate::context::remove::<ActiveScope>();
347}
348
349/// The records of model `M` in which the logged-in user of this request
350/// holds `permission` (see [`User::scopes_with`]); none without a
351/// logged-in user. Made for a `#[model(default_scope)]`, which gets no
352/// user: see [`Scopes::apply`].
353pub fn scopes_with<M: Model>(permission: &str) -> Scopes<M::Key> {
354    match super::current_user_id().and_then(super::current_grants) {
355        Some(grants) => grants.scopes_with::<M>(permission),
356        None => Scopes::Only(Vec::new()),
357    }
358}
359
360/// Creates the role `name` if it's new, and makes it grant exactly
361/// `permissions` (creating the ones that don't exist yet).
362pub async fn define_role(db: &Db, name: &str, permissions: &[&str]) -> Result {
363    let mut tx = db.begin().await?;
364    let role = ensure(&mut tx, "roles", name).await?;
365    sql("DELETE FROM permission_role WHERE role_id = ?")
366        .bind(role)
367        .execute(&mut tx)
368        .await?;
369    for permission in permissions {
370        let permission = ensure(&mut tx, "permissions", permission).await?;
371        link(&mut tx, permission, role).await?;
372    }
373    tx.commit().await?;
374    Ok(())
375}
376
377/// Adds `permissions` to the existing role `name`.
378pub async fn grant(db: &Db, role: &str, permissions: &[&str]) -> Result {
379    let mut tx = db.begin().await?;
380    let role = existing_role(&mut tx, role).await?;
381    for permission in permissions {
382        let permission = ensure(&mut tx, "permissions", permission).await?;
383        link(&mut tx, permission, role).await?;
384    }
385    tx.commit().await?;
386    Ok(())
387}
388
389/// Removes `permissions` from the role `name`.
390pub async fn revoke(db: &Db, role: &str, permissions: &[&str]) -> Result {
391    let mut tx = db.begin().await?;
392    let role = existing_role(&mut tx, role).await?;
393    for permission in permissions {
394        sql(
395            "DELETE FROM permission_role WHERE role_id = ? AND permission_id = \
396             (SELECT id FROM permissions WHERE name = ?)",
397        )
398        .bind(role)
399        .bind(*permission)
400        .execute(&mut tx)
401        .await?;
402    }
403    tx.commit().await?;
404    Ok(())
405}
406
407/// Deletes the role `name`; users lose it. Returns whether it existed.
408pub async fn delete_role(db: &Db, name: &str) -> Result<bool> {
409    Ok(sql("DELETE FROM roles WHERE name = ?")
410        .bind(name)
411        .execute(db)
412        .await?
413        > 0)
414}
415
416/// Every role, by name, with the permissions it grants.
417pub async fn roles(db: &Db) -> Result<Vec<(String, Vec<String>)>> {
418    let rows: Vec<(String, Option<String>)> = sql("SELECT r.name, p.name FROM roles r \
419         LEFT JOIN permission_role pr ON pr.role_id = r.id \
420         LEFT JOIN permissions p ON p.id = pr.permission_id \
421         ORDER BY r.name, p.name")
422    .fetch_as(db)
423    .await?;
424    let mut roles: Vec<(String, Vec<String>)> = Vec::new();
425    for (role, permission) in rows {
426        if roles.last().is_none_or(|(name, _)| *name != role) {
427            roles.push((role, Vec::new()));
428        }
429        if let (Some(permission), Some((_, list))) = (permission, roles.last_mut()) {
430            list.push(permission);
431        }
432    }
433    Ok(roles)
434}
435
436/// Deletes role assignments that ended more than `age` ago (they stopped
437/// counting when they ended), and returns how many. `rnx permissions:prune`
438/// runs it.
439pub async fn prune_ended_assignments(db: &Db, age: std::time::Duration) -> Result<u64> {
440    let age = chrono::Duration::from_std(age).unwrap_or(chrono::Duration::MAX);
441    let cut_off = now()
442        .checked_sub_signed(age)
443        .unwrap_or(chrono::DateTime::<chrono::Utc>::MIN_UTC);
444    Ok(
445        sql("DELETE FROM role_user WHERE ends_at IS NOT NULL AND ends_at < ?")
446            .bind(cut_off)
447            .execute(db)
448            .await?,
449    )
450}
451
452/// Gives a user a role in a scope, from [`User::assign_role_in`]; set its
453/// dates with [`from`](AssignRole::from) / [`until`](AssignRole::until),
454/// then `.await` it. Assigning the same role in the same scope again
455/// replaces the dates.
456#[must_use = "an assignment does nothing until it is awaited"]
457pub struct AssignRole<'a> {
458    db: &'a Db,
459    user_id: i64,
460    role: &'a str,
461    scope: Scope,
462    starts_at: Option<DateTime>,
463    ends_at: Option<DateTime>,
464}
465
466impl AssignRole<'_> {
467    /// The role counts from `at` on (before, it is ignored).
468    pub fn from(mut self, at: DateTime) -> Self {
469        self.starts_at = Some(at);
470        self
471    }
472
473    /// The role stops counting at `at`.
474    pub fn until(mut self, at: DateTime) -> Self {
475        self.ends_at = Some(at);
476        self
477    }
478}
479
480impl<'a> IntoFuture for AssignRole<'a> {
481    type Output = Result;
482    type IntoFuture = Pin<Box<dyn Future<Output = Result> + Send + 'a>>;
483
484    fn into_future(self) -> Self::IntoFuture {
485        Box::pin(async move {
486            if let (Some(start), Some(end)) = (self.starts_at, self.ends_at)
487                && end <= start
488            {
489                return Err(
490                    anyhow::anyhow!("the role `{}` would end before it starts", self.role).into(),
491                );
492            }
493            assign(
494                self.db,
495                self.user_id,
496                self.role,
497                &self.scope,
498                self.starts_at,
499                self.ends_at,
500            )
501            .await
502        })
503    }
504}
505
506/// Gives `role` in `scope` with these dates, replacing the dates of the
507/// same assignment.
508async fn assign(
509    db: &Db,
510    user_id: i64,
511    role: &str,
512    scope: &Scope,
513    starts_at: Option<DateTime>,
514    ends_at: Option<DateTime>,
515) -> Result {
516    let mut tx = db.begin().await?;
517    let role = existing_role(&mut tx, role).await?;
518    sql(
519        "INSERT INTO role_user (role_id, user_id, scope_type, scope_id, starts_at, ends_at) \
520         VALUES (?, ?, ?, ?, ?, ?) \
521         ON CONFLICT (role_id, user_id, scope_type, scope_id) \
522         DO UPDATE SET starts_at = excluded.starts_at, ends_at = excluded.ends_at",
523    )
524    .bind(role)
525    .bind(user_id)
526    .bind(scope.kind())
527    .bind(scope.id())
528    .bind(starts_at)
529    .bind(ends_at)
530    .execute(&mut tx)
531    .await?;
532    tx.commit().await?;
533    Ok(())
534}
535
536impl User {
537    /// Gives the user the existing role `role` (see
538    /// `permissions::define_role`) everywhere and for good.
539    pub async fn assign_role(&self, db: &Db, role: &str) -> Result {
540        assign(db, self.id, role, &Scope::global(), None, None).await
541    }
542
543    /// Gives the user the existing role `role` in `scope` (a store, a
544    /// team), optionally from / until a date; `.await` it:
545    /// `user.assign_role_in(&db, "manager", &Scope::of(&store)).until(end).await?`.
546    /// It counts when that scope is the request's ([`set_scope`]) and for
547    /// [`User::has_permission_in`] on that scope. With
548    /// [`Scope::global`], it is a global role with dates.
549    pub fn assign_role_in<'a>(&self, db: &'a Db, role: &'a str, scope: &Scope) -> AssignRole<'a> {
550        AssignRole {
551            db,
552            user_id: self.id,
553            role,
554            scope: scope.clone(),
555            starts_at: None,
556            ends_at: None,
557        }
558    }
559
560    /// Takes the global role `role` away from the user (roles given in a
561    /// scope stay; see [`User::remove_role_in`]).
562    pub async fn remove_role(&self, db: &Db, role: &str) -> Result {
563        self.remove_role_in(db, role, &Scope::global()).await
564    }
565
566    /// Takes the role `role` in `scope` away from the user.
567    pub async fn remove_role_in(&self, db: &Db, role: &str, scope: &Scope) -> Result {
568        sql(
569            "DELETE FROM role_user WHERE user_id = ? AND scope_type = ? AND scope_id = ? \
570             AND role_id = (SELECT id FROM roles WHERE name = ?)",
571        )
572        .bind(self.id)
573        .bind(scope.kind())
574        .bind(scope.id())
575        .bind(role)
576        .execute(db)
577        .await?;
578        Ok(())
579    }
580
581    /// Makes the user's global roles exactly `roles` (each must exist);
582    /// roles given in a scope stay.
583    pub async fn sync_roles(&self, db: &Db, roles: &[&str]) -> Result {
584        self.sync_roles_in(db, roles, &Scope::global()).await
585    }
586
587    /// Makes the user's roles in `scope` exactly `roles` (each must exist),
588    /// with no dates; other scopes stay.
589    pub async fn sync_roles_in(&self, db: &Db, roles: &[&str], scope: &Scope) -> Result {
590        let mut tx = db.begin().await?;
591        let mut ids = Vec::new();
592        for role in roles {
593            ids.push(existing_role(&mut tx, role).await?);
594        }
595        sql("DELETE FROM role_user WHERE user_id = ? AND scope_type = ? AND scope_id = ?")
596            .bind(self.id)
597            .bind(scope.kind())
598            .bind(scope.id())
599            .execute(&mut tx)
600            .await?;
601        for role in ids {
602            sql(
603                "INSERT INTO role_user (role_id, user_id, scope_type, scope_id) \
604                 VALUES (?, ?, ?, ?)",
605            )
606            .bind(role)
607            .bind(self.id)
608            .bind(scope.kind())
609            .bind(scope.id())
610            .execute(&mut tx)
611            .await?;
612        }
613        tx.commit().await?;
614        Ok(())
615    }
616
617    /// The names of the user's roles in effect now, sorted: the global ones
618    /// plus those in the request's scope ([`set_scope`]), within their
619    /// dates.
620    pub async fn roles(&self, db: &Db) -> Result<Vec<String>> {
621        Ok(grants(db, self.id).await?.roles())
622    }
623
624    /// The permissions the user's roles in effect now grant, sorted (the
625    /// same roles as [`User::roles`]).
626    pub async fn permissions(&self, db: &Db) -> Result<Vec<String>> {
627        let grants = grants(db, self.id).await?;
628        let scope = active_scope();
629        let mut list: Vec<String> = grants
630            .permissions_in(scope.as_ref())
631            .into_iter()
632            .map(str::to_owned)
633            .collect();
634        list.sort();
635        Ok(list)
636    }
637
638    /// Every role the user was given, with where and when, ordered by role
639    /// and scope; ended ones too until `permissions:prune` deletes them.
640    /// For account and admin pages.
641    pub async fn assignments(&self, db: &Db) -> Result<Vec<Assignment>> {
642        Ok(load_assignments(db, self.id, false)
643            .await?
644            .into_iter()
645            .map(|granted| Assignment {
646                role: granted.role,
647                scope: granted.scope,
648                starts_at: granted.starts_at,
649                ends_at: granted.ends_at,
650            })
651            .collect())
652    }
653
654    /// Whether this user has `role` globally or in `scope`, within its
655    /// dates, from the roles loaded for the current request (like
656    /// [`User::has_role`]); `scope` is the record's, not the request's.
657    pub fn has_role_in(&self, role: &str, scope: &Scope) -> bool {
658        super::current_grants(self.id).is_some_and(|g| g.has_role_in(role, Some(scope)))
659    }
660
661    /// Whether a global role of this user, or one given in `scope`, grants
662    /// `permission` now: for policies, which check the record's scope
663    /// (`Scope::of_id::<Store>(order.store_id)`) rather than the request's.
664    /// Answered from the roles loaded for the current request (like
665    /// [`User::has_permission`]): `false` for another user and outside a
666    /// request.
667    pub fn has_permission_in(&self, permission: &str, scope: &Scope) -> bool {
668        super::current_grants(self.id).is_some_and(|g| g.has_permission_in(permission, Some(scope)))
669    }
670
671    /// The records of model `M` in which this user holds `permission` now:
672    /// [`Scopes::All`] when a global role grants it, else the keys of the
673    /// records whose roles do. For filtering lists (`Scopes::apply`).
674    /// Answered from the roles loaded for the current request; nothing for
675    /// another user and outside a request.
676    pub fn scopes_with<M: Model>(&self, permission: &str) -> Scopes<M::Key> {
677        match super::current_grants(self.id) {
678            Some(grants) => grants.scopes_with::<M>(permission),
679            None => Scopes::Only(Vec::new()),
680        }
681    }
682}
683
684/// The users who have the global role `role` now, ordered by id (e.g. to
685/// notify every admin).
686pub async fn users_with_role(db: &Db, role: &str) -> Result<Vec<User>> {
687    users_with_role_in(db, role, &Scope::global()).await
688}
689
690/// The users who have `role` in `scope` now (given there, or globally),
691/// ordered by id, e.g. to notify the managers of one store.
692pub async fn users_with_role_in(db: &Db, role: &str, scope: &Scope) -> Result<Vec<User>> {
693    let at = now();
694    let ids: Vec<i64> = sql("SELECT DISTINCT ru.user_id FROM role_user ru \
695         JOIN roles r ON r.id = ru.role_id \
696         WHERE r.name = ? \
697         AND ((ru.scope_type = '' AND ru.scope_id = '') OR (ru.scope_type = ? AND ru.scope_id = ?)) \
698         AND (ru.starts_at IS NULL OR ru.starts_at <= ?) \
699         AND (ru.ends_at IS NULL OR ru.ends_at > ?) \
700         ORDER BY ru.user_id")
701    .bind(role)
702    .bind(scope.kind())
703    .bind(scope.id())
704    .bind(at)
705    .bind(at)
706    .scalars(db)
707    .await?;
708    let mut users = User::find_many(db, ids).await?;
709    users.sort_by_key(|u| u.id);
710    Ok(users)
711}
712
713/// One role given to the current user, as the auth middleware loads it.
714#[derive(Debug, Clone)]
715pub(crate) struct Granted {
716    role: String,
717    scope: Scope,
718    starts_at: Option<DateTime>,
719    ends_at: Option<DateTime>,
720}
721
722impl Granted {
723    fn active_at(&self, at: DateTime) -> bool {
724        self.starts_at.is_none_or(|start| start <= at) && self.ends_at.is_none_or(|end| at < end)
725    }
726}
727
728/// A user's role assignments (with their scopes and dates) and what each
729/// role grants, loaded once per request; every check filters them by scope
730/// and the current time, since the app's scope middleware runs after the
731/// auth middleware.
732#[derive(Default, Debug)]
733pub(crate) struct Grants {
734    assignments: Vec<Granted>,
735    permissions: HashMap<String, HashSet<String>>,
736}
737
738impl Grants {
739    /// The assignments in effect now in `scope` (plus the global ones).
740    fn in_effect<'a>(&'a self, scope: Option<&'a Scope>) -> impl Iterator<Item = &'a Granted> {
741        let at = now();
742        self.assignments.iter().filter(move |granted| {
743            (granted.scope.is_global() || scope.is_some_and(|scope| granted.scope == *scope))
744                && granted.active_at(at)
745        })
746    }
747
748    fn grants(&self, granted: &Granted, permission: &str) -> bool {
749        self.permissions
750            .get(&granted.role)
751            .is_some_and(|list| list.contains(permission))
752    }
753
754    /// The role names in effect in the request's scope, sorted, once each.
755    pub(crate) fn roles(&self) -> Vec<String> {
756        let scope = active_scope();
757        let mut roles: Vec<String> = self
758            .in_effect(scope.as_ref())
759            .map(|granted| granted.role.clone())
760            .collect();
761        roles.sort();
762        roles.dedup();
763        roles
764    }
765
766    /// Whether `role` is in effect in the request's scope.
767    pub(crate) fn has_role(&self, role: &str) -> bool {
768        self.has_role_in(role, active_scope().as_ref())
769    }
770
771    /// Whether a role in effect in the request's scope grants `permission`.
772    pub(crate) fn has_permission(&self, permission: &str) -> bool {
773        self.has_permission_in(permission, active_scope().as_ref())
774    }
775
776    pub(crate) fn has_role_in(&self, role: &str, scope: Option<&Scope>) -> bool {
777        self.in_effect(scope).any(|granted| granted.role == role)
778    }
779
780    pub(crate) fn has_permission_in(&self, permission: &str, scope: Option<&Scope>) -> bool {
781        self.in_effect(scope)
782            .any(|granted| self.grants(granted, permission))
783    }
784
785    fn permissions_in(&self, scope: Option<&Scope>) -> HashSet<&str> {
786        self.in_effect(scope)
787            .filter_map(|granted| self.permissions.get(&granted.role))
788            .flatten()
789            .map(String::as_str)
790            .collect()
791    }
792
793    pub(crate) fn scopes_with<M: Model>(&self, permission: &str) -> Scopes<M::Key> {
794        let at = now();
795        let mut keys: Vec<M::Key> = Vec::new();
796        for granted in &self.assignments {
797            if !granted.active_at(at) || !self.grants(granted, permission) {
798                continue;
799            }
800            if granted.scope.is_global() {
801                return Scopes::All;
802            }
803            if let Some(key) = granted.scope.key::<M>()
804                && !keys.contains(&key)
805            {
806                keys.push(key);
807            }
808        }
809        keys.sort();
810        Scopes::Only(keys)
811    }
812}
813
814/// A user's role assignments; with `current`, only those not ended yet.
815async fn load_assignments(db: &Db, user_id: i64, current: bool) -> Result<Vec<Granted>> {
816    let mut query = String::from(
817        "SELECT r.name, ru.scope_type, ru.scope_id, ru.starts_at, ru.ends_at \
818         FROM role_user ru JOIN roles r ON r.id = ru.role_id WHERE ru.user_id = ?",
819    );
820    if current {
821        query.push_str(" AND (ru.ends_at IS NULL OR ru.ends_at > ?)");
822    }
823    query.push_str(" ORDER BY r.name, ru.scope_type, ru.scope_id");
824    let mut statement = sql(query).bind(user_id);
825    if current {
826        statement = statement.bind(now());
827    }
828    type Row = (String, String, String, Option<DateTime>, Option<DateTime>);
829    let rows: Vec<Row> = statement.fetch_as(db).await?;
830    Ok(rows
831        .into_iter()
832        .map(|(role, kind, id, starts_at, ends_at)| Granted {
833            role,
834            scope: Scope { kind, id },
835            starts_at,
836            ends_at,
837        })
838        .collect())
839}
840
841/// A user's role assignments and their roles' permissions, for the auth
842/// middleware.
843pub(crate) async fn grants(db: &Db, user_id: i64) -> Result<Grants> {
844    let assignments = load_assignments(db, user_id, true).await?;
845    let mut permissions: HashMap<String, HashSet<String>> = HashMap::new();
846    if !assignments.is_empty() {
847        let rows: Vec<(String, String)> = sql("SELECT DISTINCT r.name, p.name FROM roles r \
848             JOIN permission_role pr ON pr.role_id = r.id \
849             JOIN permissions p ON p.id = pr.permission_id \
850             WHERE r.id IN (SELECT role_id FROM role_user WHERE user_id = ?)")
851        .bind(user_id)
852        .fetch_as(db)
853        .await?;
854        for (role, permission) in rows {
855            permissions.entry(role).or_default().insert(permission);
856        }
857    }
858    Ok(Grants {
859        assignments,
860        permissions,
861    })
862}
863
864/// The id of the row named `name` in `table`, created if missing.
865async fn ensure(tx: &mut crate::db::Transaction, table: &str, name: &str) -> Result<i64> {
866    let at = now();
867    sql(format!(
868        "INSERT INTO {table} (name, created_at, updated_at) SELECT ?, ?, ? \
869         WHERE NOT EXISTS (SELECT 1 FROM {table} WHERE name = ?)"
870    ))
871    .bind(name)
872    .bind(at)
873    .bind(at)
874    .bind(name)
875    .execute(&mut *tx)
876    .await?;
877    Ok(sql(format!("SELECT id FROM {table} WHERE name = ?"))
878        .bind(name)
879        .scalar(&mut *tx)
880        .await?)
881}
882
883async fn existing_role(tx: &mut crate::db::Transaction, name: &str) -> Result<i64> {
884    let id: Option<i64> = sql("SELECT id FROM roles WHERE name = ?")
885        .bind(name)
886        .scalars(&mut *tx)
887        .await?
888        .into_iter()
889        .next();
890    id.ok_or_else(|| {
891        anyhow::anyhow!("there is no role `{name}`; create it with permissions::define_role").into()
892    })
893}
894
895async fn link(tx: &mut crate::db::Transaction, permission: i64, role: i64) -> Result {
896    sql(
897        "INSERT INTO permission_role (permission_id, role_id) SELECT ?, ? \
898         WHERE NOT EXISTS (SELECT 1 FROM permission_role WHERE permission_id = ? AND role_id = ?)",
899    )
900    .bind(permission)
901    .bind(role)
902    .bind(permission)
903    .bind(role)
904    .execute(&mut *tx)
905    .await?;
906    Ok(())
907}