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
330#[cfg(test)]
331mod tests {
332 use super::*;
333
334 #[test]
335 fn administrator_and_developer_bypass_group_checks() {
336 for &(role, expected) in &[
338 (Role::User, false),
339 (Role::Staff, false),
340 (Role::Supervisor, false),
341 (Role::Administrator, true),
342 (Role::Developer, true),
343 ] {
344 let id = Identity {
345 user_id: 1,
346 email: "a@b.com".into(),
347 role,
348 is_active: true,
349 is_demo: false,
350 demo_label: None,
351 must_change_password: false,
352 mfa_enabled: false,
353 trust_level: crate::auth::SessionTrust::Authenticated,
354 };
355 assert_eq!(
356 id.role.bypasses_group_checks(),
357 expected,
358 "{role:?} should be {expected}"
359 );
360 }
361 }
362
363 #[test]
364 fn cache_ttl_is_one_minute() {
365 assert_eq!(PERM_CACHE_TTL.as_secs(), 60);
366 }
367}