api check UserGroup for access

This commit is contained in:
mbecker20
2024-04-17 02:23:41 -07:00
parent a83dedbcd0
commit 5352afee06
30 changed files with 375 additions and 252 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ use crate::{
jwt::jwt_client,
},
config::core_config,
helpers::get_user,
helpers::query::get_user,
state::State,
};
+4 -3
View File
@@ -43,9 +43,10 @@ use crate::{
config::core_config,
db::db_client,
helpers::{
add_update, channel::build_cancel_channel, get_deployment_state,
make_update, periphery_client, resource::StateResource,
update_update,
channel::build_cancel_channel,
query::get_deployment_state, periphery_client,
resource::StateResource,
update::{add_update, make_update, update_update},
},
state::{action_states, State},
};
+4 -3
View File
@@ -22,8 +22,9 @@ use serror::serialize_error_pretty;
use crate::{
db::db_client,
helpers::{
add_update, get_server_with_status, make_update,
periphery_client, resource::StateResource, update_update,
query::get_server_with_status, periphery_client,
resource::StateResource,
update::{add_update, make_update, update_update},
},
monitor::update_cache_for_server,
state::{action_states, State},
@@ -352,7 +353,7 @@ impl Resolve<StopAllContainers, User> for State {
)
.await
.context("failed to find deployments on server")?;
let server_id = server.id.clone();
let inner = || async move {
+3 -2
View File
@@ -12,8 +12,9 @@ use tokio::sync::Mutex;
use crate::{
helpers::{
add_update, make_update, procedure::execute_procedure,
resource::StateResource, update_update,
procedure::execute_procedure,
resource::StateResource,
update::{add_update, make_update, update_update},
},
state::State,
};
+3 -2
View File
@@ -22,8 +22,9 @@ use serror::serialize_error_pretty;
use crate::{
db::db_client,
helpers::{
add_update, periphery_client, resource::StateResource,
update_update,
periphery_client,
resource::StateResource,
update::{add_update, update_update},
},
state::{action_states, State},
};
+3 -2
View File
@@ -17,8 +17,9 @@ use serror::serialize_error_pretty;
use crate::{
helpers::{
add_update, make_update, periphery_client,
resource::StateResource, update_update,
periphery_client,
resource::StateResource,
update::{add_update, make_update, update_update},
},
state::{action_states, State},
};
+1 -1
View File
@@ -7,7 +7,7 @@ use monitor_client::{
use mungos::find::find_collect;
use resolver_api::Resolve;
use crate::{db::db_client, helpers::get_tag, state::State};
use crate::{db::db_client, helpers::query::get_tag, state::State};
#[async_trait]
impl Resolve<GetTag, User> for State {
+1 -1
View File
@@ -23,7 +23,7 @@ use resolver_api::Resolve;
use crate::{
db::db_client,
helpers::{
add_update, create_permission, make_update,
create_permission, update::{add_update, make_update},
remove_from_recently_viewed,
resource::{delete_all_permissions_on_resource, StateResource},
},
+1 -1
View File
@@ -12,7 +12,7 @@ use mungos::{by_id::find_one_by_id, mongodb::bson::doc};
use resolver_api::Resolve;
use crate::{
auth::random_string, db::db_client, helpers::get_user, state::State,
auth::random_string, db::db_client, helpers::query::get_user, state::State,
};
const SECRET_LENGTH: usize = 40;
+2 -2
View File
@@ -24,10 +24,10 @@ use resolver_api::Resolve;
use crate::{
db::db_client,
helpers::{
add_update, create_permission, empty_or_only_spaces, make_update,
create_permission, empty_or_only_spaces,
remove_from_recently_viewed,
resource::{delete_all_permissions_on_resource, StateResource},
update_update,
update::{add_update, make_update, update_update},
},
state::{action_states, State},
};
+2 -2
View File
@@ -25,9 +25,9 @@ use resolver_api::Resolve;
use crate::{
db::db_client,
helpers::{
add_update, create_permission, make_update,
remove_from_recently_viewed,
create_permission, remove_from_recently_viewed,
resource::{delete_all_permissions_on_resource, StateResource},
update::{add_update, make_update},
},
state::State,
};
+3 -3
View File
@@ -30,11 +30,11 @@ use resolver_api::Resolve;
use crate::{
db::db_client,
helpers::{
add_update, create_permission, empty_or_only_spaces,
get_deployment_state, make_update, periphery_client,
create_permission, empty_or_only_spaces, periphery_client,
query::get_deployment_state,
remove_from_recently_viewed,
resource::{delete_all_permissions_on_resource, StateResource},
update_update,
update::{add_update, make_update, update_update},
},
state::{action_states, State},
};
+1 -1
View File
@@ -13,7 +13,7 @@ use resolver_api::Resolve;
use crate::{
cloud::aws::launch_ec2_instance,
helpers::{add_update, make_update, update_update},
helpers::update::{add_update, make_update, update_update},
state::State,
};
+8 -2
View File
@@ -21,7 +21,10 @@ use resolver_api::Resolve;
use crate::{
db::db_client,
helpers::{add_update, get_user, make_update},
helpers::{
query::get_user,
update::{add_update, make_update},
},
state::State,
};
@@ -87,7 +90,10 @@ impl Resolve<UpdateUserPermissions, User> for State {
#[async_trait]
impl Resolve<UpdateUserPermissionsOnTarget, User> for State {
#[instrument(name = "UpdateUserPermissionsOnTarget", skip(self, admin))]
#[instrument(
name = "UpdateUserPermissionsOnTarget",
skip(self, admin)
)]
async fn resolve(
&self,
UpdateUserPermissionsOnTarget {
+2 -3
View File
@@ -26,10 +26,9 @@ use resolver_api::Resolve;
use crate::{
db::db_client,
helpers::{
add_update, create_permission, make_update,
remove_from_recently_viewed,
create_permission, remove_from_recently_viewed,
resource::{delete_all_permissions_on_resource, StateResource},
update_update,
update::{add_update, make_update, update_update},
},
state::{action_states, State},
};
+2 -3
View File
@@ -26,10 +26,9 @@ use serror::serialize_error_pretty;
use crate::{
db::db_client,
helpers::{
add_update, create_permission, make_update, periphery_client,
remove_from_recently_viewed,
create_permission, periphery_client, remove_from_recently_viewed,
resource::{delete_all_permissions_on_resource, StateResource},
update_update,
update::{add_update, make_update, update_update},
},
state::{action_states, State},
};
+2 -4
View File
@@ -25,12 +25,10 @@ use serror::serialize_error_pretty;
use crate::{
db::db_client,
helpers::{
add_update,
cache::server_status_cache,
create_permission, make_update, periphery_client,
remove_from_recently_viewed,
create_permission, periphery_client, remove_from_recently_viewed,
resource::{delete_all_permissions_on_resource, StateResource},
update_update,
update::{add_update, make_update, update_update},
},
monitor::update_cache_for_server,
state::{action_states, State},
+1 -1
View File
@@ -22,7 +22,7 @@ use resolver_api::Resolve;
use crate::{
db::db_client,
helpers::{get_tag, get_tag_check_owner, resource::StateResource},
helpers::{query::{get_tag, get_tag_check_owner}, resource::StateResource},
state::State,
};
+1 -1
View File
@@ -20,7 +20,7 @@ use mungos::{
};
use resolver_api::Resolve;
use crate::{db::db_client, helpers::get_user, state::State};
use crate::{db::db_client, helpers::query::get_user, state::State};
const RECENTLY_VIEWED_MAX: usize = 10;
+1 -1
View File
@@ -11,7 +11,7 @@ use rand::{distributions::Alphanumeric, thread_rng, Rng};
use serde::Deserialize;
use serror::AuthError;
use crate::{db::db_client, helpers::get_user};
use crate::{db::db_client, helpers::query::get_user};
use self::jwt::{jwt_client, JwtClaims};
+3
View File
@@ -13,6 +13,7 @@ use monitor_client::entities::{
tag::Tag,
update::Update,
user::User,
user_group::UserGroup,
};
use mungos::{
init::MongoBuilder,
@@ -35,6 +36,7 @@ pub async fn db_client() -> &'static DbClient {
pub struct DbClient {
pub users: Collection<User>,
pub user_groups: Collection<UserGroup>,
pub permissions: Collection<Permission>,
pub api_keys: Collection<ApiKey>,
pub tags: Collection<Tag>,
@@ -90,6 +92,7 @@ impl DbClient {
let client = DbClient {
users: User::collection(&db, true).await?,
user_groups: UserGroup::collection(&db, true).await?,
permissions: Permission::collection(&db, true).await?,
api_keys: ApiKey::collection(&db, true).await?,
tags: Tag::collection(&db, true).await?,
+11 -182
View File
@@ -1,32 +1,25 @@
use std::{str::FromStr, time::Duration};
use std::time::Duration;
use anyhow::{anyhow, Context};
use monitor_client::entities::{
deployment::{Deployment, DockerContainerState},
monitor_timestamp,
permission::{Permission, PermissionLevel},
server::{Server, ServerStatus},
tag::Tag,
update::{ResourceTarget, Update, UpdateListItem},
user::{admin_service_user, User},
Operation,
permission::{Permission, PermissionLevel, UserTarget},
server::Server,
update::ResourceTarget,
user::User,
};
use mungos::{
by_id::{find_one_by_id, update_one_by_id},
mongodb::bson::{doc, oid::ObjectId, to_document},
};
use periphery_client::{api, PeripheryClient};
use mungos::mongodb::bson::doc;
use periphery_client::PeripheryClient;
use rand::{thread_rng, Rng};
use crate::{config::core_config, db::db_client};
use self::{channel::update_channel, resource::StateResource};
pub mod alert;
pub mod cache;
pub mod channel;
pub mod procedure;
pub mod query;
pub mod resource;
pub mod update;
pub fn empty_or_only_spaces(word: &str) -> bool {
if word.is_empty() {
@@ -44,170 +37,6 @@ pub fn random_duration(min_ms: u64, max_ms: u64) -> Duration {
Duration::from_millis(thread_rng().gen_range(min_ms..max_ms))
}
pub fn make_update(
target: impl Into<ResourceTarget>,
operation: Operation,
user: &User,
) -> Update {
Update {
start_ts: monitor_timestamp(),
target: target.into(),
operation,
operator: user.id.clone(),
success: true,
..Default::default()
}
}
#[instrument(level = "debug")]
pub async fn get_user(user_id: &str) -> anyhow::Result<User> {
if let Some(user) = admin_service_user(user_id) {
return Ok(user);
}
find_one_by_id(&db_client().await.users, user_id)
.await
.context("failed to query mongo for user")?
.with_context(|| format!("no user found with id {user_id}"))
}
#[instrument(level = "debug")]
pub async fn get_server_with_status(
server_id_or_name: &str,
) -> anyhow::Result<(Server, ServerStatus)> {
let server = Server::get_resource(server_id_or_name).await?;
if !server.config.enabled {
return Ok((server, ServerStatus::Disabled));
}
let status =
match periphery_client(&server)?.request(api::GetHealth {}).await
{
Ok(_) => ServerStatus::Ok,
Err(_) => ServerStatus::NotOk,
};
Ok((server, status))
}
#[instrument(level = "debug")]
pub async fn get_deployment_state(
deployment: &Deployment,
) -> anyhow::Result<DockerContainerState> {
if deployment.config.server_id.is_empty() {
return Ok(DockerContainerState::NotDeployed);
}
let (server, status) =
get_server_with_status(&deployment.config.server_id).await?;
if status != ServerStatus::Ok {
return Ok(DockerContainerState::Unknown);
}
let container = periphery_client(&server)?
.request(api::container::GetContainerList {})
.await?
.into_iter()
.find(|container| container.name == deployment.name);
let state = match container {
Some(container) => container.state,
None => DockerContainerState::NotDeployed,
};
Ok(state)
}
// TAG
#[instrument(level = "debug")]
pub async fn get_tag(id_or_name: &str) -> anyhow::Result<Tag> {
let query = match ObjectId::from_str(id_or_name) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": id_or_name },
};
db_client()
.await
.tags
.find_one(query, None)
.await
.context("failed to query mongo for tag")?
.with_context(|| format!("no tag found matching {id_or_name}"))
}
#[instrument(level = "debug")]
pub async fn get_tag_check_owner(
id_or_name: &str,
user: &User,
) -> anyhow::Result<Tag> {
let tag = get_tag(id_or_name).await?;
if user.admin || tag.owner == user.id {
return Ok(tag);
}
Err(anyhow!("user must be tag owner or admin"))
}
// UPDATE
#[instrument(level = "debug")]
async fn update_list_item(
update: Update,
) -> anyhow::Result<UpdateListItem> {
let username = if User::is_service_user(&update.operator) {
update.operator.clone()
} else {
find_one_by_id(&db_client().await.users, &update.operator)
.await
.context("failed to query mongo for user")?
.with_context(|| {
format!("no user found with id {}", update.operator)
})?
.username
};
let update = UpdateListItem {
id: update.id,
operation: update.operation,
start_ts: update.start_ts,
success: update.success,
operator: update.operator,
target: update.target,
status: update.status,
version: update.version,
username,
};
Ok(update)
}
#[instrument(level = "debug")]
async fn send_update(update: UpdateListItem) -> anyhow::Result<()> {
update_channel().sender.lock().await.send(update)?;
Ok(())
}
#[instrument(level = "debug")]
pub async fn add_update(
mut update: Update,
) -> anyhow::Result<String> {
update.id = db_client()
.await
.updates
.insert_one(&update, None)
.await
.context("failed to insert update into db")?
.inserted_id
.as_object_id()
.context("inserted_id is not object id")?
.to_string();
let id = update.id.clone();
let update = update_list_item(update).await?;
let _ = send_update(update).await;
Ok(id)
}
#[instrument(level = "debug")]
pub async fn update_update(update: Update) -> anyhow::Result<()> {
update_one_by_id(&db_client().await.updates, &update.id, mungos::update::Update::Set(to_document(&update)?), None)
.await
.context("failed to update the update on db. the update build process was deleted")?;
let update = update_list_item(update).await?;
let _ = send_update(update).await;
Ok(())
}
#[instrument]
pub async fn remove_from_recently_viewed<T>(
resource: T,
@@ -275,8 +104,8 @@ pub async fn create_permission<T>(
.insert_one(
Permission {
id: Default::default(),
user_id: user.id.clone(),
target: target.clone(),
user_target: UserTarget::User(user.id.clone()),
resource_target: target.clone(),
level,
},
None,
+1 -1
View File
@@ -16,7 +16,7 @@ use tokio::sync::Mutex;
use crate::state::State;
use super::update_update;
use super::update::update_update;
#[instrument]
pub async fn execute_procedure(
+121
View File
@@ -0,0 +1,121 @@
use std::str::FromStr;
use anyhow::{anyhow, Context};
use monitor_client::entities::{
deployment::{Deployment, DockerContainerState},
server::{Server, ServerStatus},
tag::Tag,
user::{admin_service_user, User},
};
use mungos::{
by_id::find_one_by_id,
find::find_collect,
mongodb::bson::{doc, oid::ObjectId},
};
use crate::db::db_client;
use super::resource::StateResource;
#[instrument(level = "debug")]
pub async fn get_user(user_id: &str) -> anyhow::Result<User> {
if let Some(user) = admin_service_user(user_id) {
return Ok(user);
}
find_one_by_id(&db_client().await.users, user_id)
.await
.context("failed to query mongo for user")?
.with_context(|| format!("no user found with id {user_id}"))
}
#[instrument(level = "debug")]
pub async fn get_server_with_status(
server_id_or_name: &str,
) -> anyhow::Result<(Server, ServerStatus)> {
let server = Server::get_resource(server_id_or_name).await?;
if !server.config.enabled {
return Ok((server, ServerStatus::Disabled));
}
let status = match super::periphery_client(&server)?
.request(periphery_client::api::GetHealth {})
.await
{
Ok(_) => ServerStatus::Ok,
Err(_) => ServerStatus::NotOk,
};
Ok((server, status))
}
#[instrument(level = "debug")]
pub async fn get_deployment_state(
deployment: &Deployment,
) -> anyhow::Result<DockerContainerState> {
if deployment.config.server_id.is_empty() {
return Ok(DockerContainerState::NotDeployed);
}
let (server, status) =
get_server_with_status(&deployment.config.server_id).await?;
if status != ServerStatus::Ok {
return Ok(DockerContainerState::Unknown);
}
let container = super::periphery_client(&server)?
.request(periphery_client::api::container::GetContainerList {})
.await?
.into_iter()
.find(|container| container.name == deployment.name);
let state = match container {
Some(container) => container.state,
None => DockerContainerState::NotDeployed,
};
Ok(state)
}
// TAG
#[instrument(level = "debug")]
pub async fn get_tag(id_or_name: &str) -> anyhow::Result<Tag> {
let query = match ObjectId::from_str(id_or_name) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": id_or_name },
};
db_client()
.await
.tags
.find_one(query, None)
.await
.context("failed to query mongo for tag")?
.with_context(|| format!("no tag found matching {id_or_name}"))
}
#[instrument(level = "debug")]
pub async fn get_tag_check_owner(
id_or_name: &str,
user: &User,
) -> anyhow::Result<Tag> {
let tag = get_tag(id_or_name).await?;
if user.admin || tag.owner == user.id {
return Ok(tag);
}
Err(anyhow!("user must be tag owner or admin"))
}
#[instrument(level = "debug")]
pub async fn get_user_user_group_ids(
user_id: &str,
) -> anyhow::Result<Vec<String>> {
let res = find_collect(
&db_client().await.user_groups,
doc! {
"users": user_id
},
None,
)
.await
.context("failed to query db for user groups")?
.into_iter()
.map(|ug| ug.id)
.collect();
Ok(res)
}
+47 -22
View File
@@ -1,4 +1,4 @@
use std::str::FromStr;
use std::{collections::HashSet, str::FromStr};
use anyhow::{anyhow, Context};
use futures::future::join_all;
@@ -54,7 +54,7 @@ use crate::{db::db_client, state::State};
use super::{
cache::{deployment_status_cache, server_status_cache},
get_tag,
query::{get_tag, get_user_user_group_ids},
};
pub trait StateResource {
@@ -582,21 +582,26 @@ pub async fn get_user_permission_on_resource(
resource_variant: ResourceTargetVariant,
resource_id: &str,
) -> anyhow::Result<PermissionLevel> {
let permission = db_client()
.await
.permissions
.find_one(
doc! {
"user_id": user_id,
"target.type": resource_variant.as_ref(),
"target.id": resource_id
},
None,
)
.await
.context("failed to query permissions table")?
.map(|permission| permission.level)
.unwrap_or_default();
let permission = find_collect(
&db_client().await.permissions,
doc! {
"$or": user_target_query(user_id).await?,
"target.type": resource_variant.as_ref(),
"target.id": resource_id
},
None,
)
.await
.context("failed to query db for permissions")?
.into_iter()
// get the max permission user has between personal / any user groups
.fold(PermissionLevel::None, |level, permission| {
if permission.level > level {
permission.level
} else {
level
}
});
Ok(permission)
}
@@ -628,8 +633,8 @@ pub async fn get_resource_ids_for_non_admin(
let permissions = find_collect(
&db_client().await.permissions,
doc! {
"user_id": user_id,
"target.type": resource_type.as_ref(),
"$or": user_target_query(user_id).await?,
"resource_target.type": resource_type.as_ref(),
"level": { "$in": ["Read", "Execute", "Update"] }
},
None,
@@ -637,9 +642,29 @@ pub async fn get_resource_ids_for_non_admin(
.await
.context("failed to query permissions on db")?
.into_iter()
.map(|p| p.target.extract_variant_id().1.to_string())
.collect();
Ok(permissions)
.map(|p| p.resource_target.extract_variant_id().1.to_string())
// collect into hashset first to remove any duplicates
.collect::<HashSet<_>>();
Ok(permissions.into_iter().collect())
}
#[instrument(level = "debug")]
async fn user_target_query(
user_id: &str,
) -> anyhow::Result<Vec<Document>> {
let mut user_target_query = vec![
doc! { "user_target.type": "User", "user_target.id": user_id },
];
let user_groups = get_user_user_group_ids(user_id)
.await?
.into_iter()
.map(|ug_id| {
doc! {
"user_target.type": "UserGroup", "user_target.id": ug_id,
}
});
user_target_query.extend(user_groups);
Ok(user_target_query)
}
#[instrument(level = "debug")]
+95
View File
@@ -0,0 +1,95 @@
use anyhow::Context;
use monitor_client::entities::{
monitor_timestamp,
update::{ResourceTarget, Update, UpdateListItem},
user::User,
Operation,
};
use mungos::{
by_id::{find_one_by_id, update_one_by_id},
mongodb::bson::to_document,
};
use crate::db::db_client;
use super::channel::update_channel;
pub fn make_update(
target: impl Into<ResourceTarget>,
operation: Operation,
user: &User,
) -> Update {
Update {
start_ts: monitor_timestamp(),
target: target.into(),
operation,
operator: user.id.clone(),
success: true,
..Default::default()
}
}
#[instrument(level = "debug")]
async fn update_list_item(
update: Update,
) -> anyhow::Result<UpdateListItem> {
let username = if User::is_service_user(&update.operator) {
update.operator.clone()
} else {
find_one_by_id(&db_client().await.users, &update.operator)
.await
.context("failed to query mongo for user")?
.with_context(|| {
format!("no user found with id {}", update.operator)
})?
.username
};
let update = UpdateListItem {
id: update.id,
operation: update.operation,
start_ts: update.start_ts,
success: update.success,
operator: update.operator,
target: update.target,
status: update.status,
version: update.version,
username,
};
Ok(update)
}
#[instrument(level = "debug")]
async fn send_update(update: UpdateListItem) -> anyhow::Result<()> {
update_channel().sender.lock().await.send(update)?;
Ok(())
}
#[instrument(level = "debug")]
pub async fn add_update(
mut update: Update,
) -> anyhow::Result<String> {
update.id = db_client()
.await
.updates
.insert_one(&update, None)
.await
.context("failed to insert update into db")?
.inserted_id
.as_object_id()
.context("inserted_id is not object id")?
.to_string();
let id = update.id.clone();
let update = update_list_item(update).await?;
let _ = send_update(update).await;
Ok(id)
}
#[instrument(level = "debug")]
pub async fn update_update(update: Update) -> anyhow::Result<()> {
update_one_by_id(&db_client().await.updates, &update.id, mungos::update::Update::Set(to_document(&update)?), None)
.await
.context("failed to update the update on db. the update build process was deleted")?;
let update = update_list_item(update).await?;
let _ = send_update(update).await;
Ok(())
}
+1
View File
@@ -19,6 +19,7 @@ pub mod server;
pub mod tag;
pub mod update;
pub mod user;
pub mod user_group;
#[typeshare(serialized_as = "number")]
pub type I64 = i64;
+13 -4
View File
@@ -8,8 +8,7 @@ use typeshare::typeshare;
use super::{update::ResourceTarget, MongoId};
/// Representation of a User or UserGroups permission on a resource.
/// 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
@@ -27,14 +26,24 @@ pub struct Permission {
pub id: MongoId,
/// Attached user
#[index]
pub user_id: String,
pub user_target: UserTarget,
/// The target resource
pub target: ResourceTarget,
pub resource_target: ResourceTarget,
/// The permission level
#[serde(default)]
pub level: PermissionLevel,
}
#[typeshare]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "id")]
pub enum UserTarget {
/// User Id
User(String),
/// UserGroup Id
UserGroup(String),
}
/// The levels of permission that a User or UserGroup can have on a resource.
#[typeshare]
#[derive(
+2 -4
View File
@@ -7,9 +7,7 @@ use mungos::mongodb::bson::{
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
use crate::entities::{MongoId, I64};
use super::update::ResourceTarget;
use crate::entities::{update::ResourceTarget, MongoId, I64};
#[typeshare]
#[derive(
@@ -20,7 +18,7 @@ use super::update::ResourceTarget;
#[sparse_doc_index(doc! { "config.data.github_id": 1 })]
pub struct User {
/// The Mongo ID of the User.
/// This field is de/serialized from/to JSON as
/// This field is de/serialized from/to JSON as
/// `{ "_id": { "$oid": "..." }, ...(rest of serialized User) }`
#[serde(
default,
+35
View File
@@ -0,0 +1,35 @@
use mongo_indexed::derive::MongoIndexed;
use mungos::mongodb::bson::{
serde_helpers::hex_string_as_object_id, Document,
};
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
use super::{MongoId, I64};
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Default, MongoIndexed,
)]
pub struct UserGroup {
/// The Mongo ID of the UserGroup.
/// This field is de/serialized from/to JSON as
/// `{ "_id": { "$oid": "..." }, ...(rest of serialized User) }`
#[serde(
default,
rename = "_id",
skip_serializing_if = "String::is_empty",
with = "hex_string_as_object_id"
)]
pub id: MongoId,
#[unique_index]
pub name: String,
/// User ids
#[index]
pub users: Vec<String>,
#[serde(default)]
pub updated_at: I64,
}