1use 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
120pub 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 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#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, serde::Serialize)]
166pub struct Scope {
167 kind: String,
168 id: String,
169}
170
171impl Scope {
172 pub fn global() -> Self {
175 Self::default()
176 }
177
178 pub fn of<M: Model>(model: &M) -> Self {
180 Self::of_id::<M>(model.id())
181 }
182
183 pub fn of_id<M: Model>(id: M::Key) -> Self {
185 Self::new(M::TABLE, id)
186 }
187
188 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 pub fn is_global(&self) -> bool {
199 self.kind.is_empty() && self.id.is_empty()
200 }
201
202 pub fn kind(&self) -> &str {
204 &self.kind
205 }
206
207 pub fn id(&self) -> &str {
209 &self.id
210 }
211
212 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#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
235pub enum Scopes<K> {
236 All,
238 Only(Vec<K>),
240}
241
242impl<K> Scopes<K> {
243 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 pub fn is_empty(&self) -> bool {
256 matches!(self, Scopes::Only(keys) if keys.is_empty())
257 }
258
259 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#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
300#[non_exhaustive]
301pub struct Assignment {
302 pub role: String,
304 pub scope: Scope,
306 pub starts_at: Option<DateTime>,
308 pub ends_at: Option<DateTime>,
310}
311
312impl Assignment {
313 pub fn is_active(&self) -> bool {
315 self.is_active_at(now())
316 }
317
318 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#[derive(Clone)]
326struct ActiveScope(Scope);
327
328pub fn set_scope(scope: Scope) {
336 crate::context::set(ActiveScope(scope));
337}
338
339pub fn active_scope() -> Option<Scope> {
341 crate::context::get::<ActiveScope>().map(|active| active.0)
342}
343
344pub fn clear_scope() {
346 crate::context::remove::<ActiveScope>();
347}
348
349pub 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
360pub 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
377pub 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
389pub 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
407pub 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
416pub 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
436pub 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#[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 pub fn from(mut self, at: DateTime) -> Self {
469 self.starts_at = Some(at);
470 self
471 }
472
473 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
506async 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 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 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 pub async fn remove_role(&self, db: &Db, role: &str) -> Result {
563 self.remove_role_in(db, role, &Scope::global()).await
564 }
565
566 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 pub async fn sync_roles(&self, db: &Db, roles: &[&str]) -> Result {
584 self.sync_roles_in(db, roles, &Scope::global()).await
585 }
586
587 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 pub async fn roles(&self, db: &Db) -> Result<Vec<String>> {
621 Ok(grants(db, self.id).await?.roles())
622 }
623
624 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 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 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 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 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
684pub async fn users_with_role(db: &Db, role: &str) -> Result<Vec<User>> {
687 users_with_role_in(db, role, &Scope::global()).await
688}
689
690pub 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#[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#[derive(Default, Debug)]
733pub(crate) struct Grants {
734 assignments: Vec<Granted>,
735 permissions: HashMap<String, HashSet<String>>,
736}
737
738impl Grants {
739 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 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 pub(crate) fn has_role(&self, role: &str) -> bool {
768 self.has_role_in(role, active_scope().as_ref())
769 }
770
771 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
814async 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
841pub(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
864async 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}