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;
40
41#[derive(Debug, Clone)]
42pub struct Permission {
43 pub id: i64,
44 pub name: String,
45 pub description: String,
46}
47
48#[derive(Debug, thiserror::Error)]
49pub enum PermissionError {
50 #[error("permission `{0}` not found")]
51 Missing(String),
52 #[error("user not found")]
53 NoSuchUser,
54 #[error("group not found")]
55 NoSuchGroup,
56}
57
58pub async fn init_permission_tables(db: &Db) -> Result<()> {
61 sqlx::query(
62 "CREATE TABLE IF NOT EXISTS rustio_permissions (
63 id BIGSERIAL PRIMARY KEY,
64 name TEXT NOT NULL UNIQUE,
65 description TEXT NOT NULL DEFAULT '',
66 created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
67 )",
68 )
69 .execute(db.pool())
70 .await?;
71
72 sqlx::query(
73 "CREATE TABLE IF NOT EXISTS rustio_groups (
74 id BIGSERIAL PRIMARY KEY,
75 name TEXT NOT NULL UNIQUE,
76 description TEXT NOT NULL DEFAULT '',
77 created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
78 )",
79 )
80 .execute(db.pool())
81 .await?;
82
83 sqlx::query(
84 "CREATE TABLE IF NOT EXISTS rustio_group_permissions (
85 group_id BIGINT NOT NULL REFERENCES rustio_groups(id) ON DELETE CASCADE,
86 permission_id BIGINT NOT NULL REFERENCES rustio_permissions(id) ON DELETE CASCADE,
87 PRIMARY KEY (group_id, permission_id)
88 )",
89 )
90 .execute(db.pool())
91 .await?;
92
93 sqlx::query(
94 "CREATE TABLE IF NOT EXISTS rustio_user_groups (
95 user_id BIGINT NOT NULL REFERENCES rustio_users(id) ON DELETE CASCADE,
96 group_id BIGINT NOT NULL REFERENCES rustio_groups(id) ON DELETE CASCADE,
97 PRIMARY KEY (user_id, group_id)
98 )",
99 )
100 .execute(db.pool())
101 .await?;
102
103 sqlx::query(
104 "CREATE TABLE IF NOT EXISTS rustio_user_permissions (
105 user_id BIGINT NOT NULL REFERENCES rustio_users(id) ON DELETE CASCADE,
106 permission_id BIGINT NOT NULL REFERENCES rustio_permissions(id) ON DELETE CASCADE,
107 PRIMARY KEY (user_id, permission_id)
108 )",
109 )
110 .execute(db.pool())
111 .await?;
112
113 Ok(())
114}
115
116struct CacheEntry {
119 perms: Arc<HashSet<String>>,
120 expires: Instant,
121}
122
123static PERM_CACHE: Lazy<DashMap<i64, CacheEntry>> = Lazy::new(DashMap::new);
124
125const PERM_CACHE_TTL: Duration = Duration::from_secs(60);
126
127pub(crate) fn invalidate_user_cache(user_id: i64) {
128 PERM_CACHE.remove(&user_id);
129}
130
131fn invalidate_group_cache(db: &Db, group_id: i64) {
132 let db = db.clone();
135 tokio::spawn(async move {
136 let rows = sqlx::query("SELECT user_id FROM rustio_user_groups WHERE group_id = $1")
137 .bind(group_id)
138 .fetch_all(db.pool())
139 .await
140 .unwrap_or_default();
141 for r in rows {
142 if let Ok(uid) = r.try_get::<i64, _>("user_id") {
143 invalidate_user_cache(uid);
144 }
145 }
146 });
147}
148
149pub async fn permissions_for_user(db: &Db, user_id: i64) -> Result<Arc<HashSet<String>>> {
154 if let Some(e) = PERM_CACHE.get(&user_id) {
155 if e.expires > Instant::now() {
156 return Ok(e.perms.clone());
157 }
158 }
159
160 let rows = sqlx::query(
161 "SELECT DISTINCT p.name
162 FROM rustio_permissions p
163 LEFT JOIN rustio_user_permissions up ON up.permission_id = p.id
164 LEFT JOIN rustio_group_permissions gp ON gp.permission_id = p.id
165 LEFT JOIN rustio_user_groups ug ON ug.group_id = gp.group_id
166 WHERE up.user_id = $1 OR ug.user_id = $1",
167 )
168 .bind(user_id)
169 .fetch_all(db.pool())
170 .await?;
171
172 let mut set = HashSet::with_capacity(rows.len());
173 for r in rows {
174 if let Ok(name) = r.try_get::<String, _>("name") {
175 set.insert(name);
176 }
177 }
178 let arc = Arc::new(set);
179 PERM_CACHE.insert(
180 user_id,
181 CacheEntry {
182 perms: arc.clone(),
183 expires: Instant::now() + PERM_CACHE_TTL,
184 },
185 );
186 Ok(arc)
187}
188
189pub async fn check_permission(db: &Db, identity: &Identity, permission: &str) -> Result<bool> {
197 if !identity.is_active {
198 return Ok(false);
199 }
200 if identity.role.bypasses_group_checks() {
201 return Ok(true);
202 }
203 let perms = permissions_for_user(db, identity.user_id).await?;
204 Ok(perms.contains(permission))
205}
206
207async fn permission_id(db: &Db, name: &str) -> Result<i64> {
210 if let Some(row) = sqlx::query("SELECT id FROM rustio_permissions WHERE name = $1")
211 .bind(name)
212 .fetch_optional(db.pool())
213 .await?
214 {
215 return row
216 .try_get("id")
217 .map_err(|e| Error::Internal(format!("{e}")));
218 }
219 let row = sqlx::query(
220 "INSERT INTO rustio_permissions (name, description)
221 VALUES ($1, $2)
222 ON CONFLICT (name) DO UPDATE SET description = rustio_permissions.description
223 RETURNING id",
224 )
225 .bind(name)
226 .bind("")
227 .fetch_one(db.pool())
228 .await?;
229 row.try_get("id")
230 .map_err(|e| Error::Internal(format!("{e}")))
231}
232
233pub async fn grant_to_user(db: &Db, user_id: i64, permission: &str) -> Result<()> {
234 let pid = permission_id(db, permission).await?;
235 sqlx::query(
236 "INSERT INTO rustio_user_permissions (user_id, permission_id)
237 VALUES ($1, $2)
238 ON CONFLICT DO NOTHING",
239 )
240 .bind(user_id)
241 .bind(pid)
242 .execute(db.pool())
243 .await?;
244 invalidate_user_cache(user_id);
245 Ok(())
246}
247
248pub async fn grant_to_group(db: &Db, group_id: i64, permission: &str) -> Result<()> {
249 let pid = permission_id(db, permission).await?;
250 sqlx::query(
251 "INSERT INTO rustio_group_permissions (group_id, permission_id)
252 VALUES ($1, $2)
253 ON CONFLICT DO NOTHING",
254 )
255 .bind(group_id)
256 .bind(pid)
257 .execute(db.pool())
258 .await?;
259 invalidate_group_cache(db, group_id);
260 Ok(())
261}
262
263pub async fn create_group(db: &Db, name: &str, description: &str) -> Result<i64> {
264 let row = sqlx::query(
265 "INSERT INTO rustio_groups (name, description)
266 VALUES ($1, $2)
267 RETURNING id",
268 )
269 .bind(name)
270 .bind(description)
271 .fetch_one(db.pool())
272 .await?;
273 row.try_get("id")
274 .map_err(|e| Error::Internal(format!("{e}")))
275}
276
277pub async fn add_user_to_group(db: &Db, user_id: i64, group_id: i64) -> Result<()> {
278 sqlx::query(
279 "INSERT INTO rustio_user_groups (user_id, group_id)
280 VALUES ($1, $2)
281 ON CONFLICT DO NOTHING",
282 )
283 .bind(user_id)
284 .bind(group_id)
285 .execute(db.pool())
286 .await?;
287 invalidate_user_cache(user_id);
288 Ok(())
289}
290
291pub async fn remove_user_from_group(db: &Db, user_id: i64, group_id: i64) -> Result<()> {
292 sqlx::query("DELETE FROM rustio_user_groups WHERE user_id = $1 AND group_id = $2")
293 .bind(user_id)
294 .bind(group_id)
295 .execute(db.pool())
296 .await?;
297 invalidate_user_cache(user_id);
298 Ok(())
299}
300
301pub async fn register_model_permissions(db: &Db, app: &str, singular: &str) -> Result<()> {
305 let actions = ["add", "change", "delete", "view"];
306 for action in actions {
307 let name = format!("{app}.{action}_{singular}");
308 let _ = permission_id(db, &name).await?;
309 }
310 Ok(())
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316
317 #[test]
318 fn administrator_and_developer_bypass_group_checks() {
319 for &(role, expected) in &[
321 (Role::User, false),
322 (Role::Staff, false),
323 (Role::Supervisor, false),
324 (Role::Administrator, true),
325 (Role::Developer, true),
326 ] {
327 let id = Identity {
328 user_id: 1,
329 email: "a@b.com".into(),
330 role,
331 is_active: true,
332 is_demo: false,
333 demo_label: None,
334 };
335 assert_eq!(
336 id.role.bypasses_group_checks(),
337 expected,
338 "{role:?} should be {expected}"
339 );
340 }
341 }
342
343 #[test]
344 fn cache_ttl_is_one_minute() {
345 assert_eq!(PERM_CACHE_TTL.as_secs(), 60);
346 }
347}