implement user group api

This commit is contained in:
mbecker20
2024-04-17 18:19:52 -07:00
parent 5352afee06
commit 3bad049682
13 changed files with 473 additions and 107 deletions

View File

@@ -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),

View File

@@ -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<GetUserGroup, User> for State {
async fn resolve(
&self,
GetUserGroup { user_group }: GetUserGroup,
user: User,
) -> anyhow::Result<GetUserGroupResponse> {
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<ListUserGroups, User> for State {
async fn resolve(
&self,
ListUserGroups {}: ListUserGroups,
user: User,
) -> anyhow::Result<ListUserGroupsResponse> {
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")
}
}

View File

@@ -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),

View File

@@ -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<UpdateUserPermissions, User> for State {
#[instrument(name = "UpdateUserPermissions", skip(self, admin))]
impl Resolve<UpdateUserBasePermissions, User> 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<Update> {
) -> anyhow::Result<UpdateUserBasePermissionsResponse> {
if !admin.admin {
return Err(anyhow!("this method is admin only"));
}
@@ -72,74 +62,69 @@ impl Resolve<UpdateUserPermissions, User> 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<UpdateUserPermissionsOnTarget, User> for State {
#[instrument(
name = "UpdateUserPermissionsOnTarget",
skip(self, admin)
)]
impl Resolve<UpdatePermissionOnTarget, User> 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<Update> {
) -> anyhow::Result<UpdatePermissionOnTargetResponse> {
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 {})
}
}

View File

@@ -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<CreateUserGroup, User> for State {
async fn resolve(
&self,
CreateUserGroup { name }: CreateUserGroup,
admin: User,
) -> anyhow::Result<UserGroup> {
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<RenameUserGroup, User> for State {
async fn resolve(
&self,
RenameUserGroup { id, name }: RenameUserGroup,
admin: User,
) -> anyhow::Result<UserGroup> {
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<DeleteUserGroup, User> for State {
async fn resolve(
&self,
DeleteUserGroup { id }: DeleteUserGroup,
admin: User,
) -> anyhow::Result<UserGroup> {
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<AddUserToUserGroup, User> for State {
async fn resolve(
&self,
AddUserToUserGroup {
user_group,
user_id,
}: AddUserToUserGroup,
admin: User,
) -> anyhow::Result<UserGroup> {
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<RemoveUserFromUserGroup, User> for State {
async fn resolve(
&self,
RemoveUserFromUserGroup {
user_group,
user_id,
}: RemoveUserFromUserGroup,
admin: User,
) -> anyhow::Result<UserGroup> {
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")
}
}

View File

@@ -193,16 +193,10 @@ impl From<Operation> 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,
}

View File

@@ -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};

View File

@@ -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<UserGroup>;

View File

@@ -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 {}

View File

@@ -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<bool>,
pub create_servers: Option<bool>,
pub create_builds: Option<bool>,
}
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct UpdateUserBasePermissionsResponse {}

View File

@@ -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,
}

View File

@@ -375,10 +375,6 @@ pub enum Operation {
UpdateProcedure,
DeleteProcedure,
RunProcedure,
// user
UpdateUserPermissions,
UpdateUserPermissionsOnTarget,
}
#[typeshare]

View File

@@ -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(