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    users_granted(
694        db,
695        "JOIN roles r ON r.id = ru.role_id WHERE r.name = ?",
696        role,
697        scope,
698    )
699    .await
700}
701
702/// The users a global role lets do `permission` now, ordered by id (e.g. to
703/// notify everyone who may approve refunds).
704pub async fn users_with_permission(db: &Db, permission: &str) -> Result<Vec<User>> {
705    users_with_permission_in(db, permission, &Scope::global()).await
706}
707
708/// The users who may do `permission` in `scope` now: through a role given
709/// there or a global one, within its dates, ordered by id. The question a
710/// job asks to notify "everyone who may take overdue bikes back in this
711/// store", without naming roles.
712///
713/// ```
714/// # use renox::prelude::*;
715/// # use renox::auth::permissions::{self, Scope};
716/// # #[derive(Model, serde::Serialize)] struct Store { id: i64 }
717/// # async fn demo(db: &renox::db::Db, store: &Store) -> Result {
718/// let staff = permissions::users_with_permission_in(db, "rentals.return", &Scope::of(store)).await?;
719/// # Ok(()) }
720/// ```
721pub async fn users_with_permission_in(
722    db: &Db,
723    permission: &str,
724    scope: &Scope,
725) -> Result<Vec<User>> {
726    users_granted(
727        db,
728        "JOIN permission_role pr ON pr.role_id = ru.role_id \
729         JOIN permissions p ON p.id = pr.permission_id WHERE p.name = ?",
730        permission,
731        scope,
732    )
733    .await
734}
735
736/// The users whose assignment, joined by `join` (which ends in a `WHERE`
737/// on `name`), is in effect in `scope` (or global) now.
738async fn users_granted(db: &Db, join: &str, name: &str, scope: &Scope) -> Result<Vec<User>> {
739    let at = now();
740    let ids: Vec<i64> = sql(format!(
741        "SELECT DISTINCT ru.user_id FROM role_user ru {join} \
742         AND ((ru.scope_type = '' AND ru.scope_id = '') OR (ru.scope_type = ? AND ru.scope_id = ?)) \
743         AND (ru.starts_at IS NULL OR ru.starts_at <= ?) \
744         AND (ru.ends_at IS NULL OR ru.ends_at > ?) \
745         ORDER BY ru.user_id"
746    ))
747    .bind(name)
748    .bind(scope.kind())
749    .bind(scope.id())
750    .bind(at)
751    .bind(at)
752    .scalars(db)
753    .await?;
754    let mut users = User::find_many(db, ids).await?;
755    users.sort_by_key(|u| u.id);
756    Ok(users)
757}
758
759/// One role given to the current user, as the auth middleware loads it.
760#[derive(Debug, Clone)]
761pub(crate) struct Granted {
762    role: String,
763    scope: Scope,
764    starts_at: Option<DateTime>,
765    ends_at: Option<DateTime>,
766}
767
768impl Granted {
769    fn active_at(&self, at: DateTime) -> bool {
770        self.starts_at.is_none_or(|start| start <= at) && self.ends_at.is_none_or(|end| at < end)
771    }
772}
773
774/// A user's role assignments (with their scopes and dates) and what each
775/// role grants, loaded once per request; every check filters them by scope
776/// and the current time, since the app's scope middleware runs after the
777/// auth middleware.
778#[derive(Default, Debug)]
779pub(crate) struct Grants {
780    assignments: Vec<Granted>,
781    permissions: HashMap<String, HashSet<String>>,
782}
783
784impl Grants {
785    /// The assignments in effect now in `scope` (plus the global ones).
786    fn in_effect<'a>(&'a self, scope: Option<&'a Scope>) -> impl Iterator<Item = &'a Granted> {
787        let at = now();
788        self.assignments.iter().filter(move |granted| {
789            (granted.scope.is_global() || scope.is_some_and(|scope| granted.scope == *scope))
790                && granted.active_at(at)
791        })
792    }
793
794    fn grants(&self, granted: &Granted, permission: &str) -> bool {
795        self.permissions
796            .get(&granted.role)
797            .is_some_and(|list| list.contains(permission))
798    }
799
800    /// The role names in effect in the request's scope, sorted, once each.
801    pub(crate) fn roles(&self) -> Vec<String> {
802        let scope = active_scope();
803        let mut roles: Vec<String> = self
804            .in_effect(scope.as_ref())
805            .map(|granted| granted.role.clone())
806            .collect();
807        roles.sort();
808        roles.dedup();
809        roles
810    }
811
812    /// Whether `role` is in effect in the request's scope.
813    pub(crate) fn has_role(&self, role: &str) -> bool {
814        self.has_role_in(role, active_scope().as_ref())
815    }
816
817    /// Whether a role in effect in the request's scope grants `permission`.
818    pub(crate) fn has_permission(&self, permission: &str) -> bool {
819        self.has_permission_in(permission, active_scope().as_ref())
820    }
821
822    pub(crate) fn has_role_in(&self, role: &str, scope: Option<&Scope>) -> bool {
823        self.in_effect(scope).any(|granted| granted.role == role)
824    }
825
826    pub(crate) fn has_permission_in(&self, permission: &str, scope: Option<&Scope>) -> bool {
827        self.in_effect(scope)
828            .any(|granted| self.grants(granted, permission))
829    }
830
831    fn permissions_in(&self, scope: Option<&Scope>) -> HashSet<&str> {
832        self.in_effect(scope)
833            .filter_map(|granted| self.permissions.get(&granted.role))
834            .flatten()
835            .map(String::as_str)
836            .collect()
837    }
838
839    pub(crate) fn scopes_with<M: Model>(&self, permission: &str) -> Scopes<M::Key> {
840        let at = now();
841        let mut keys: Vec<M::Key> = Vec::new();
842        for granted in &self.assignments {
843            if !granted.active_at(at) || !self.grants(granted, permission) {
844                continue;
845            }
846            if granted.scope.is_global() {
847                return Scopes::All;
848            }
849            if let Some(key) = granted.scope.key::<M>()
850                && !keys.contains(&key)
851            {
852                keys.push(key);
853            }
854        }
855        keys.sort();
856        Scopes::Only(keys)
857    }
858}
859
860/// A user's role assignments; with `current`, only those not ended yet.
861async fn load_assignments(db: &Db, user_id: i64, current: bool) -> Result<Vec<Granted>> {
862    let mut query = String::from(
863        "SELECT r.name, ru.scope_type, ru.scope_id, ru.starts_at, ru.ends_at \
864         FROM role_user ru JOIN roles r ON r.id = ru.role_id WHERE ru.user_id = ?",
865    );
866    if current {
867        query.push_str(" AND (ru.ends_at IS NULL OR ru.ends_at > ?)");
868    }
869    query.push_str(" ORDER BY r.name, ru.scope_type, ru.scope_id");
870    let mut statement = sql(query).bind(user_id);
871    if current {
872        statement = statement.bind(now());
873    }
874    type Row = (String, String, String, Option<DateTime>, Option<DateTime>);
875    let rows: Vec<Row> = statement.fetch_as(db).await?;
876    Ok(rows
877        .into_iter()
878        .map(|(role, kind, id, starts_at, ends_at)| Granted {
879            role,
880            scope: Scope { kind, id },
881            starts_at,
882            ends_at,
883        })
884        .collect())
885}
886
887/// A user's role assignments and their roles' permissions, for the auth
888/// middleware.
889pub(crate) async fn grants(db: &Db, user_id: i64) -> Result<Grants> {
890    let assignments = load_assignments(db, user_id, true).await?;
891    let mut permissions: HashMap<String, HashSet<String>> = HashMap::new();
892    if !assignments.is_empty() {
893        let rows: Vec<(String, String)> = sql("SELECT DISTINCT r.name, p.name FROM roles r \
894             JOIN permission_role pr ON pr.role_id = r.id \
895             JOIN permissions p ON p.id = pr.permission_id \
896             WHERE r.id IN (SELECT role_id FROM role_user WHERE user_id = ?)")
897        .bind(user_id)
898        .fetch_as(db)
899        .await?;
900        for (role, permission) in rows {
901            permissions.entry(role).or_default().insert(permission);
902        }
903    }
904    Ok(Grants {
905        assignments,
906        permissions,
907    })
908}
909
910/// The id of the row named `name` in `table`, created if missing.
911async fn ensure(tx: &mut crate::db::Transaction, table: &str, name: &str) -> Result<i64> {
912    let at = now();
913    sql(format!(
914        "INSERT INTO {table} (name, created_at, updated_at) SELECT ?, ?, ? \
915         WHERE NOT EXISTS (SELECT 1 FROM {table} WHERE name = ?)"
916    ))
917    .bind(name)
918    .bind(at)
919    .bind(at)
920    .bind(name)
921    .execute(&mut *tx)
922    .await?;
923    Ok(sql(format!("SELECT id FROM {table} WHERE name = ?"))
924        .bind(name)
925        .scalar(&mut *tx)
926        .await?)
927}
928
929async fn existing_role(tx: &mut crate::db::Transaction, name: &str) -> Result<i64> {
930    let id: Option<i64> = sql("SELECT id FROM roles WHERE name = ?")
931        .bind(name)
932        .scalars(&mut *tx)
933        .await?
934        .into_iter()
935        .next();
936    id.ok_or_else(|| {
937        anyhow::anyhow!("there is no role `{name}`; create it with permissions::define_role").into()
938    })
939}
940
941async fn link(tx: &mut crate::db::Transaction, permission: i64, role: i64) -> Result {
942    sql(
943        "INSERT INTO permission_role (permission_id, role_id) SELECT ?, ? \
944         WHERE NOT EXISTS (SELECT 1 FROM permission_role WHERE permission_id = ? AND role_id = ?)",
945    )
946    .bind(permission)
947    .bind(role)
948    .bind(permission)
949    .bind(role)
950    .execute(&mut *tx)
951    .await?;
952    Ok(())
953}