1use std::collections::HashSet;
23use std::sync::Arc;
24use std::time::{Duration, Instant};
25
26use dashmap::DashMap;
27use once_cell::sync::Lazy;
28use sqlx::Row as SqlxRow;
29
30use crate::error::{Error, Result};
31use crate::orm::Db;
32
33use super::users::Identity;
34
35#[cfg(test)]
36use super::role::Role;
37
38pub struct Superuser;
41
42#[derive(Debug, Clone)]
44pub struct Permission {
45 pub id: i64,
46 pub name: String,
47 pub description: String,
48}
49
50#[derive(Debug, thiserror::Error)]
52pub enum PermissionError {
53 #[error("permission `{0}` not found")]
54 Missing(String),
55 #[error("user not found")]
56 NoSuchUser,
57 #[error("group not found")]
58 NoSuchGroup,
59}
60
61pub async fn init_permission_tables(db: &Db) -> Result<()> {
65 sqlx::query(
66 "CREATE TABLE IF NOT EXISTS rustio_permissions (
67 id BIGSERIAL PRIMARY KEY,
68 name TEXT NOT NULL UNIQUE,
69 description TEXT NOT NULL DEFAULT '',
70 created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
71 )",
72 )
73 .execute(db.pool())
74 .await?;
75
76 sqlx::query(
77 "CREATE TABLE IF NOT EXISTS rustio_groups (
78 id BIGSERIAL PRIMARY KEY,
79 name TEXT NOT NULL UNIQUE,
80 description TEXT NOT NULL DEFAULT '',
81 created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
82 )",
83 )
84 .execute(db.pool())
85 .await?;
86
87 sqlx::query(
88 "CREATE TABLE IF NOT EXISTS rustio_group_permissions (
89 group_id BIGINT NOT NULL REFERENCES rustio_groups(id) ON DELETE CASCADE,
90 permission_id BIGINT NOT NULL REFERENCES rustio_permissions(id) ON DELETE CASCADE,
91 PRIMARY KEY (group_id, permission_id)
92 )",
93 )
94 .execute(db.pool())
95 .await?;
96
97 sqlx::query(
98 "CREATE TABLE IF NOT EXISTS rustio_user_groups (
99 user_id BIGINT NOT NULL REFERENCES rustio_users(id) ON DELETE CASCADE,
100 group_id BIGINT NOT NULL REFERENCES rustio_groups(id) ON DELETE CASCADE,
101 PRIMARY KEY (user_id, group_id)
102 )",
103 )
104 .execute(db.pool())
105 .await?;
106
107 sqlx::query(
108 "CREATE TABLE IF NOT EXISTS rustio_user_permissions (
109 user_id BIGINT NOT NULL REFERENCES rustio_users(id) ON DELETE CASCADE,
110 permission_id BIGINT NOT NULL REFERENCES rustio_permissions(id) ON DELETE CASCADE,
111 PRIMARY KEY (user_id, permission_id)
112 )",
113 )
114 .execute(db.pool())
115 .await?;
116
117 Ok(())
118}
119
120struct CacheEntry {
123 perms: Arc<HashSet<String>>,
124 expires: Instant,
125}
126
127static PERM_CACHE: Lazy<DashMap<i64, CacheEntry>> = Lazy::new(DashMap::new);
128
129const PERM_CACHE_TTL: Duration = Duration::from_secs(60);
130
131pub(crate) fn invalidate_user_cache(user_id: i64) {
132 PERM_CACHE.remove(&user_id);
133}
134
135fn invalidate_group_cache(db: &Db, group_id: i64) {
136 let db = db.clone();
139 tokio::spawn(async move {
140 let rows = sqlx::query("SELECT user_id FROM rustio_user_groups WHERE group_id = $1")
141 .bind(group_id)
142 .fetch_all(db.pool())
143 .await
144 .unwrap_or_default();
145 for r in rows {
146 if let Ok(uid) = r.try_get::<i64, _>("user_id") {
147 invalidate_user_cache(uid);
148 }
149 }
150 });
151}
152
153pub async fn permissions_for_user(db: &Db, user_id: i64) -> Result<Arc<HashSet<String>>> {
159 if let Some(e) = PERM_CACHE.get(&user_id) {
160 if e.expires > Instant::now() {
161 return Ok(e.perms.clone());
162 }
163 }
164
165 let rows = sqlx::query(
166 "SELECT DISTINCT p.name
167 FROM rustio_permissions p
168 LEFT JOIN rustio_user_permissions up ON up.permission_id = p.id
169 LEFT JOIN rustio_group_permissions gp ON gp.permission_id = p.id
170 LEFT JOIN rustio_user_groups ug ON ug.group_id = gp.group_id
171 WHERE up.user_id = $1 OR ug.user_id = $1",
172 )
173 .bind(user_id)
174 .fetch_all(db.pool())
175 .await?;
176
177 let mut set = HashSet::with_capacity(rows.len());
178 for r in rows {
179 if let Ok(name) = r.try_get::<String, _>("name") {
180 set.insert(name);
181 }
182 }
183 let arc = Arc::new(set);
184 PERM_CACHE.insert(
185 user_id,
186 CacheEntry {
187 perms: arc.clone(),
188 expires: Instant::now() + PERM_CACHE_TTL,
189 },
190 );
191 Ok(arc)
192}
193
194pub async fn check_permission(db: &Db, identity: &Identity, permission: &str) -> Result<bool> {
203 if !identity.is_active {
204 return Ok(false);
205 }
206 if identity.role.bypasses_group_checks() {
207 return Ok(true);
208 }
209 let perms = permissions_for_user(db, identity.user_id).await?;
210 Ok(perms.contains(permission))
211}
212
213async fn permission_id(db: &Db, name: &str) -> Result<i64> {
216 if let Some(row) = sqlx::query("SELECT id FROM rustio_permissions WHERE name = $1")
217 .bind(name)
218 .fetch_optional(db.pool())
219 .await?
220 {
221 return row
222 .try_get("id")
223 .map_err(|e| Error::Internal(format!("{e}")));
224 }
225 let row = sqlx::query(
226 "INSERT INTO rustio_permissions (name, description)
227 VALUES ($1, $2)
228 ON CONFLICT (name) DO UPDATE SET description = rustio_permissions.description
229 RETURNING id",
230 )
231 .bind(name)
232 .bind("")
233 .fetch_one(db.pool())
234 .await?;
235 row.try_get("id")
236 .map_err(|e| Error::Internal(format!("{e}")))
237}
238
239pub async fn grant_to_user(db: &Db, user_id: i64, permission: &str) -> Result<()> {
241 let pid = permission_id(db, permission).await?;
242 sqlx::query(
243 "INSERT INTO rustio_user_permissions (user_id, permission_id)
244 VALUES ($1, $2)
245 ON CONFLICT DO NOTHING",
246 )
247 .bind(user_id)
248 .bind(pid)
249 .execute(db.pool())
250 .await?;
251 invalidate_user_cache(user_id);
252 Ok(())
253}
254
255pub async fn grant_to_group(db: &Db, group_id: i64, permission: &str) -> Result<()> {
257 let pid = permission_id(db, permission).await?;
258 sqlx::query(
259 "INSERT INTO rustio_group_permissions (group_id, permission_id)
260 VALUES ($1, $2)
261 ON CONFLICT DO NOTHING",
262 )
263 .bind(group_id)
264 .bind(pid)
265 .execute(db.pool())
266 .await?;
267 invalidate_group_cache(db, group_id);
268 Ok(())
269}
270
271pub async fn create_group(db: &Db, name: &str, description: &str) -> Result<i64> {
277 let row = sqlx::query(
278 "INSERT INTO rustio_groups (name, description)
279 VALUES ($1, $2)
280 ON CONFLICT (name) DO UPDATE SET description = rustio_groups.description
281 RETURNING id",
282 )
283 .bind(name)
284 .bind(description)
285 .fetch_one(db.pool())
286 .await?;
287 row.try_get("id")
288 .map_err(|e| Error::Internal(format!("{e}")))
289}
290
291pub async fn add_user_to_group(db: &Db, user_id: i64, group_id: i64) -> Result<()> {
293 sqlx::query(
294 "INSERT INTO rustio_user_groups (user_id, group_id)
295 VALUES ($1, $2)
296 ON CONFLICT DO NOTHING",
297 )
298 .bind(user_id)
299 .bind(group_id)
300 .execute(db.pool())
301 .await?;
302 invalidate_user_cache(user_id);
303 Ok(())
304}
305
306pub async fn remove_user_from_group(db: &Db, user_id: i64, group_id: i64) -> Result<()> {
308 sqlx::query("DELETE FROM rustio_user_groups WHERE user_id = $1 AND group_id = $2")
309 .bind(user_id)
310 .bind(group_id)
311 .execute(db.pool())
312 .await?;
313 invalidate_user_cache(user_id);
314 Ok(())
315}
316
317pub async fn register_model_permissions(db: &Db, app: &str, singular: &str) -> Result<()> {
322 let actions = ["add", "change", "delete", "view"];
323 for action in actions {
324 let name = format!("{app}.{action}_{singular}");
325 let _ = permission_id(db, &name).await?;
326 }
327 Ok(())
328}
329
330pub const DEFAULT_GROUP_NAMES: [&str; 3] = ["administrator", "editor", "viewer"];
345
346pub async fn seed_default_groups(db: &Db) -> Result<()> {
359 let foreign_count: i64 = sqlx::query_scalar(
360 "SELECT COUNT(*) FROM rustio_groups
361 WHERE name NOT IN ('administrator', 'editor', 'viewer')",
362 )
363 .fetch_one(db.pool())
364 .await
365 .map_err(|e| Error::Internal(format!("seed_default_groups guard: {e}")))?;
366 if foreign_count > 0 {
367 return Ok(());
369 }
370 create_group(db, "administrator", "Full system access.").await?;
371 create_group(
372 db,
373 "editor",
374 "Create / read / update on content models only. No user, group, settings, or framework-admin actions.",
375 )
376 .await?;
377 create_group(db, "viewer", "Read-only access to content models.").await?;
378 Ok(())
379}
380
381pub async fn grant_model_to_default_groups(db: &Db, app: &str, singular: &str) -> Result<()> {
404 let admin_id = group_id_by_name(db, "administrator").await?;
408 let editor_id = group_id_by_name(db, "editor").await?;
409 let viewer_id = group_id_by_name(db, "viewer").await?;
410
411 let add = format!("{app}.add_{singular}");
412 let change = format!("{app}.change_{singular}");
413 let delete = format!("{app}.delete_{singular}");
414 let view = format!("{app}.view_{singular}");
415
416 if let Some(id) = admin_id {
417 grant_to_group(db, id, &add).await?;
418 grant_to_group(db, id, &change).await?;
419 grant_to_group(db, id, &delete).await?;
420 grant_to_group(db, id, &view).await?;
421 }
422 if let Some(id) = editor_id {
423 grant_to_group(db, id, &add).await?;
424 grant_to_group(db, id, &change).await?;
425 grant_to_group(db, id, &view).await?;
426 }
428 if let Some(id) = viewer_id {
429 grant_to_group(db, id, &view).await?;
430 }
431 Ok(())
432}
433
434async fn group_id_by_name(db: &Db, name: &str) -> Result<Option<i64>> {
438 let id: Option<i64> = sqlx::query_scalar("SELECT id FROM rustio_groups WHERE name = $1")
439 .bind(name)
440 .fetch_optional(db.pool())
441 .await
442 .map_err(|e| Error::Internal(format!("group_id_by_name({name}): {e}")))?;
443 Ok(id)
444}
445
446#[cfg(test)]
447mod tests {
448 use super::*;
449
450 #[test]
451 fn administrator_and_developer_bypass_group_checks() {
452 for &(role, expected) in &[
454 (Role::User, false),
455 (Role::Staff, false),
456 (Role::Supervisor, false),
457 (Role::Administrator, true),
458 (Role::Developer, true),
459 ] {
460 let id = Identity {
461 user_id: 1,
462 email: "a@b.com".into(),
463 role,
464 is_active: true,
465 is_demo: false,
466 demo_label: None,
467 must_change_password: false,
468 mfa_enabled: false,
469 trust_level: crate::auth::SessionTrust::Authenticated,
470 };
471 assert_eq!(
472 id.role.bypasses_group_checks(),
473 expected,
474 "{role:?} should be {expected}"
475 );
476 }
477 }
478
479 #[test]
480 fn cache_ttl_is_one_minute() {
481 assert_eq!(PERM_CACHE_TTL.as_secs(), 60);
482 }
483}