1use crate::{
2 models::{InsertQueryBuilder, UpdateQueryBuilder},
3 prelude::*,
4};
5use garde::Validate;
6use rand::distr::SampleString;
7use serde::{Deserialize, Serialize};
8use sqlx::{Row, postgres::PgRow};
9use std::{
10 collections::BTreeMap,
11 sync::{Arc, LazyLock},
12};
13use utoipa::ToSchema;
14
15#[derive(Clone, Serialize, Deserialize)]
16pub struct ServerSubuser {
17 pub user: super::user::User,
18 pub server: Fetchable<super::server::Server>,
19
20 pub permissions: Vec<compact_str::CompactString>,
21 pub ignored_files: Vec<compact_str::CompactString>,
22
23 pub created: chrono::NaiveDateTime,
24
25 extension_data: super::ModelExtensionData,
26}
27
28impl BaseModel for ServerSubuser {
29 const NAME: &'static str = "server_subuser";
30
31 fn get_extension_list() -> &'static super::ModelExtensionList {
32 static EXTENSIONS: LazyLock<super::ModelExtensionList> =
33 LazyLock::new(|| parking_lot::RwLock::new(Vec::new()));
34
35 &EXTENSIONS
36 }
37
38 fn get_extension_data(&self) -> &super::ModelExtensionData {
39 &self.extension_data
40 }
41
42 #[inline]
43 fn base_columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
44 let prefix = prefix.unwrap_or_default();
45
46 let mut columns = BTreeMap::from([
47 (
48 "server_subusers.server_uuid",
49 compact_str::format_compact!("{prefix}server_uuid"),
50 ),
51 (
52 "server_subusers.permissions",
53 compact_str::format_compact!("{prefix}permissions"),
54 ),
55 (
56 "server_subusers.ignored_files",
57 compact_str::format_compact!("{prefix}ignored_files"),
58 ),
59 (
60 "server_subusers.created",
61 compact_str::format_compact!("{prefix}created"),
62 ),
63 ]);
64
65 columns.extend(super::user::User::base_columns(Some("user_")));
66
67 columns
68 }
69
70 #[inline]
71 fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
72 let prefix = prefix.unwrap_or_default();
73
74 Ok(Self {
75 user: super::user::User::map(Some("user_"), row)?,
76 server: super::server::Server::get_fetchable(
77 row.try_get(compact_str::format_compact!("{prefix}server_uuid").as_str())?,
78 ),
79 permissions: row
80 .try_get(compact_str::format_compact!("{prefix}permissions").as_str())?,
81 ignored_files: row
82 .try_get(compact_str::format_compact!("{prefix}ignored_files").as_str())?,
83 created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
84 extension_data: Self::map_extensions(prefix, row)?,
85 })
86 }
87
88 fn cache_invalidation_keys(&self) -> Vec<compact_str::CompactString> {
89 vec![Self::permissions_cache_key(
90 self.server.uuid,
91 self.user.uuid,
92 )]
93 }
94}
95
96impl ServerSubuser {
97 async fn invalidate_wings_permissions(&self, database: &Arc<crate::database::Database>) {
98 let node = async {
99 self.server
100 .fetch_cached(database)
101 .await?
102 .node
103 .fetch_cached(database)
104 .await
105 }
106 .await;
107
108 match node {
109 Ok(node) => node.invalidate_permissions(
110 database,
111 super::node::PermissionsInvalidation {
112 users: vec![self.user.uuid],
113 ..Default::default()
114 },
115 ),
116 Err(err) => {
117 tracing::error!(server = %self.server.uuid, "failed to invalidate permissions in wings: {:?}", err);
118 }
119 }
120 }
121
122 fn permissions_cache_key(
123 server_uuid: uuid::Uuid,
124 user_uuid: uuid::Uuid,
125 ) -> compact_str::CompactString {
126 compact_str::format_compact!("{}::{server_uuid}::{user_uuid}", Self::NAME)
127 }
128
129 pub async fn permissions_by_server_uuid_user_uuid_cached(
130 database: &crate::database::Database,
131 server_uuid: uuid::Uuid,
132 user_uuid: uuid::Uuid,
133 ) -> Result<
134 Option<(
135 Vec<compact_str::CompactString>,
136 Vec<compact_str::CompactString>,
137 )>,
138 anyhow::Error,
139 > {
140 database
141 .cache
142 .cached(
143 &Self::permissions_cache_key(server_uuid, user_uuid),
144 super::BY_UUID_CACHE_TTL,
145 || Self::permissions_by_server_uuid_user_uuid(database, server_uuid, user_uuid),
146 )
147 .await
148 }
149
150 pub async fn permissions_by_server_uuid_user_uuid(
151 database: &crate::database::Database,
152 server_uuid: uuid::Uuid,
153 user_uuid: uuid::Uuid,
154 ) -> Result<
155 Option<(
156 Vec<compact_str::CompactString>,
157 Vec<compact_str::CompactString>,
158 )>,
159 sqlx::Error,
160 > {
161 let row = sqlx::query(
162 r#"
163 SELECT server_subusers.permissions, server_subusers.ignored_files
164 FROM server_subusers
165 WHERE server_subusers.server_uuid = $1 AND server_subusers.user_uuid = $2
166 "#,
167 )
168 .bind(server_uuid)
169 .bind(user_uuid)
170 .fetch_optional(database.read())
171 .await?;
172
173 row.map(|row| Ok((row.try_get("permissions")?, row.try_get("ignored_files")?)))
174 .transpose()
175 }
176
177 pub async fn by_server_uuid_user_uuid(
178 database: &crate::database::Database,
179 server_uuid: uuid::Uuid,
180 user_uuid: uuid::Uuid,
181 ) -> Result<Option<Self>, crate::database::DatabaseError> {
182 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
183 r#"
184 SELECT {}
185 FROM server_subusers
186 JOIN users ON users.uuid = server_subusers.user_uuid
187 LEFT JOIN roles ON roles.uuid = users.role_uuid
188 WHERE server_subusers.server_uuid = $1 AND server_subusers.user_uuid = $2
189 "#,
190 Self::columns_sql(None)
191 )))
192 .bind(server_uuid)
193 .bind(user_uuid)
194 .fetch_optional(database.read())
195 .await?;
196
197 row.try_map(|row| Self::map(None, &row))
198 }
199
200 pub async fn by_server_uuid_with_pagination(
201 database: &crate::database::Database,
202 server_uuid: uuid::Uuid,
203 page: i64,
204 per_page: i64,
205 search: Option<&str>,
206 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
207 super::PaginatedQueryBuilder::new(
208 Self::columns_sql(None),
209 format!(
210 r#"
211 FROM server_subusers
212 JOIN users ON users.uuid = server_subusers.user_uuid
213 LEFT JOIN roles ON roles.uuid = users.role_uuid
214 WHERE server_subusers.server_uuid = $1 AND {search}
215 "#,
216 search = super::search_sql(2, &["users.username"], &["users.uuid"]),
217 ),
218 "server_subusers.created, server_subusers.server_uuid, server_subusers.user_uuid",
219 )
220 .bind(server_uuid)
221 .bind(search)
222 .fetch(database, page, per_page)
223 .await
224 }
225
226 pub async fn count_by_server_uuid(
227 database: &crate::database::Database,
228 server_uuid: uuid::Uuid,
229 ) -> Result<i64, sqlx::Error> {
230 sqlx::query_scalar(
231 r#"
232 SELECT COUNT(*)
233 FROM server_subusers
234 WHERE server_subusers.server_uuid = $1
235 "#,
236 )
237 .bind(server_uuid)
238 .fetch_one(database.read())
239 .await
240 }
241}
242
243#[async_trait::async_trait]
244impl IntoApiObject for ServerSubuser {
245 type ApiObject = ApiServerSubuser;
246 type ExtraArgs<'a> = &'a crate::storage::StorageUrlRetriever<'a>;
247
248 async fn into_api_object<'a>(
249 self,
250 state: &crate::State,
251 storage_url_retriever: Self::ExtraArgs<'a>,
252 ) -> Result<Self::ApiObject, crate::database::DatabaseError> {
253 let api_object = ApiServerSubuser::init_hooks(&self, state).await?;
254
255 let api_object = finish_extendible!(
256 ApiServerSubuser {
257 user: self
258 .user
259 .into_api_object(state, storage_url_retriever)
260 .await?,
261 permissions: self.permissions,
262 ignored_files: self.ignored_files,
263 created: self.created.and_utc(),
264 },
265 api_object,
266 state
267 )?;
268
269 Ok(api_object)
270 }
271}
272
273#[derive(Validate)]
274pub struct CreateServerSubuserOptions<'a> {
275 #[garde(skip)]
276 pub server: &'a super::server::Server,
277
278 #[garde(email)]
279 pub email: compact_str::CompactString,
280 #[garde(custom(crate::permissions::validate_server_permissions))]
281 pub permissions: Vec<compact_str::CompactString>,
282 #[garde(
283 length(max = 1024),
284 inner(length(chars, min = 1, max = 255)),
285 custom(crate::utils::validate_ignored_files)
286 )]
287 pub ignored_files: Vec<compact_str::CompactString>,
288}
289
290#[async_trait::async_trait]
291impl CreatableModel for ServerSubuser {
292 type CreateOptions<'a> = CreateServerSubuserOptions<'a>;
293 type CreateResult = Self;
294
295 fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
296 static CREATE_LISTENERS: LazyLock<CreateListenerList<ServerSubuser>> =
297 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
298
299 &CREATE_LISTENERS
300 }
301
302 async fn create(
303 state: &crate::State,
304 options: Self::CreateOptions<'_>,
305 ) -> Result<Self, crate::database::DatabaseError> {
306 let mut transaction = state.database.write().begin().await?;
307
308 let result = match Self::create_with_transaction(state, options, &mut transaction).await {
309 Ok(result) => result,
310 Err(err) => {
311 transaction.rollback().await?;
312 return Err(err);
313 }
314 };
315
316 transaction.commit().await?;
317
318 super::invalidate_cache_keys(&state.database, &result.cache_invalidation_keys()).await;
319
320 result.invalidate_wings_permissions(&state.database).await;
321
322 Ok(result)
323 }
324
325 async fn create_with_transaction(
326 state: &crate::State,
327 mut options: Self::CreateOptions<'_>,
328 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
329 ) -> Result<Self, crate::database::DatabaseError> {
330 options.validate()?;
331
332 let user = match super::user::User::by_email(&state.database, &options.email).await? {
333 Some(user) => user,
334 None => {
335 let username = options
336 .email
337 .split('@')
338 .next()
339 .unwrap_or("unknown")
340 .chars()
341 .filter(|c| c.is_alphanumeric() || *c == '_')
342 .take(10)
343 .collect::<compact_str::CompactString>();
344 let username = compact_str::format_compact!(
345 "{username}_{}",
346 rand::distr::Alphanumeric.sample_string(&mut rand::rng(), 4)
347 );
348
349 let app_settings = state.settings.get().await?;
350
351 let create_options = super::user::CreateUserOptions {
352 role_uuid: None,
353 external_id: None,
354 username: username.clone(),
355 email: options.email.clone(),
356 name_first: None,
357 name_last: None,
358 password: None,
359 admin: false,
360 frozen: false,
361 suspended: false,
362 verify_email: false,
363 send_email: true,
364 language: app_settings.app.language.clone(),
365 };
366 drop(app_settings);
367 match super::user::User::create(state, create_options).await {
368 Ok(user) => user,
369 Err(err) => {
370 tracing::error!(username = %username, email = %options.email, "failed to create subuser user: {:?}", err);
371 return Err(err);
372 }
373 }
374 }
375 };
376
377 if options.server.owner.uuid == user.uuid {
378 return Err(sqlx::Error::InvalidArgument(
379 "cannot create subuser for server owner".into(),
380 )
381 .into());
382 }
383
384 let server_uuid = options.server.uuid;
385 let username = user.username.clone();
386
387 let mut query_builder = InsertQueryBuilder::new("server_subusers");
388
389 Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
390
391 query_builder
392 .set("server_uuid", options.server.uuid)
393 .set("user_uuid", user.uuid)
394 .set("permissions", &options.permissions)
395 .set("ignored_files", &options.ignored_files);
396
397 query_builder.execute(&mut **transaction).await?;
398
399 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
400 r#"
401 SELECT {}
402 FROM server_subusers
403 JOIN users ON users.uuid = server_subusers.user_uuid
404 LEFT JOIN roles ON roles.uuid = users.role_uuid
405 WHERE server_subusers.server_uuid = $1 AND users.username = $2
406 "#,
407 Self::columns_sql(None)
408 )))
409 .bind(server_uuid)
410 .bind(username.as_str())
411 .fetch_one(&mut **transaction)
412 .await?;
413
414 let mut result = Self::map(None, &row)?;
415
416 Self::run_after_create_handlers(&mut result, &options, state, transaction).await?;
417
418 let settings = state.settings.get().await?;
419 state
420 .mail
421 .send_template(
422 state,
423 "added_to_server",
424 &user,
425 minijinja::context! {
426 user => minijinja::value::Serde(&user),
427 server => minijinja::value::Serde(&options.server),
428 server_link => format!(
429 "{}/server/{:08x}",
430 settings.app.url,
431 options.server.uuid_short,
432 )
433 },
434 )
435 .await;
436
437 Ok(result)
438 }
439}
440
441#[derive(ToSchema, Serialize, Deserialize, Validate, Default)]
442pub struct UpdateServerSubuserOptions {
443 #[garde(inner(custom(crate::permissions::validate_server_permissions)))]
444 pub permissions: Option<Vec<compact_str::CompactString>>,
445 #[garde(inner(
446 length(max = 1024),
447 inner(length(chars, min = 1, max = 255)),
448 custom(crate::utils::validate_ignored_files)
449 ))]
450 pub ignored_files: Option<Vec<compact_str::CompactString>>,
451}
452
453#[async_trait::async_trait]
454impl UpdatableModel for ServerSubuser {
455 type UpdateOptions = UpdateServerSubuserOptions;
456
457 fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
458 static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<ServerSubuser>> =
459 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
460
461 &UPDATE_LISTENERS
462 }
463
464 async fn update(
465 &mut self,
466 state: &crate::State,
467 options: Self::UpdateOptions,
468 ) -> Result<(), crate::database::DatabaseError> {
469 let mut transaction = state.database.write().begin().await?;
470
471 if let Err(err) = self
472 .update_with_transaction(state, options, &mut transaction)
473 .await
474 {
475 transaction.rollback().await?;
476 return Err(err);
477 }
478
479 transaction.commit().await?;
480
481 super::invalidate_cache_keys(&state.database, &self.cache_invalidation_keys()).await;
482
483 self.invalidate_wings_permissions(&state.database).await;
484
485 Ok(())
486 }
487
488 async fn update_with_transaction(
489 &mut self,
490 state: &crate::State,
491 mut options: Self::UpdateOptions,
492 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
493 ) -> Result<(), crate::database::DatabaseError> {
494 options.validate()?;
495
496 let mut query_builder = UpdateQueryBuilder::new("server_subusers");
497
498 self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
499 .await?;
500
501 query_builder
502 .set("permissions", options.permissions.as_ref())
503 .set("ignored_files", options.ignored_files.as_ref())
504 .where_eq("server_uuid", self.server.uuid)
505 .where_eq("user_uuid", self.user.uuid);
506
507 query_builder.execute(&mut **transaction).await?;
508
509 if let Some(permissions) = options.permissions {
510 self.permissions = permissions;
511 }
512 if let Some(ignored_files) = options.ignored_files {
513 self.ignored_files = ignored_files;
514 }
515
516 self.run_after_update_handlers(state, transaction).await?;
517
518 Ok(())
519 }
520}
521
522#[async_trait::async_trait]
523impl DeletableModel for ServerSubuser {
524 type DeleteOptions = ();
525
526 fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
527 static DELETE_LISTENERS: LazyLock<DeleteHandlerList<ServerSubuser>> =
528 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
529
530 &DELETE_LISTENERS
531 }
532
533 async fn delete(
534 &self,
535 state: &crate::State,
536 options: Self::DeleteOptions,
537 ) -> Result<(), anyhow::Error> {
538 let mut transaction = state.database.write().begin().await?;
539
540 if let Err(err) = self
541 .delete_with_transaction(state, options, &mut transaction)
542 .await
543 {
544 transaction.rollback().await?;
545 return Err(err);
546 }
547
548 transaction.commit().await?;
549
550 super::invalidate_cache_keys(&state.database, &self.cache_invalidation_keys()).await;
551
552 self.invalidate_wings_permissions(&state.database).await;
553
554 Ok(())
555 }
556
557 async fn delete_with_transaction(
558 &self,
559 state: &crate::State,
560 options: Self::DeleteOptions,
561 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
562 ) -> Result<(), anyhow::Error> {
563 self.run_delete_handlers(&options, state, transaction)
564 .await?;
565
566 sqlx::query(
567 r#"
568 DELETE FROM server_subusers
569 WHERE server_subusers.server_uuid = $1 AND server_subusers.user_uuid = $2
570 "#,
571 )
572 .bind(self.server.uuid)
573 .bind(self.user.uuid)
574 .execute(&mut **transaction)
575 .await?;
576
577 self.run_after_delete_handlers(&options, state, transaction)
578 .await?;
579
580 state
581 .mail
582 .send_template(
583 state,
584 "removed_from_server",
585 &self.user,
586 minijinja::context! {
587 user => minijinja::value::Serde(&self.user),
588 },
589 )
590 .await;
591
592 Ok(())
593 }
594}
595
596#[schema_extension_derive::extendible]
597#[init_args(ServerSubuser, crate::State)]
598#[hook_args(crate::State)]
599#[derive(ToSchema, Serialize)]
600#[schema(title = "ServerSubuser")]
601pub struct ApiServerSubuser {
602 pub user: super::user::ApiUser,
603
604 pub permissions: Vec<compact_str::CompactString>,
605 pub ignored_files: Vec<compact_str::CompactString>,
606
607 pub created: chrono::DateTime<chrono::Utc>,
608}