Skip to main content

shared/models/
role.rs

1use crate::{
2    models::{InsertQueryBuilder, UpdateQueryBuilder},
3    prelude::*,
4};
5use garde::Validate;
6use serde::{Deserialize, Serialize};
7use sqlx::{Row, postgres::PgRow};
8use std::{
9    collections::BTreeMap,
10    sync::{Arc, LazyLock},
11};
12use utoipa::ToSchema;
13
14#[derive(Serialize, Deserialize, Clone)]
15pub struct Role {
16    pub uuid: uuid::Uuid,
17
18    pub name: compact_str::CompactString,
19    pub description: Option<compact_str::CompactString>,
20
21    pub require_two_factor: bool,
22
23    pub admin_permissions: Arc<Vec<compact_str::CompactString>>,
24    pub server_permissions: Arc<Vec<compact_str::CompactString>>,
25
26    pub created: chrono::NaiveDateTime,
27
28    extension_data: super::ModelExtensionData,
29}
30
31impl BaseModel for Role {
32    const NAME: &'static str = "role";
33
34    fn get_extension_list() -> &'static super::ModelExtensionList {
35        static EXTENSIONS: LazyLock<super::ModelExtensionList> =
36            LazyLock::new(|| parking_lot::RwLock::new(Vec::new()));
37
38        &EXTENSIONS
39    }
40
41    fn get_extension_data(&self) -> &super::ModelExtensionData {
42        &self.extension_data
43    }
44
45    #[inline]
46    fn base_columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
47        let prefix = prefix.unwrap_or_default();
48
49        BTreeMap::from([
50            ("roles.uuid", compact_str::format_compact!("{prefix}uuid")),
51            ("roles.name", compact_str::format_compact!("{prefix}name")),
52            (
53                "roles.description",
54                compact_str::format_compact!("{prefix}description"),
55            ),
56            (
57                "roles.require_two_factor",
58                compact_str::format_compact!("{prefix}require_two_factor"),
59            ),
60            (
61                "roles.admin_permissions",
62                compact_str::format_compact!("{prefix}admin_permissions"),
63            ),
64            (
65                "roles.server_permissions",
66                compact_str::format_compact!("{prefix}server_permissions"),
67            ),
68            (
69                "roles.created",
70                compact_str::format_compact!("{prefix}created"),
71            ),
72        ])
73    }
74
75    #[inline]
76    fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
77        let prefix = prefix.unwrap_or_default();
78
79        Ok(Self {
80            uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
81            name: row.try_get(compact_str::format_compact!("{prefix}name").as_str())?,
82            description: row
83                .try_get(compact_str::format_compact!("{prefix}description").as_str())?,
84            require_two_factor: row
85                .try_get(compact_str::format_compact!("{prefix}require_two_factor").as_str())?,
86            admin_permissions: Arc::new(
87                row.try_get(compact_str::format_compact!("{prefix}admin_permissions").as_str())?,
88            ),
89            server_permissions: Arc::new(
90                row.try_get(compact_str::format_compact!("{prefix}server_permissions").as_str())?,
91            ),
92            created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
93            extension_data: Self::map_extensions(prefix, row)?,
94        })
95    }
96
97    fn cache_invalidation_keys(&self) -> Vec<compact_str::CompactString> {
98        vec![compact_str::format_compact!(
99            "{}::{}",
100            Self::NAME,
101            self.uuid
102        )]
103    }
104}
105
106impl Role {
107    async fn user_uuids(
108        &self,
109        database: &crate::database::Database,
110    ) -> Result<Vec<uuid::Uuid>, sqlx::Error> {
111        sqlx::query_scalar(
112            r#"
113            SELECT users.uuid
114            FROM users
115            WHERE users.role_uuid = $1
116            "#,
117        )
118        .bind(self.uuid)
119        .fetch_all(database.read())
120        .await
121    }
122
123    async fn invalidate_cached_with_users(
124        &self,
125        database: &crate::database::Database,
126    ) -> Result<(), sqlx::Error> {
127        Self::invalidate_cached(database, self.uuid).await;
128        for user_uuid in self.user_uuids(database).await? {
129            super::user::User::invalidate_cached(database, user_uuid).await;
130        }
131
132        Ok(())
133    }
134
135    pub async fn all_with_pagination(
136        database: &crate::database::Database,
137        page: i64,
138        per_page: i64,
139        search: Option<&str>,
140    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
141        super::PaginatedQueryBuilder::new(
142            Self::columns_sql(None),
143            format!(
144                r#"
145                FROM roles
146                WHERE {search}
147                "#,
148                search = super::search_sql(1, &["roles.name"], &["roles.uuid"]),
149            ),
150            "roles.created, roles.uuid",
151        )
152        .bind(search)
153        .fetch(database, page, per_page)
154        .await
155    }
156}
157
158#[async_trait::async_trait]
159impl IntoAdminApiObject for Role {
160    type AdminApiObject = AdminApiRole;
161    type ExtraArgs<'a> = ();
162
163    async fn into_admin_api_object<'a>(
164        self,
165        state: &crate::State,
166        _args: Self::ExtraArgs<'a>,
167    ) -> Result<Self::AdminApiObject, crate::database::DatabaseError> {
168        let api_object = AdminApiRole::init_hooks(&self, state).await?;
169
170        let api_object = finish_extendible!(
171            AdminApiRole {
172                uuid: self.uuid,
173                name: self.name,
174                description: self.description,
175                require_two_factor: self.require_two_factor,
176                admin_permissions: self.admin_permissions,
177                server_permissions: self.server_permissions,
178                created: self.created.and_utc(),
179            },
180            api_object,
181            state
182        )?;
183
184        Ok(api_object)
185    }
186}
187
188#[async_trait::async_trait]
189impl ByUuid for Role {
190    async fn by_uuid(
191        database: &crate::database::Database,
192        uuid: uuid::Uuid,
193    ) -> Result<Self, crate::database::DatabaseError> {
194        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
195            r#"
196            SELECT {}
197            FROM roles
198            WHERE roles.uuid = $1
199            "#,
200            Self::columns_sql(None)
201        )))
202        .bind(uuid)
203        .fetch_one(database.read())
204        .await?;
205
206        Self::map(None, &row)
207    }
208
209    async fn by_uuid_with_transaction(
210        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
211        uuid: uuid::Uuid,
212    ) -> Result<Self, crate::database::DatabaseError> {
213        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
214            r#"
215            SELECT {}
216            FROM roles
217            WHERE roles.uuid = $1
218            "#,
219            Self::columns_sql(None)
220        )))
221        .bind(uuid)
222        .fetch_one(&mut **transaction)
223        .await?;
224
225        Self::map(None, &row)
226    }
227}
228
229#[derive(ToSchema, Deserialize, Validate)]
230pub struct CreateRoleOptions {
231    #[garde(length(chars, min = 1, max = 255))]
232    #[schema(min_length = 1, max_length = 255)]
233    pub name: compact_str::CompactString,
234    #[garde(length(chars, min = 1, max = 1024))]
235    #[schema(min_length = 1, max_length = 1024)]
236    pub description: Option<compact_str::CompactString>,
237    #[garde(skip)]
238    pub require_two_factor: bool,
239    #[garde(custom(crate::permissions::validate_admin_permissions))]
240    pub admin_permissions: Vec<compact_str::CompactString>,
241    #[garde(custom(crate::permissions::validate_server_permissions))]
242    pub server_permissions: Vec<compact_str::CompactString>,
243}
244
245#[async_trait::async_trait]
246impl CreatableModel for Role {
247    type CreateOptions<'a> = CreateRoleOptions;
248    type CreateResult = Self;
249
250    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
251        static CREATE_LISTENERS: LazyLock<CreateListenerList<Role>> =
252            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
253
254        &CREATE_LISTENERS
255    }
256
257    async fn create_with_transaction(
258        state: &crate::State,
259        mut options: Self::CreateOptions<'_>,
260        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
261    ) -> Result<Self, crate::database::DatabaseError> {
262        options.validate()?;
263
264        let mut query_builder = InsertQueryBuilder::new("roles");
265
266        Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
267
268        query_builder
269            .set("name", &options.name)
270            .set("description", &options.description)
271            .set("require_two_factor", options.require_two_factor)
272            .set("admin_permissions", &options.admin_permissions)
273            .set("server_permissions", &options.server_permissions);
274
275        let row = query_builder
276            .returning(&Self::columns_sql(None))
277            .fetch_one(&mut **transaction)
278            .await?;
279        let mut role = Self::map(None, &row)?;
280
281        Self::run_after_create_handlers(&mut role, &options, state, transaction).await?;
282
283        Ok(role)
284    }
285}
286
287#[derive(ToSchema, Serialize, Deserialize, Validate, Clone, Default)]
288pub struct UpdateRoleOptions {
289    #[garde(length(chars, min = 1, max = 255))]
290    #[schema(min_length = 1, max_length = 255)]
291    pub name: Option<compact_str::CompactString>,
292    #[garde(length(chars, min = 1, max = 1024))]
293    #[schema(min_length = 1, max_length = 1024)]
294    #[serde(
295        default,
296        skip_serializing_if = "Option::is_none",
297        with = "::serde_with::rust::double_option"
298    )]
299    pub description: Option<Option<compact_str::CompactString>>,
300    #[garde(skip)]
301    pub require_two_factor: Option<bool>,
302    #[garde(inner(custom(crate::permissions::validate_admin_permissions)))]
303    pub admin_permissions: Option<Vec<compact_str::CompactString>>,
304    #[garde(inner(custom(crate::permissions::validate_server_permissions)))]
305    pub server_permissions: Option<Vec<compact_str::CompactString>>,
306}
307
308#[async_trait::async_trait]
309impl UpdatableModel for Role {
310    type UpdateOptions = UpdateRoleOptions;
311
312    fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
313        static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<Role>> =
314            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
315
316        &UPDATE_LISTENERS
317    }
318
319    async fn update(
320        &mut self,
321        state: &crate::State,
322        options: Self::UpdateOptions,
323    ) -> Result<(), crate::database::DatabaseError> {
324        let access_changed = options
325            .admin_permissions
326            .as_ref()
327            .is_some_and(|permissions| permissions != self.admin_permissions.as_ref())
328            || options
329                .server_permissions
330                .as_ref()
331                .is_some_and(|permissions| permissions != self.server_permissions.as_ref())
332            || options
333                .require_two_factor
334                .is_some_and(|require_two_factor| require_two_factor != self.require_two_factor);
335
336        let mut transaction = state.database.write().begin().await?;
337
338        if let Err(err) = self
339            .update_with_transaction(state, options, &mut transaction)
340            .await
341        {
342            transaction.rollback().await?;
343            return Err(err);
344        }
345
346        transaction.commit().await?;
347
348        self.invalidate_cached_with_users(&state.database).await?;
349
350        if access_changed {
351            match self.user_uuids(&state.database).await {
352                Ok(user_uuids) => super::node::Node::invalidate_permissions_everywhere(
353                    &state.database,
354                    super::node::PermissionsInvalidation {
355                        users: user_uuids,
356                        ..Default::default()
357                    },
358                ),
359                Err(err) => {
360                    tracing::error!(role = %self.uuid, "failed to invalidate permissions in wings: {:?}", err);
361                }
362            }
363        }
364
365        Ok(())
366    }
367
368    async fn update_with_transaction(
369        &mut self,
370        state: &crate::State,
371        mut options: Self::UpdateOptions,
372        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
373    ) -> Result<(), crate::database::DatabaseError> {
374        options.validate()?;
375
376        let mut query_builder = UpdateQueryBuilder::new("roles");
377
378        self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
379            .await?;
380
381        query_builder
382            .set("name", options.name.as_ref())
383            .set(
384                "description",
385                options.description.as_ref().map(|d| d.as_ref()),
386            )
387            .set("require_two_factor", options.require_two_factor)
388            .set("admin_permissions", options.admin_permissions.as_ref())
389            .set("server_permissions", options.server_permissions.as_ref())
390            .where_eq("uuid", self.uuid);
391
392        query_builder.execute(&mut **transaction).await?;
393
394        if let Some(name) = options.name {
395            self.name = name;
396        }
397        if let Some(description) = options.description {
398            self.description = description;
399        }
400        if let Some(require_two_factor) = options.require_two_factor {
401            self.require_two_factor = require_two_factor;
402        }
403        if let Some(admin_permissions) = options.admin_permissions {
404            self.admin_permissions = Arc::new(admin_permissions);
405        }
406        if let Some(server_permissions) = options.server_permissions {
407            self.server_permissions = Arc::new(server_permissions);
408        }
409
410        self.run_after_update_handlers(state, transaction).await?;
411
412        Ok(())
413    }
414}
415
416#[async_trait::async_trait]
417impl DeletableModel for Role {
418    type DeleteOptions = ();
419
420    fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
421        static DELETE_LISTENERS: LazyLock<DeleteHandlerList<Role>> =
422            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
423
424        &DELETE_LISTENERS
425    }
426
427    async fn delete(
428        &self,
429        state: &crate::State,
430        options: Self::DeleteOptions,
431    ) -> Result<(), anyhow::Error> {
432        let user_uuids = self.user_uuids(&state.database).await?;
433
434        let mut transaction = state.database.write().begin().await?;
435
436        if let Err(err) = self
437            .delete_with_transaction(state, options, &mut transaction)
438            .await
439        {
440            transaction.rollback().await?;
441            return Err(err);
442        }
443
444        transaction.commit().await?;
445
446        Self::invalidate_cached(&state.database, self.uuid).await;
447        for &user_uuid in &user_uuids {
448            super::user::User::invalidate_cached(&state.database, user_uuid).await;
449        }
450        super::node::Node::invalidate_permissions_everywhere(
451            &state.database,
452            super::node::PermissionsInvalidation {
453                users: user_uuids,
454                ..Default::default()
455            },
456        );
457
458        Ok(())
459    }
460
461    async fn delete_with_transaction(
462        &self,
463        state: &crate::State,
464        options: Self::DeleteOptions,
465        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
466    ) -> Result<(), anyhow::Error> {
467        self.run_delete_handlers(&options, state, transaction)
468            .await?;
469
470        sqlx::query(
471            r#"
472            DELETE FROM roles
473            WHERE roles.uuid = $1
474            "#,
475        )
476        .bind(self.uuid)
477        .execute(&mut **transaction)
478        .await?;
479
480        self.run_after_delete_handlers(&options, state, transaction)
481            .await?;
482
483        Ok(())
484    }
485}
486
487#[derive(Validate)]
488pub struct DuplicateRoleOptions {
489    #[garde(length(chars, min = 1, max = 255))]
490    pub name: compact_str::CompactString,
491}
492
493#[async_trait::async_trait]
494impl DuplicableModel for Role {
495    type DuplicateOptions<'a> = DuplicateRoleOptions;
496    type DuplicateResult = Self;
497
498    fn get_duplicate_handlers() -> &'static LazyLock<DuplicateHandlerList<Self>> {
499        static DUPLICATE_LISTENERS: LazyLock<DuplicateHandlerList<Role>> =
500            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
501
502        &DUPLICATE_LISTENERS
503    }
504
505    async fn duplicate_with_transaction(
506        &self,
507        state: &crate::State,
508        options: Self::DuplicateOptions<'_>,
509        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
510    ) -> Result<Self, crate::database::DatabaseError> {
511        options.validate()?;
512
513        self.run_duplicate_handlers(&options, state, transaction)
514            .await?;
515
516        let mut query_builder = InsertQueryBuilder::new("roles");
517
518        query_builder
519            .set("name", &options.name)
520            .set("description", &self.description)
521            .set("require_two_factor", self.require_two_factor)
522            .set("admin_permissions", self.admin_permissions.as_ref())
523            .set("server_permissions", self.server_permissions.as_ref());
524
525        let row = query_builder
526            .returning(&Self::columns_sql(None))
527            .fetch_one(&mut **transaction)
528            .await?;
529        let mut role = Self::map(None, &row)?;
530
531        self.run_after_duplicate_handlers(&mut role, &options, state, transaction)
532            .await?;
533
534        Ok(role)
535    }
536}
537
538#[schema_extension_derive::extendible]
539#[init_args(Role, crate::State)]
540#[hook_args(crate::State)]
541#[derive(ToSchema, Serialize)]
542#[schema(title = "Role")]
543pub struct AdminApiRole {
544    pub uuid: uuid::Uuid,
545
546    pub name: compact_str::CompactString,
547    pub description: Option<compact_str::CompactString>,
548
549    pub require_two_factor: bool,
550
551    pub admin_permissions: Arc<Vec<compact_str::CompactString>>,
552    pub server_permissions: Arc<Vec<compact_str::CompactString>>,
553
554    pub created: chrono::DateTime<chrono::Utc>,
555}