From 3bad0496820427efb5586b2e2bd5817ee9651719 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Wed, 17 Apr 2024 18:19:52 -0700 Subject: [PATCH] implement user group api --- bin/core/src/api/read/mod.rs | 5 + bin/core/src/api/read/user_group.rs | 61 +++++++ bin/core/src/api/write/mod.rs | 12 +- bin/core/src/api/write/permissions.rs | 137 +++++++-------- bin/core/src/api/write/user_group.rs | 174 ++++++++++++++++++++ bin/migrator/src/legacy/v0/mod.rs | 14 +- client/core/rs/src/api/read/mod.rs | 2 + client/core/rs/src/api/read/user_group.rs | 31 ++++ client/core/rs/src/api/write/mod.rs | 2 + client/core/rs/src/api/write/permissions.rs | 26 ++- client/core/rs/src/api/write/user_group.rs | 74 +++++++++ client/core/rs/src/entities/mod.rs | 4 - client/core/rs/src/entities/permission.rs | 38 ++++- 13 files changed, 473 insertions(+), 107 deletions(-) create mode 100644 bin/core/src/api/read/user_group.rs create mode 100644 bin/core/src/api/write/user_group.rs create mode 100644 client/core/rs/src/api/read/user_group.rs create mode 100644 client/core/rs/src/api/write/user_group.rs diff --git a/bin/core/src/api/read/mod.rs b/bin/core/src/api/read/mod.rs index a30393222..2bb574dc3 100644 --- a/bin/core/src/api/read/mod.rs +++ b/bin/core/src/api/read/mod.rs @@ -28,6 +28,7 @@ mod server; mod tag; mod update; mod user; +mod user_group; #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolver)] @@ -44,6 +45,10 @@ enum ReadRequest { ListApiKeys(ListApiKeys), ListUserPermissions(ListUserPermissions), + // ==== USER GROUP ==== + GetUserGroup(GetUserGroup), + ListUserGroups(ListUserGroups), + // ==== SEARCH ==== FindResources(FindResources), diff --git a/bin/core/src/api/read/user_group.rs b/bin/core/src/api/read/user_group.rs new file mode 100644 index 000000000..4b5566000 --- /dev/null +++ b/bin/core/src/api/read/user_group.rs @@ -0,0 +1,61 @@ +use std::str::FromStr; + +use anyhow::Context; +use async_trait::async_trait; +use monitor_client::{ + api::read::{ + GetUserGroup, GetUserGroupResponse, ListUserGroups, + ListUserGroupsResponse, + }, + entities::user::User, +}; +use mungos::{ + find::find_collect, + mongodb::bson::{doc, oid::ObjectId, Document}, +}; +use resolver_api::Resolve; + +use crate::{db::db_client, state::State}; + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetUserGroup { user_group }: GetUserGroup, + user: User, + ) -> anyhow::Result { + let mut filter = match ObjectId::from_str(&user_group) { + Ok(id) => doc! { "_id": id }, + Err(_) => doc! { "name": &user_group }, + }; + // Don't allow non admin users to get UserGroups they aren't a part of. + if !user.admin { + // Filter for only UserGroups which contain the users id + filter.insert("users", &user.id); + } + db_client() + .await + .user_groups + .find_one(filter, None) + .await + .context("failed to query db for user groups")? + .context("no UserGroup found with given name or id") + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + ListUserGroups {}: ListUserGroups, + user: User, + ) -> anyhow::Result { + let mut filter = Document::new(); + if !user.admin { + filter.insert("users", &user.id); + } + find_collect(&db_client().await.user_groups, filter, None) + .await + .context("failed to query db for UserGroups") + } +} diff --git a/bin/core/src/api/write/mod.rs b/bin/core/src/api/write/mod.rs index 7b59a897d..1ada8630b 100644 --- a/bin/core/src/api/write/mod.rs +++ b/bin/core/src/api/write/mod.rs @@ -25,6 +25,7 @@ mod repo; mod server; mod tag; mod user; +mod user_group; #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolver)] @@ -44,9 +45,16 @@ enum WriteRequest { CreateServiceUser(CreateServiceUser), UpdateServiceUserDescription(UpdateServiceUserDescription), + // ==== USER GROUP ==== + CreateUserGroup(CreateUserGroup), + RenameUserGroup(RenameUserGroup), + DeleteUserGroup(DeleteUserGroup), + AddUserToUserGroup(AddUserToUserGroup), + RemoveUserFromUserGroup(RemoveUserFromUserGroup), + // ==== PERMISSIONS ==== - UpdateUserPerimissions(UpdateUserPermissions), - UpdateUserPermissionsOnTarget(UpdateUserPermissionsOnTarget), + UpdateUserBasePermissions(UpdateUserBasePermissions), + UpdatePermissionOnTarget(UpdatePermissionOnTarget), // ==== DESCRIPTION ==== UpdateDescription(UpdateDescription), diff --git a/bin/core/src/api/write/permissions.rs b/bin/core/src/api/write/permissions.rs index 8688555d4..d4966d95e 100644 --- a/bin/core/src/api/write/permissions.rs +++ b/bin/core/src/api/write/permissions.rs @@ -2,13 +2,10 @@ use anyhow::{anyhow, Context}; use async_trait::async_trait; use monitor_client::{ api::write::{ - UpdateUserPermissions, UpdateUserPermissionsOnTarget, - }, - entities::{ - update::{ResourceTarget, Update}, - user::User, - Operation, + UpdatePermissionOnTarget, UpdatePermissionOnTargetResponse, + UpdateUserBasePermissions, UpdateUserBasePermissionsResponse, }, + entities::{permission::UserTarget, user::User}, }; use mungos::{ by_id::{find_one_by_id, update_one_by_id}, @@ -19,28 +16,21 @@ use mungos::{ }; use resolver_api::Resolve; -use crate::{ - db::db_client, - helpers::{ - query::get_user, - update::{add_update, make_update}, - }, - state::State, -}; +use crate::{db::db_client, helpers::query::get_user, state::State}; #[async_trait] -impl Resolve for State { - #[instrument(name = "UpdateUserPermissions", skip(self, admin))] +impl Resolve for State { + #[instrument(name = "UpdateUserBasePermissions", skip(self, admin))] async fn resolve( &self, - UpdateUserPermissions { + UpdateUserBasePermissions { user_id, enabled, create_servers, create_builds, - }: UpdateUserPermissions, + }: UpdateUserBasePermissions, admin: User, - ) -> anyhow::Result { + ) -> anyhow::Result { if !admin.admin { return Err(anyhow!("this method is admin only")); } @@ -72,74 +62,69 @@ impl Resolve for State { ) .await?; - let mut update = make_update( - ResourceTarget::System("system".to_string()), - Operation::UpdateUserPermissions, - &admin, - ); - update.push_simple_log("modify user enabled", format!( - "update permissions for {} ({})\nenabled: {enabled:?}\ncreate servers: {create_servers:?}\ncreate builds: {create_builds:?}", - user.username, - user.id, - )); - update.finalize(); - update.id = add_update(update.clone()).await?; - Ok(update) + Ok(UpdateUserBasePermissionsResponse {}) } } #[async_trait] -impl Resolve for State { - #[instrument( - name = "UpdateUserPermissionsOnTarget", - skip(self, admin) - )] +impl Resolve for State { + #[instrument(name = "UpdatePermissionOnTarget", skip(self, admin))] async fn resolve( &self, - UpdateUserPermissionsOnTarget { - user_id, + UpdatePermissionOnTarget { + user_target, + resource_target, permission, - target, - }: UpdateUserPermissionsOnTarget, + }: UpdatePermissionOnTarget, admin: User, - ) -> anyhow::Result { + ) -> anyhow::Result { if !admin.admin { return Err(anyhow!("this method is admin only")); } - let user = get_user(&user_id).await?; - if user.admin { - return Err(anyhow!( - "cannot use this method to update other admins permissions" - )); + + // Some extra checks if user target is an actual User + if let UserTarget::User(user_id) = &user_target { + let user = get_user(user_id).await?; + if user.admin { + return Err(anyhow!( + "cannot use this method to update other admins permissions" + )); + } + if !user.enabled { + return Err(anyhow!("user not enabled")); + } } - if !user.enabled { - return Err(anyhow!("user not enabled")); - } - let (variant, id) = target.extract_variant_id(); - db_client().await.permissions.update_one( - doc! { "user_id": &user.id, "target.type": variant.as_ref(), "target.id": id }, - doc! { - "$set": { - "user_id": &user.id, - "target.type": variant.as_ref(), - "target.id": id, - "level": permission.as_ref(), - } - }, - UpdateOptions::builder().upsert(true).build() - ).await?; - let log_text = format!( - "user {} given {} permissions on {target:?}", - user.username, permission, - ); - let mut update = make_update( - target, - Operation::UpdateUserPermissionsOnTarget, - &admin, - ); - update.push_simple_log("modify permissions", log_text); - update.finalize(); - update.id = add_update(update.clone()).await?; - Ok(update) + + let (user_target_variant, user_target_id) = + user_target.extract_variant_id(); + let (resource_variant, resource_id) = + resource_target.extract_variant_id(); + let (user_target_variant, resource_variant) = + (user_target_variant.as_ref(), resource_variant.as_ref()); + + db_client() + .await + .permissions + .update_one( + doc! { + "user_target.type": user_target_variant, + "user_target.id": &user_target_id, + "resource_target.type": resource_variant, + "resource_target.id": &resource_id + }, + doc! { + "$set": { + "user_target.type": user_target_variant, + "user_target.id": user_target_id, + "resource_target.type": resource_variant, + "resource_target.id": resource_id, + "level": permission.as_ref(), + } + }, + UpdateOptions::builder().upsert(true).build(), + ) + .await?; + + Ok(UpdatePermissionOnTargetResponse {}) } } diff --git a/bin/core/src/api/write/user_group.rs b/bin/core/src/api/write/user_group.rs new file mode 100644 index 000000000..561c35b82 --- /dev/null +++ b/bin/core/src/api/write/user_group.rs @@ -0,0 +1,174 @@ +use std::str::FromStr; + +use anyhow::{anyhow, Context}; +use axum::async_trait; +use monitor_client::{ + api::write::{ + AddUserToUserGroup, CreateUserGroup, DeleteUserGroup, + RemoveUserFromUserGroup, RenameUserGroup, + }, + entities::{monitor_timestamp, user::User, user_group::UserGroup}, +}; +use mungos::{ + by_id::{delete_one_by_id, find_one_by_id, update_one_by_id}, + mongodb::bson::{doc, oid::ObjectId}, +}; +use resolver_api::Resolve; + +use crate::{db::db_client, state::State}; + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + CreateUserGroup { name }: CreateUserGroup, + admin: User, + ) -> anyhow::Result { + if !admin.admin { + return Err(anyhow!("This call is admin-only")); + } + let user_group = UserGroup { + id: Default::default(), + users: Default::default(), + updated_at: monitor_timestamp(), + name, + }; + let db = db_client().await; + let id = db + .user_groups + .insert_one(user_group, None) + .await + .context("failed to create UserGroup on db")? + .inserted_id + .as_object_id() + .context("inserted id is not ObjectId")? + .to_string(); + find_one_by_id(&db.user_groups, &id) + .await + .context("failed to query db for user groups")? + .context("user group at id not found") + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + RenameUserGroup { id, name }: RenameUserGroup, + admin: User, + ) -> anyhow::Result { + if !admin.admin { + return Err(anyhow!("This call is admin-only")); + } + let db = db_client().await; + update_one_by_id( + &db.user_groups, + &id, + doc! { "$set": { "name": name } }, + None, + ) + .await + .context("failed to rename UserGroup on db")?; + find_one_by_id(&db.user_groups, &id) + .await + .context("failed to query db for UserGroups")? + .context("no user group with given id") + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + DeleteUserGroup { id }: DeleteUserGroup, + admin: User, + ) -> anyhow::Result { + if !admin.admin { + return Err(anyhow!("This call is admin-only")); + } + let db = db_client().await; + let ug = find_one_by_id(&db.user_groups, &id) + .await + .context("failed to query db for UserGroups")? + .context("no UserGroup found with given id")?; + delete_one_by_id(&db.user_groups, &id, None) + .await + .context("failed to delete UserGroup from db")?; + db.permissions + .delete_many(doc! { + "user_target.type": "UserGroup", + "user_target.id": id, + }, None) + .await + .context("failed to clean up UserGroups permissions. User Group has been deleted")?; + Ok(ug) + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + AddUserToUserGroup { + user_group, + user_id, + }: AddUserToUserGroup, + admin: User, + ) -> anyhow::Result { + if !admin.admin { + return Err(anyhow!("This call is admin-only")); + } + let filter = match ObjectId::from_str(&user_group) { + Ok(id) => doc! { "_id": id }, + Err(_) => doc! { "name": &user_group }, + }; + let db = db_client().await; + db.user_groups + .update_one( + filter.clone(), + doc! { "$push": { "users": user_id } }, + None, + ) + .await + .context("failed to add user to group on db")?; + db.user_groups + .find_one(filter, None) + .await + .context("failed to query db for UserGroups")? + .context("no user group with given id") + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + RemoveUserFromUserGroup { + user_group, + user_id, + }: RemoveUserFromUserGroup, + admin: User, + ) -> anyhow::Result { + if !admin.admin { + return Err(anyhow!("This call is admin-only")); + } + let filter = match ObjectId::from_str(&user_group) { + Ok(id) => doc! { "_id": id }, + Err(_) => doc! { "name": &user_group }, + }; + let db = db_client().await; + db.user_groups + .update_one( + filter.clone(), + doc! { "$pull": { "users": user_id } }, + None, + ) + .await + .context("failed to add user to group on db")?; + db.user_groups + .find_one(filter, None) + .await + .context("failed to query db for UserGroups")? + .context("no user group with given id") + } +} diff --git a/bin/migrator/src/legacy/v0/mod.rs b/bin/migrator/src/legacy/v0/mod.rs index 801ca8b3e..523b4563a 100644 --- a/bin/migrator/src/legacy/v0/mod.rs +++ b/bin/migrator/src/legacy/v0/mod.rs @@ -193,16 +193,10 @@ impl From for monitor_client::entities::Operation { Operation::CreateGroup => None, Operation::UpdateGroup => None, Operation::DeleteGroup => None, - Operation::ModifyUserEnabled => UpdateUserPermissions, - Operation::ModifyUserCreateServerPermissions => { - UpdateUserPermissions - } - Operation::ModifyUserCreateBuildPermissions => { - UpdateUserPermissions - } - Operation::ModifyUserPermissions => { - UpdateUserPermissionsOnTarget - } + Operation::ModifyUserEnabled => None, + Operation::ModifyUserCreateServerPermissions => None, + Operation::ModifyUserCreateBuildPermissions => None, + Operation::ModifyUserPermissions => None, Operation::AutoBuild => RunBuild, Operation::AutoPull => PullRepo, } diff --git a/client/core/rs/src/api/read/mod.rs b/client/core/rs/src/api/read/mod.rs index a6e76e941..1aa7e426e 100644 --- a/client/core/rs/src/api/read/mod.rs +++ b/client/core/rs/src/api/read/mod.rs @@ -15,6 +15,7 @@ mod search; mod server; mod tag; mod update; +mod user_group; pub use alert::*; pub use alerter::*; @@ -28,6 +29,7 @@ pub use search::*; pub use server::*; pub use tag::*; pub use update::*; +pub use user_group::*; use crate::entities::{api_key::ApiKey, user::User, Timelength}; diff --git a/client/core/rs/src/api/read/user_group.rs b/client/core/rs/src/api/read/user_group.rs new file mode 100644 index 000000000..9ce209017 --- /dev/null +++ b/client/core/rs/src/api/read/user_group.rs @@ -0,0 +1,31 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::user_group::UserGroup; + +use super::MonitorReadRequest; + +#[typeshare] +#[derive(Debug, Clone, Serialize, Deserialize, Request, EmptyTraits)] +#[empty_traits(MonitorReadRequest)] +#[response(GetUserGroupResponse)] +pub struct GetUserGroup { + /// Name or Id + pub user_group: String, +} + +#[typeshare] +pub type GetUserGroupResponse = UserGroup; + +// + +#[typeshare] +#[derive(Debug, Clone, Serialize, Deserialize, Request, EmptyTraits)] +#[empty_traits(MonitorReadRequest)] +#[response(ListUserGroupsResponse)] +pub struct ListUserGroups {} + +#[typeshare] +pub type ListUserGroupsResponse = Vec; \ No newline at end of file diff --git a/client/core/rs/src/api/write/mod.rs b/client/core/rs/src/api/write/mod.rs index d2edc6aad..bef3093c6 100644 --- a/client/core/rs/src/api/write/mod.rs +++ b/client/core/rs/src/api/write/mod.rs @@ -11,6 +11,7 @@ mod repo; mod server; mod tags; mod user; +mod user_group; pub use alerter::*; pub use api_key::*; @@ -25,5 +26,6 @@ pub use repo::*; pub use server::*; pub use tags::*; pub use user::*; +pub use user_group::*; pub trait MonitorWriteRequest: resolver_api::HasResponse {} diff --git a/client/core/rs/src/api/write/permissions.rs b/client/core/rs/src/api/write/permissions.rs index 8e4e977f9..882d84b84 100644 --- a/client/core/rs/src/api/write/permissions.rs +++ b/client/core/rs/src/api/write/permissions.rs @@ -4,8 +4,8 @@ use serde::{Deserialize, Serialize}; use typeshare::typeshare; use crate::entities::{ - permission::PermissionLevel, - update::{ResourceTarget, Update}, + permission::{PermissionLevel, UserTarget}, + update::ResourceTarget, }; use super::MonitorWriteRequest; @@ -15,22 +15,32 @@ use super::MonitorWriteRequest; Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, )] #[empty_traits(MonitorWriteRequest)] -#[response(Update)] -pub struct UpdateUserPermissionsOnTarget { - pub user_id: String, +#[response(UpdatePermissionOnTargetResponse)] +pub struct UpdatePermissionOnTarget { + pub user_target: UserTarget, + pub resource_target: ResourceTarget, pub permission: PermissionLevel, - pub target: ResourceTarget, } +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct UpdatePermissionOnTargetResponse {} + +// + #[typeshare] #[derive( Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, )] #[empty_traits(MonitorWriteRequest)] -#[response(Update)] -pub struct UpdateUserPermissions { +#[response(UpdateUserBasePermissionsResponse)] +pub struct UpdateUserBasePermissions { pub user_id: String, pub enabled: Option, pub create_servers: Option, pub create_builds: Option, } + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct UpdateUserBasePermissionsResponse {} diff --git a/client/core/rs/src/api/write/user_group.rs b/client/core/rs/src/api/write/user_group.rs new file mode 100644 index 000000000..4728fbc77 --- /dev/null +++ b/client/core/rs/src/api/write/user_group.rs @@ -0,0 +1,74 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::user_group::UserGroup; + +use super::MonitorWriteRequest; + +/// Admin only +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(UserGroup)] +pub struct CreateUserGroup { + /// The name to assign to the new UserGroup + pub name: String, +} + +/// Admin only +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(UserGroup)] +pub struct RenameUserGroup { + /// The id of the UserGroup + pub id: String, + /// The new name for the UserGroup + pub name: String, +} + +/// Admin only +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(UserGroup)] +pub struct DeleteUserGroup { + /// The id of the UserGroup + pub id: String, +} + +/// Admin only +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(UserGroup)] +pub struct AddUserToUserGroup { + /// The name or id of UserGroup that user should be added to. + pub user_group: String, + /// The id of the user to add + pub user_id: String, +} + +/// Admin only +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(UserGroup)] +pub struct RemoveUserFromUserGroup { + /// The name or id of UserGroup that user should be removed from. + pub user_group: String, + /// The id of the user to remove + pub user_id: String, +} diff --git a/client/core/rs/src/entities/mod.rs b/client/core/rs/src/entities/mod.rs index 7dc92b2dd..30c974195 100644 --- a/client/core/rs/src/entities/mod.rs +++ b/client/core/rs/src/entities/mod.rs @@ -375,10 +375,6 @@ pub enum Operation { UpdateProcedure, DeleteProcedure, RunProcedure, - - // user - UpdateUserPermissions, - UpdateUserPermissionsOnTarget, } #[typeshare] diff --git a/client/core/rs/src/entities/permission.rs b/client/core/rs/src/entities/permission.rs index 25ec7452d..0e21f4957 100644 --- a/client/core/rs/src/entities/permission.rs +++ b/client/core/rs/src/entities/permission.rs @@ -1,3 +1,4 @@ +use derive_variants::EnumVariants; use mongo_indexed::derive::MongoIndexed; use mungos::mongodb::bson::{ doc, serde_helpers::hex_string_as_object_id, Document, @@ -11,10 +12,17 @@ use super::{update::ResourceTarget, MongoId}; /// Representation of a User or UserGroups permission on a resource. #[typeshare] #[derive(Debug, Clone, Serialize, Deserialize, MongoIndexed)] -// To query for all permissions on a target -#[doc_index(doc! { "target.type": 1, "target.id": 1 })] -// Only one permission allowed per user / target -#[unique_doc_index(doc! { "user_id": 1, "target.type": 1, "target.id": 1 })] +// To query for all permissions on user target +#[doc_index(doc! { "user_target.type": 1, "user_target.id": 1 })] +// To query for all permissions on a resource target +#[doc_index(doc! { "resource_target.type": 1, "resource_target.id": 1 })] +// Only one permission allowed per user / resource target +#[unique_doc_index(doc! { + "user_target.type": 1, + "user_target.id": 1, + "target.type": 1, + "target.id": 1 +})] pub struct Permission { /// The id of the permission document #[serde( @@ -24,8 +32,7 @@ pub struct Permission { with = "hex_string_as_object_id" )] pub id: MongoId, - /// Attached user - #[index] + /// The target User / UserGroup pub user_target: UserTarget, /// The target resource pub resource_target: ResourceTarget, @@ -35,7 +42,15 @@ pub struct Permission { } #[typeshare] -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, EnumVariants)] +#[variant_derive( + Debug, + Clone, + Copy, + Serialize, + Deserialize, + AsRefStr +)] #[serde(tag = "type", content = "id")] pub enum UserTarget { /// User Id @@ -44,6 +59,15 @@ pub enum UserTarget { UserGroup(String), } +impl UserTarget { + pub fn extract_variant_id(self) -> (UserTargetVariant, String) { + match self { + UserTarget::User(id) => (UserTargetVariant::User, id), + UserTarget::UserGroup(id) => (UserTargetVariant::UserGroup, id), + } + } +} + /// The levels of permission that a User or UserGroup can have on a resource. #[typeshare] #[derive(