From 8c6f38cafbc4070077da500fc4d6c51282ec7dc1 Mon Sep 17 00:00:00 2001 From: Maxwell Becker <49575486+mbecker20@users.noreply.github.com> Date: Fri, 19 Jul 2024 02:11:36 -0700 Subject: [PATCH] v1.11 Improve permission management (#6) * add "all permissions" feature on user and user group schema * prepare support for group all * implement user.all and user_group.all for broad base permissioning * clean up unused deps * sync support user group permissions regex * 1.11 * fix fe ? issue * this doesn't work * sync handle user group all set * retain above non earlier * remove permissions that already exist * update docs * add user group docs * minimize user group permissions for execute * sync toml * add sync name to slack alert title * add syncs to alerter white/blacklist * use \\ instead of $reg * share resource type base permissions api users and user groups * manage user / group base permissions ui * manage user / group base resource type permissions * update api permission handling * manage all resource permissions in table * user show group membership * update client to 1.11 --- Cargo.lock | 27 +- Cargo.toml | 5 +- bin/core/Cargo.toml | 1 + bin/core/src/api/read/alert.rs | 10 +- bin/core/src/api/read/alerter.rs | 33 +- bin/core/src/api/read/builder.rs | 32 +- bin/core/src/api/read/mod.rs | 1 + bin/core/src/api/read/permission.rs | 2 +- bin/core/src/api/read/server_template.rs | 32 +- bin/core/src/api/read/toml.rs | 1 + bin/core/src/api/read/update.rs | 136 ++++-- bin/core/src/api/read/user.rs | 36 +- bin/core/src/api/user.rs | 45 +- bin/core/src/api/write/mod.rs | 1 + bin/core/src/api/write/permissions.rs | 74 +++- bin/core/src/api/write/service_user.rs | 7 +- bin/core/src/api/write/user_group.rs | 3 +- bin/core/src/auth/github/mod.rs | 7 +- bin/core/src/auth/google/mod.rs | 7 +- bin/core/src/auth/local.rs | 7 +- bin/core/src/helpers/alert.rs | 2 +- bin/core/src/helpers/query.rs | 145 ++++-- bin/core/src/helpers/sync/user_groups.rs | 417 ++++++++++++++++-- bin/core/src/resource/mod.rs | 14 +- bin/core/src/ws.rs | 2 +- bin/migrator/src/legacy/v0/user.rs | 7 +- client/core/rs/src/api/read/user.rs | 29 +- client/core/rs/src/api/write/permissions.rs | 28 +- client/core/rs/src/entities/toml.rs | 19 +- client/core/rs/src/entities/update.rs | 18 +- client/core/rs/src/entities/user.rs | 26 +- client/core/rs/src/entities/user_group.rs | 18 +- client/core/ts/src/responses.ts | 2 + client/core/ts/src/types.ts | 95 ++-- docsite/docs/sync-resources.md | 107 +++-- frontend/src/components/config/util.tsx | 33 +- frontend/src/components/layouts.tsx | 2 +- .../resources/alerter/config/resources.tsx | 11 + frontend/src/components/users/hooks.ts | 4 + .../components/users/permissions-table.tsx | 28 +- .../users/resource-type-permissions.tsx | 100 +++++ frontend/src/components/util.tsx | 2 +- frontend/src/pages/home/dashboard.tsx | 5 +- frontend/src/pages/user-group.tsx | 4 + frontend/src/pages/user.tsx | 53 ++- 45 files changed, 1246 insertions(+), 392 deletions(-) create mode 100644 frontend/src/components/users/resource-type-permissions.tsx diff --git a/Cargo.lock b/Cargo.lock index cb5ba7351..c3bef3fda 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,7 +41,7 @@ dependencies = [ [[package]] name = "alerter" -version = "1.10.5" +version = "1.11.0" dependencies = [ "anyhow", "axum 0.7.5", @@ -967,7 +967,7 @@ dependencies = [ [[package]] name = "command" -version = "1.10.5" +version = "1.11.0" dependencies = [ "monitor_client", "run_command", @@ -1351,7 +1351,7 @@ dependencies = [ [[package]] name = "formatting" -version = "1.10.5" +version = "1.11.0" dependencies = [ "serror", ] @@ -1482,7 +1482,7 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "git" -version = "1.10.5" +version = "1.11.0" dependencies = [ "anyhow", "command", @@ -2083,7 +2083,7 @@ dependencies = [ [[package]] name = "logger" -version = "1.10.5" +version = "1.11.0" dependencies = [ "anyhow", "monitor_client", @@ -2152,7 +2152,7 @@ dependencies = [ [[package]] name = "migrator" -version = "1.10.5" +version = "1.11.0" dependencies = [ "anyhow", "chrono", @@ -2286,7 +2286,7 @@ dependencies = [ [[package]] name = "monitor_cli" -version = "1.10.5" +version = "1.11.0" dependencies = [ "anyhow", "clap", @@ -2306,7 +2306,7 @@ dependencies = [ [[package]] name = "monitor_client" -version = "1.10.5" +version = "1.11.0" dependencies = [ "anyhow", "async_timing_util", @@ -2338,7 +2338,7 @@ dependencies = [ [[package]] name = "monitor_core" -version = "1.10.5" +version = "1.11.0" dependencies = [ "anyhow", "async_timing_util", @@ -2370,6 +2370,7 @@ dependencies = [ "partial_derive2", "periphery_client", "rand", + "regex", "reqwest 0.12.5", "resolver_api", "run_command", @@ -2394,7 +2395,7 @@ dependencies = [ [[package]] name = "monitor_periphery" -version = "1.10.5" +version = "1.11.0" dependencies = [ "anyhow", "async_timing_util", @@ -2838,7 +2839,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "periphery_client" -version = "1.10.5" +version = "1.11.0" dependencies = [ "anyhow", "monitor_client", @@ -3977,7 +3978,7 @@ dependencies = [ [[package]] name = "tests" -version = "1.10.5" +version = "1.11.0" dependencies = [ "anyhow", "dotenv", @@ -4579,7 +4580,7 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "update_logger" -version = "1.10.5" +version = "1.11.0" dependencies = [ "anyhow", "logger", diff --git a/Cargo.toml b/Cargo.toml index 0e5659ee4..24a8a8cd9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["bin/*", "lib/*", "client/core/rs", "client/periphery/rs"] [workspace.package] -version = "1.10.5" +version = "1.11.0" edition = "2021" authors = ["mbecker20 "] license = "GPL-3.0-or-later" @@ -15,7 +15,7 @@ monitor_client = { path = "client/core/rs" } [workspace.dependencies] # LOCAL -monitor_client = "1.10.5" +monitor_client = "1.11.0" periphery_client = { path = "client/periphery/rs" } formatting = { path = "lib/formatting" } command = { path = "lib/command" } @@ -103,5 +103,6 @@ derive_builder = "0.20.0" typeshare = "1.0.3" octorust = "0.7.0" colored = "2.1.0" +regex = "1.10.5" bson = "2.11.0" diff --git a/bin/core/Cargo.toml b/bin/core/Cargo.toml index f7049b96a..4646a4b9c 100644 --- a/bin/core/Cargo.toml +++ b/bin/core/Cargo.toml @@ -58,6 +58,7 @@ tokio.workspace = true tower.workspace = true serde.workspace = true strum.workspace = true +regex.workspace = true axum.workspace = true toml.workspace = true uuid.workspace = true diff --git a/bin/core/src/api/read/alert.rs b/bin/core/src/api/read/alert.rs index 97506f901..fd5952d9c 100644 --- a/bin/core/src/api/read/alert.rs +++ b/bin/core/src/api/read/alert.rs @@ -14,7 +14,7 @@ use resolver_api::Resolve; use crate::{ config::core_config, - helpers::query::get_resource_ids_for_non_admin, + helpers::query::get_resource_ids_for_user, state::{db_client, State}, }; @@ -28,13 +28,13 @@ impl Resolve for State { ) -> anyhow::Result { let mut query = query.unwrap_or_default(); if !user.admin && !core_config().transparent_mode { - let server_ids = get_resource_ids_for_non_admin( - &user.id, + let server_ids = get_resource_ids_for_user( + &user, ResourceTargetVariant::Server, ) .await?; - let deployment_ids = get_resource_ids_for_non_admin( - &user.id, + let deployment_ids = get_resource_ids_for_user( + &user, ResourceTargetVariant::Deployment, ) .await?; diff --git a/bin/core/src/api/read/alerter.rs b/bin/core/src/api/read/alerter.rs index 714a3fa28..2d35b474f 100644 --- a/bin/core/src/api/read/alerter.rs +++ b/bin/core/src/api/read/alerter.rs @@ -1,6 +1,5 @@ -use std::str::FromStr; - use anyhow::Context; +use mongo_indexed::Document; use monitor_client::{ api::read::*, entities::{ @@ -10,12 +9,11 @@ use monitor_client::{ user::User, }, }; -use mungos::mongodb::bson::{doc, oid::ObjectId}; +use mungos::mongodb::bson::doc; use resolver_api::Resolve; use crate::{ - config::core_config, - helpers::query::get_resource_ids_for_non_admin, + helpers::query::get_resource_ids_for_user, resource, state::{db_client, State}, }; @@ -61,26 +59,21 @@ impl Resolve for State { GetAlertersSummary {}: GetAlertersSummary, user: User, ) -> anyhow::Result { - let query = if user.admin || core_config().transparent_mode { - None - } else { - let ids = get_resource_ids_for_non_admin( - &user.id, - ResourceTargetVariant::Alerter, - ) - .await? - .into_iter() - .flat_map(|id| ObjectId::from_str(&id)) - .collect::>(); - let query = doc! { + let query = match get_resource_ids_for_user( + &user, + ResourceTargetVariant::Alerter, + ) + .await? + { + Some(ids) => doc! { "_id": { "$in": ids } - }; - Some(query) + }, + None => Document::new(), }; let total = db_client() .await .alerters - .count_documents(query.unwrap_or_default()) + .count_documents(query) .await .context("failed to count all alerter documents")?; let res = GetAlertersSummaryResponse { diff --git a/bin/core/src/api/read/builder.rs b/bin/core/src/api/read/builder.rs index 1dc1e0c37..7319257f7 100644 --- a/bin/core/src/api/read/builder.rs +++ b/bin/core/src/api/read/builder.rs @@ -1,6 +1,7 @@ -use std::{collections::HashSet, str::FromStr}; +use std::collections::HashSet; use anyhow::Context; +use mongo_indexed::Document; use monitor_client::{ api::read::{self, *}, entities::{ @@ -10,12 +11,12 @@ use monitor_client::{ user::User, }, }; -use mungos::mongodb::bson::{doc, oid::ObjectId}; +use mungos::mongodb::bson::doc; use resolver_api::Resolve; use crate::{ config::core_config, - helpers::query::get_resource_ids_for_non_admin, + helpers::query::get_resource_ids_for_user, resource, state::{db_client, State}, }; @@ -61,26 +62,21 @@ impl Resolve for State { GetBuildersSummary {}: GetBuildersSummary, user: User, ) -> anyhow::Result { - let query = if user.admin || core_config().transparent_mode { - None - } else { - let ids = get_resource_ids_for_non_admin( - &user.id, - ResourceTargetVariant::Builder, - ) - .await? - .into_iter() - .flat_map(|id| ObjectId::from_str(&id)) - .collect::>(); - let query = doc! { + let query = match get_resource_ids_for_user( + &user, + ResourceTargetVariant::Builder, + ) + .await? + { + Some(ids) => doc! { "_id": { "$in": ids } - }; - Some(query) + }, + None => Document::new(), }; let total = db_client() .await .builders - .count_documents(query.unwrap_or_default()) + .count_documents(query) .await .context("failed to count all builder documents")?; let res = GetBuildersSummaryResponse { diff --git a/bin/core/src/api/read/mod.rs b/bin/core/src/api/read/mod.rs index 2fec09858..873d421ce 100644 --- a/bin/core/src/api/read/mod.rs +++ b/bin/core/src/api/read/mod.rs @@ -47,6 +47,7 @@ enum ReadRequest { // ==== USER ==== GetUsername(GetUsername), GetPermissionLevel(GetPermissionLevel), + FindUser(FindUser), ListUsers(ListUsers), ListApiKeys(ListApiKeys), ListApiKeysForServiceUser(ListApiKeysForServiceUser), diff --git a/bin/core/src/api/read/permission.rs b/bin/core/src/api/read/permission.rs index 19d25a13f..3baf00593 100644 --- a/bin/core/src/api/read/permission.rs +++ b/bin/core/src/api/read/permission.rs @@ -44,7 +44,7 @@ impl Resolve for State { return Ok(PermissionLevel::Write); } let (variant, id) = target.extract_variant_id(); - get_user_permission_on_resource(&user.id, variant, id).await + get_user_permission_on_resource(&user, variant, id).await } } diff --git a/bin/core/src/api/read/server_template.rs b/bin/core/src/api/read/server_template.rs index 203d5d230..9f9a6417b 100644 --- a/bin/core/src/api/read/server_template.rs +++ b/bin/core/src/api/read/server_template.rs @@ -1,6 +1,5 @@ -use std::str::FromStr; - use anyhow::Context; +use mongo_indexed::Document; use monitor_client::{ api::read::*, entities::{ @@ -8,11 +7,11 @@ use monitor_client::{ update::ResourceTargetVariant, user::User, }, }; -use mungos::mongodb::bson::{doc, oid::ObjectId}; +use mungos::mongodb::bson::doc; use resolver_api::Resolve; use crate::{ - helpers::query::get_resource_ids_for_non_admin, + helpers::query::get_resource_ids_for_user, resource, state::{db_client, State}, }; @@ -58,26 +57,21 @@ impl Resolve for State { GetServerTemplatesSummary {}: GetServerTemplatesSummary, user: User, ) -> anyhow::Result { - let query = if user.admin { - None - } else { - let ids = get_resource_ids_for_non_admin( - &user.id, - ResourceTargetVariant::ServerTemplate, - ) - .await? - .into_iter() - .flat_map(|id| ObjectId::from_str(&id)) - .collect::>(); - let query = doc! { + let query = match get_resource_ids_for_user( + &user, + ResourceTargetVariant::ServerTemplate, + ) + .await? + { + Some(ids) => doc! { "_id": { "$in": ids } - }; - Some(query) + }, + None => Document::new(), }; let total = db_client() .await .server_templates - .count_documents(query.unwrap_or_default()) + .count_documents(query) .await .context("failed to count all server template documents")?; let res = GetServerTemplatesSummaryResponse { diff --git a/bin/core/src/api/read/toml.rs b/bin/core/src/api/read/toml.rs index 1dca71580..7755c51c3 100644 --- a/bin/core/src/api/read/toml.rs +++ b/bin/core/src/api/read/toml.rs @@ -573,6 +573,7 @@ async fn add_user_groups( .into_iter() .filter_map(|user_id| usernames.get(&user_id).cloned()) .collect(), + all: ug.all, permissions, }); } diff --git a/bin/core/src/api/read/update.rs b/bin/core/src/api/read/update.rs index ffd705bcf..019eb66e0 100644 --- a/bin/core/src/api/read/update.rs +++ b/bin/core/src/api/read/update.rs @@ -29,7 +29,7 @@ use resolver_api::Resolve; use crate::{ config::core_config, - helpers::query::get_resource_ids_for_non_admin, + helpers::query::get_resource_ids_for_user, resource, state::{db_client, State}, }; @@ -45,58 +45,124 @@ impl Resolve for State { let query = if user.admin || core_config().transparent_mode { query } else { - let server_ids = get_resource_ids_for_non_admin( - &user.id, + let server_query = get_resource_ids_for_user( + &user, ResourceTargetVariant::Server, ) - .await?; - let deployment_ids = get_resource_ids_for_non_admin( - &user.id, + .await? + .map(|ids| { + doc! { + "target.type": "Server", "target.id": { "$in": ids } + } + }) + .unwrap_or_else(|| doc! { "target.type": "Server" }); + + let deployment_query = get_resource_ids_for_user( + &user, ResourceTargetVariant::Deployment, ) - .await?; - let build_ids = get_resource_ids_for_non_admin( - &user.id, + .await? + .map(|ids| { + doc! { + "target.type": "Deployment", "target.id": { "$in": ids } + } + }) + .unwrap_or_else(|| doc! { "target.type": "Deployment" }); + + let build_query = get_resource_ids_for_user( + &user, ResourceTargetVariant::Build, ) - .await?; - let repo_ids = get_resource_ids_for_non_admin( - &user.id, - ResourceTargetVariant::Repo, - ) - .await?; - let procedure_ids = get_resource_ids_for_non_admin( - &user.id, + .await? + .map(|ids| { + doc! { + "target.type": "Build", "target.id": { "$in": ids } + } + }) + .unwrap_or_else(|| doc! { "target.type": "Build" }); + + let repo_query = + get_resource_ids_for_user(&user, ResourceTargetVariant::Repo) + .await? + .map(|ids| { + doc! { + "target.type": "Repo", "target.id": { "$in": ids } + } + }) + .unwrap_or_else(|| doc! { "target.type": "Repo" }); + + let procedure_query = get_resource_ids_for_user( + &user, ResourceTargetVariant::Procedure, ) - .await?; - let builder_ids = get_resource_ids_for_non_admin( - &user.id, + .await? + .map(|ids| { + doc! { + "target.type": "Procedure", "target.id": { "$in": ids } + } + }) + .unwrap_or_else(|| doc! { "target.type": "Procedure" }); + + let builder_query = get_resource_ids_for_user( + &user, ResourceTargetVariant::Builder, ) - .await?; - let alerter_ids = get_resource_ids_for_non_admin( - &user.id, + .await? + .map(|ids| { + doc! { + "target.type": "Builder", "target.id": { "$in": ids } + } + }) + .unwrap_or_else(|| doc! { "target.type": "Builder" }); + + let alerter_query = get_resource_ids_for_user( + &user, ResourceTargetVariant::Alerter, ) - .await?; - let server_template_ids = get_resource_ids_for_non_admin( - &user.id, + .await? + .map(|ids| { + doc! { + "target.type": "Alerter", "target.id": { "$in": ids } + } + }) + .unwrap_or_else(|| doc! { "target.type": "Alerter" }); + + let server_template_query = get_resource_ids_for_user( + &user, ResourceTargetVariant::ServerTemplate, ) - .await?; + .await? + .map(|ids| { + doc! { + "target.type": "ServerTemplate", "target.id": { "$in": ids } + } + }) + .unwrap_or_else(|| doc! { "target.type": "ServerTemplate" }); + + let resource_sync_query = get_resource_ids_for_user( + &user, + ResourceTargetVariant::ResourceSync, + ) + .await? + .map(|ids| { + doc! { + "target.type": "ResourceSync", "target.id": { "$in": ids } + } + }) + .unwrap_or_else(|| doc! { "target.type": "ResourceSync" }); let mut query = query.unwrap_or_default(); query.extend(doc! { "$or": [ - { "target.type": "Server", "target.id": { "$in": &server_ids } }, - { "target.type": "Deployment", "target.id": { "$in": &deployment_ids } }, - { "target.type": "Build", "target.id": { "$in": &build_ids } }, - { "target.type": "Repo", "target.id": { "$in": &repo_ids } }, - { "target.type": "Procedure", "target.id": { "$in": &procedure_ids } }, - { "target.type": "Builder", "target.id": { "$in": &builder_ids } }, - { "target.type": "Alerter", "target.id": { "$in": &alerter_ids } }, - { "target.type": "ServerTemplate", "target.id": { "$in": &server_template_ids } }, + server_query, + build_query, + deployment_query, + repo_query, + procedure_query, + alerter_query, + builder_query, + server_template_query, + resource_sync_query, ] }); query.into() diff --git a/bin/core/src/api/read/user.rs b/bin/core/src/api/read/user.rs index eadcded11..a28ba2bb9 100644 --- a/bin/core/src/api/read/user.rs +++ b/bin/core/src/api/read/user.rs @@ -1,9 +1,10 @@ use anyhow::{anyhow, Context}; use monitor_client::{ api::read::{ - GetUsername, GetUsernameResponse, ListApiKeys, - ListApiKeysForServiceUser, ListApiKeysForServiceUserResponse, - ListApiKeysResponse, ListUsers, ListUsersResponse, + FindUser, FindUserResponse, GetUsername, GetUsernameResponse, + ListApiKeys, ListApiKeysForServiceUser, + ListApiKeysForServiceUserResponse, ListApiKeysResponse, + ListUsers, ListUsersResponse, }, entities::user::{User, UserConfig}, }; @@ -14,7 +15,10 @@ use mungos::{ }; use resolver_api::Resolve; -use crate::state::{db_client, State}; +use crate::{ + helpers::query::get_user, + state::{db_client, State}, +}; impl Resolve for State { async fn resolve( @@ -40,6 +44,19 @@ impl Resolve for State { } } +impl Resolve for State { + async fn resolve( + &self, + FindUser { user }: FindUser, + admin: User, + ) -> anyhow::Result { + if !admin.admin { + return Err(anyhow!("This method is admin only.")); + } + get_user(&user).await + } +} + impl Resolve for State { async fn resolve( &self, @@ -87,22 +104,21 @@ impl Resolve for State { impl Resolve for State { async fn resolve( &self, - ListApiKeysForServiceUser { user_id }: ListApiKeysForServiceUser, + ListApiKeysForServiceUser { user }: ListApiKeysForServiceUser, admin: User, ) -> anyhow::Result { if !admin.admin { return Err(anyhow!("This method is admin only.")); } - let user = find_one_by_id(&db_client().await.users, &user_id) - .await - .context("failed to query db for users")? - .context("user at id not found")?; + + let user = get_user(&user).await?; + let UserConfig::Service { .. } = user.config else { return Err(anyhow!("Given user is not service user")); }; let api_keys = find_collect( &db_client().await.api_keys, - doc! { "user_id": user_id }, + doc! { "user_id": &user.id }, None, ) .await diff --git a/bin/core/src/api/user.rs b/bin/core/src/api/user.rs index d21070da4..de68f679f 100644 --- a/bin/core/src/api/user.rs +++ b/bin/core/src/api/user.rs @@ -11,10 +11,7 @@ use monitor_client::{ PushRecentlyViewedResponse, SetLastSeenUpdate, SetLastSeenUpdateResponse, }, - entities::{ - api_key::ApiKey, monitor_timestamp, update::ResourceTarget, - user::User, - }, + entities::{api_key::ApiKey, monitor_timestamp, user::User}, }; use mungos::{by_id::update_one_by_id, mongodb::bson::to_bson}; use resolver_api::{derive::Resolver, Resolve, Resolver}; @@ -90,33 +87,21 @@ impl Resolve for State { ) -> anyhow::Result { let user = get_user(&user.id).await?; - let (recents, id, field) = match resource { - ResourceTarget::Server(id) => { - (user.recent_servers, id, "recent_servers") + let (resource_type, id) = resource.extract_variant_id(); + let update = match user.recents.get(&resource_type) { + Some(recents) => { + let mut recents = recents + .iter() + .filter(|_id| !id.eq(*_id)) + .take(RECENTLY_VIEWED_MAX - 1) + .collect::>(); + recents.push_front(id); + doc! { format!("recents.{resource_type}"): to_bson(&recents)? } } - ResourceTarget::Deployment(id) => { - (user.recent_deployments, id, "recent_deployments") + None => { + doc! { format!("recents.{resource_type}"): [id] } } - ResourceTarget::Build(id) => { - (user.recent_builds, id, "recent_builds") - } - ResourceTarget::Repo(id) => { - (user.recent_repos, id, "recent_repos") - } - ResourceTarget::Procedure(id) => { - (user.recent_procedures, id, "recent_procedures") - } - _ => return Ok(PushRecentlyViewedResponse {}), }; - - let mut recents = recents - .into_iter() - .filter(|_id| !id.eq(_id)) - .take(RECENTLY_VIEWED_MAX - 1) - .collect::>(); - recents.push_front(id); - let update = doc! { field: to_bson(&recents)? }; - update_one_by_id( &db_client().await.users, &user.id, @@ -124,7 +109,9 @@ impl Resolve for State { None, ) .await - .with_context(|| format!("failed to update {field}"))?; + .with_context(|| { + format!("failed to update recents.{resource_type}") + })?; Ok(PushRecentlyViewedResponse {}) } diff --git a/bin/core/src/api/write/mod.rs b/bin/core/src/api/write/mod.rs index 313bde4eb..2a752c028 100644 --- a/bin/core/src/api/write/mod.rs +++ b/bin/core/src/api/write/mod.rs @@ -50,6 +50,7 @@ pub enum WriteRequest { // ==== PERMISSIONS ==== UpdateUserBasePermissions(UpdateUserBasePermissions), + UpdatePermissionOnResourceType(UpdatePermissionOnResourceType), UpdatePermissionOnTarget(UpdatePermissionOnTarget), // ==== DESCRIPTION ==== diff --git a/bin/core/src/api/write/permissions.rs b/bin/core/src/api/write/permissions.rs index 3351d3674..aee407c36 100644 --- a/bin/core/src/api/write/permissions.rs +++ b/bin/core/src/api/write/permissions.rs @@ -3,8 +3,10 @@ use std::str::FromStr; use anyhow::{anyhow, Context}; use monitor_client::{ api::write::{ - UpdatePermissionOnTarget, UpdatePermissionOnTargetResponse, - UpdateUserBasePermissions, UpdateUserBasePermissionsResponse, + UpdatePermissionOnResourceType, + UpdatePermissionOnResourceTypeResponse, UpdatePermissionOnTarget, + UpdatePermissionOnTargetResponse, UpdateUserBasePermissions, + UpdateUserBasePermissionsResponse, }, entities::{ permission::{UserTarget, UserTargetVariant}, @@ -41,6 +43,7 @@ impl Resolve for State { if !admin.admin { return Err(anyhow!("this method is admin only")); } + let user = find_one_by_id(&db_client().await.users, &user_id) .await .context("failed to query mongo for user")? @@ -73,6 +76,73 @@ impl Resolve for State { } } +impl Resolve for State { + #[instrument( + name = "UpdatePermissionOnResourceType", + skip(self, admin) + )] + async fn resolve( + &self, + UpdatePermissionOnResourceType { + user_target, + resource_type, + permission, + }: UpdatePermissionOnResourceType, + admin: User, + ) -> anyhow::Result { + if !admin.admin { + return Err(anyhow!("this method is admin only")); + } + + // 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")); + } + } + + let (user_target_variant, user_target_id) = + extract_user_target_with_validation(&user_target).await?; + + let id = ObjectId::from_str(&user_target_id) + .context("id is not ObjectId")?; + let field = format!("all.{resource_type}"); + let filter = doc! { "_id": id }; + let update = doc! { "$set": { &field: permission.as_ref() } }; + + match user_target_variant { + UserTargetVariant::User => { + db_client() + .await + .users + .update_one(filter, update) + .await + .with_context(|| { + format!("failed to set {field}: {permission} on db") + })?; + } + UserTargetVariant::UserGroup => { + db_client() + .await + .user_groups + .update_one(filter, update) + .await + .with_context(|| { + format!("failed to set {field}: {permission} on db") + })?; + } + } + + Ok(UpdatePermissionOnResourceTypeResponse {}) + } +} + impl Resolve for State { #[instrument(name = "UpdatePermissionOnTarget", skip(self, admin))] async fn resolve( diff --git a/bin/core/src/api/write/service_user.rs b/bin/core/src/api/write/service_user.rs index 0647a578b..d1f09c7e1 100644 --- a/bin/core/src/api/write/service_user.rs +++ b/bin/core/src/api/write/service_user.rs @@ -51,11 +51,8 @@ impl Resolve for State { create_server_permissions: false, create_build_permissions: false, last_update_view: 0, - recent_servers: Vec::new(), - recent_deployments: Vec::new(), - recent_builds: Vec::new(), - recent_repos: Vec::new(), - recent_procedures: Vec::new(), + recents: Default::default(), + all: Default::default(), updated_at: monitor_timestamp(), }; user.id = db_client() diff --git a/bin/core/src/api/write/user_group.rs b/bin/core/src/api/write/user_group.rs index b9de7733d..0c0b61bbe 100644 --- a/bin/core/src/api/write/user_group.rs +++ b/bin/core/src/api/write/user_group.rs @@ -29,6 +29,7 @@ impl Resolve for State { let user_group = UserGroup { id: Default::default(), users: Default::default(), + all: Default::default(), updated_at: monitor_timestamp(), name, }; @@ -229,7 +230,7 @@ impl Resolve for State { db.user_groups .update_one(filter.clone(), doc! { "$set": { "users": users } }) .await - .context("failed to add user to group on db")?; + .context("failed to set users on user group")?; db.user_groups .find_one(filter) .await diff --git a/bin/core/src/auth/github/mod.rs b/bin/core/src/auth/github/mod.rs index deeb8f176..1f33e59c2 100644 --- a/bin/core/src/auth/github/mod.rs +++ b/bin/core/src/auth/github/mod.rs @@ -87,11 +87,8 @@ async fn callback( create_build_permissions: no_users_exist, updated_at: ts, last_update_view: 0, - recent_servers: Vec::new(), - recent_deployments: Vec::new(), - recent_builds: Vec::new(), - recent_repos: Vec::new(), - recent_procedures: Vec::new(), + recents: Default::default(), + all: Default::default(), config: UserConfig::Github { github_id, avatar: github_user.avatar_url, diff --git a/bin/core/src/auth/google/mod.rs b/bin/core/src/auth/google/mod.rs index f05c9a7ce..0336c5d10 100644 --- a/bin/core/src/auth/google/mod.rs +++ b/bin/core/src/auth/google/mod.rs @@ -102,11 +102,8 @@ async fn callback( create_build_permissions: no_users_exist, updated_at: ts, last_update_view: 0, - recent_servers: Vec::new(), - recent_deployments: Vec::new(), - recent_builds: Vec::new(), - recent_repos: Vec::new(), - recent_procedures: Vec::new(), + recents: Default::default(), + all: Default::default(), config: UserConfig::Google { google_id, avatar: google_user.picture, diff --git a/bin/core/src/auth/local.rs b/bin/core/src/auth/local.rs index 5b9bd140d..43bd551d4 100644 --- a/bin/core/src/auth/local.rs +++ b/bin/core/src/auth/local.rs @@ -62,11 +62,8 @@ impl Resolve for State { create_build_permissions: no_users_exist, updated_at: ts, last_update_view: 0, - recent_servers: Vec::new(), - recent_deployments: Vec::new(), - recent_builds: Vec::new(), - recent_repos: Vec::new(), - recent_procedures: Vec::new(), + recents: Default::default(), + all: Default::default(), config: UserConfig::Local { password }, }; diff --git a/bin/core/src/helpers/alert.rs b/bin/core/src/helpers/alert.rs index 1bb6d7c87..cdb370b3b 100644 --- a/bin/core/src/helpers/alert.rs +++ b/bin/core/src/helpers/alert.rs @@ -335,7 +335,7 @@ async fn send_slack_alert( } AlertData::ResourceSyncPendingUpdates { id, name } => { let text = - format!("{level} | There are pending resource sync updates"); + format!("{level} | Pending resource sync updates on {name}"); let blocks = vec![ Block::header(text.clone()), Block::section(format!( diff --git a/bin/core/src/helpers/query.rs b/bin/core/src/helpers/query.rs index 96e6e4f96..7f2218299 100644 --- a/bin/core/src/helpers/query.rs +++ b/bin/core/src/helpers/query.rs @@ -11,11 +11,11 @@ use monitor_client::entities::{ tag::Tag, update::{ResourceTargetVariant, Update}, user::{admin_service_user, User}, + user_group::UserGroup, variable::Variable, Operation, }; use mungos::{ - by_id::find_one_by_id, find::find_collect, mongodb::{ bson::{doc, oid::ObjectId, Document}, @@ -26,14 +26,18 @@ use mungos::{ use crate::{config::core_config, resource, state::db_client}; #[instrument(level = "debug")] -pub async fn get_user(user_id: &str) -> anyhow::Result { - if let Some(user) = admin_service_user(user_id) { +// user: Id or username +pub async fn get_user(user: &str) -> anyhow::Result { + if let Some(user) = admin_service_user(user) { return Ok(user); } - find_one_by_id(&db_client().await.users, user_id) + db_client() + .await + .users + .find_one(id_or_username_filter(user)) .await .context("failed to query mongo for user")? - .with_context(|| format!("no user found with id {user_id}")) + .with_context(|| format!("no user found with {user}")) } #[instrument(level = "debug")] @@ -120,10 +124,10 @@ pub async fn get_id_to_tags( } #[instrument(level = "debug")] -pub async fn get_user_user_group_ids( +pub async fn get_user_user_groups( user_id: &str, -) -> anyhow::Result> { - let res = find_collect( +) -> anyhow::Result> { + find_collect( &db_client().await.user_groups, doc! { "users": user_id @@ -131,50 +135,84 @@ pub async fn get_user_user_group_ids( None, ) .await - .context("failed to query db for user groups")? - .into_iter() - .map(|ug| ug.id) - .collect(); + .context("failed to query db for user groups") +} + +#[instrument(level = "debug")] +pub async fn get_user_user_group_ids( + user_id: &str, +) -> anyhow::Result> { + let res = get_user_user_groups(user_id) + .await? + .into_iter() + .map(|ug| ug.id) + .collect(); Ok(res) } -/// Returns Vec of all queries on permissions that match against the user -/// or any user groups that the user is a part of. -/// Result used with Mongodb '$or'. -#[instrument(level = "debug")] -pub async fn user_target_query( +pub fn user_target_query( user_id: &str, + user_groups: &[UserGroup], ) -> anyhow::Result> { 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, - } - }); + let user_groups = user_groups.iter().map(|ug| { + doc! { + "user_target.type": "UserGroup", "user_target.id": &ug.id, + } + }); user_target_query.extend(user_groups); Ok(user_target_query) } #[instrument(level = "debug")] pub async fn get_user_permission_on_resource( - user_id: &str, + user: &User, resource_variant: ResourceTargetVariant, resource_id: &str, ) -> anyhow::Result { - let lowest_permission = if core_config().transparent_mode { + if user.admin { + return Ok(PermissionLevel::Write); + } + + // Start with base of Read or None + let mut base = if core_config().transparent_mode { PermissionLevel::Read } else { PermissionLevel::None }; + + // Overlay users base on resource variant + if let Some(level) = user.all.get(&resource_variant).cloned() { + if level > base { + base = level; + } + } + if base == PermissionLevel::Write { + // No reason to keep going if already Write at this point. + return Ok(PermissionLevel::Write); + } + + // Overlay any user groups base on resource variant + let groups = get_user_user_groups(&user.id).await?; + for group in &groups { + if let Some(level) = group.all.get(&resource_variant).cloned() { + if level > base { + base = level; + } + } + } + if base == PermissionLevel::Write { + // No reason to keep going if already Write at this point. + return Ok(PermissionLevel::Write); + } + + // Overlay any specific permissions let permission = find_collect( &db_client().await.permissions, doc! { - "$or": user_target_query(user_id).await?, + "$or": user_target_query(&user.id, &groups)?, "resource_target.type": resource_variant.as_ref(), "resource_target.id": resource_id }, @@ -184,7 +222,7 @@ pub async fn get_user_permission_on_resource( .context("failed to query db for permissions")? .into_iter() // get the max permission user has between personal / any user groups - .fold(lowest_permission, |level, permission| { + .fold(base, |level, permission| { if permission.level > level { permission.level } else { @@ -194,15 +232,39 @@ pub async fn get_user_permission_on_resource( Ok(permission) } +/// Returns None if still no need to filter by resource id (eg transparent mode, group membership with all access). #[instrument(level = "debug")] -pub async fn get_resource_ids_for_non_admin( - user_id: &str, +pub async fn get_resource_ids_for_user( + user: &User, resource_type: ResourceTargetVariant, -) -> anyhow::Result> { - let permissions = find_collect( +) -> anyhow::Result>> { + // Check admin or transparent mode + if user.admin || core_config().transparent_mode { + return Ok(None); + } + + // Check user 'all' on variant + if let Some(level) = user.all.get(&resource_type).cloned() { + if level > PermissionLevel::None { + return Ok(None); + } + } + + // Check user groups 'all' on variant + let groups = get_user_user_groups(&user.id).await?; + for group in &groups { + if let Some(level) = group.all.get(&resource_type).cloned() { + if level > PermissionLevel::None { + return Ok(None); + } + } + } + + // Get specific ids + let ids = find_collect( &db_client().await.permissions, doc! { - "$or": user_target_query(user_id).await?, + "$or": user_target_query(&user.id, &groups)?, "resource_target.type": resource_type.as_ref(), "level": { "$in": ["Read", "Execute", "Write"] } }, @@ -213,8 +275,12 @@ pub async fn get_resource_ids_for_non_admin( .into_iter() .map(|p| p.resource_target.extract_variant_id().1.to_string()) // collect into hashset first to remove any duplicates - .collect::>(); - Ok(permissions.into_iter().collect()) + .collect::>() + .into_iter() + .flat_map(|id| ObjectId::from_str(&id)) + .collect::>(); + + Ok(Some(ids)) } pub fn id_or_name_filter(id_or_name: &str) -> Document { @@ -224,6 +290,13 @@ pub fn id_or_name_filter(id_or_name: &str) -> Document { } } +pub fn id_or_username_filter(id_or_username: &str) -> Document { + match ObjectId::from_str(id_or_username) { + Ok(id) => doc! { "_id": id }, + Err(_) => doc! { "username": id_or_username }, + } +} + pub async fn get_global_variables( ) -> anyhow::Result> { Ok( diff --git a/bin/core/src/helpers/sync/user_groups.rs b/bin/core/src/helpers/sync/user_groups.rs index f3d0ce0cb..679fcd086 100644 --- a/bin/core/src/helpers/sync/user_groups.rs +++ b/bin/core/src/helpers/sync/user_groups.rs @@ -7,18 +7,19 @@ use monitor_client::{ read::ListUserTargetPermissions, write::{ CreateUserGroup, DeleteUserGroup, SetUsersInUserGroup, - UpdatePermissionOnTarget, + UpdatePermissionOnResourceType, UpdatePermissionOnTarget, }, }, entities::{ - permission::UserTarget, + permission::{PermissionLevel, UserTarget}, sync::SyncUpdate, toml::{PermissionToml, UserGroupToml}, - update::{Log, ResourceTarget}, + update::{Log, ResourceTarget, ResourceTargetVariant}, user::sync_user, }, }; use mungos::find::find_collect; +use regex::Regex; use resolver_api::Resolve; use crate::state::{db_client, State}; @@ -28,7 +29,7 @@ use super::resource::AllResourcesById; pub struct UpdateItem { user_group: UserGroupToml, update_users: bool, - update_permissions: bool, + all_diff: HashMap, } pub struct DeleteItem { @@ -72,19 +73,49 @@ pub async fn get_updates_for_view( .collect::>(); for mut user_group in user_groups { + user_group + .permissions + .retain(|p| p.level > PermissionLevel::None); + + user_group.permissions = expand_user_group_permissions( + user_group.permissions, + all_resources, + ) + .await + .with_context(|| { + format!( + "failed to expand user group {} permissions", + user_group.name + ) + })?; + let original = match map.get(&user_group.name).cloned() { Some(original) => original, None => { update.to_create += 1; - update.log.push_str(&format!( - "\n\n{}: user group: {}\n{}: {:?}\n{}: {:?}", - colored("CREATE", Color::Green), - colored(&user_group.name, Color::Green), - muted("users"), - user_group.users, - muted("permissions"), - user_group.permissions, - )); + if user_group.all.is_empty() { + update.log.push_str(&format!( + "\n\n{}: user group: {}\n{}: {:#?}\n{}: {:#?}", + colored("CREATE", Color::Green), + colored(&user_group.name, Color::Green), + muted("users"), + user_group.users, + muted("permissions"), + user_group.permissions, + )); + } else { + update.log.push_str(&format!( + "\n\n{}: user group: {}\n{}: {:#?}\n{}: {:#?}\n{}: {:#?}", + colored("CREATE", Color::Green), + colored(&user_group.name, Color::Green), + muted("users"), + user_group.users, + muted("base permissions"), + user_group.all, + muted("permissions"), + user_group.permissions, + )); + } continue; } }; @@ -107,6 +138,7 @@ pub async fn get_updates_for_view( .await .context("failed to query for existing UserGroup permissions")? .into_iter() + .filter(|p| p.level > PermissionLevel::None) .map(|mut p| { // replace the ids with names match &mut p.resource_target { @@ -185,22 +217,27 @@ pub async fn get_updates_for_view( original_users.sort(); user_group.users.sort(); + let all_diff = diff_group_all(&original.all, &user_group.all); + user_group.permissions.sort_by(sort_permissions); original_permissions.sort_by(sort_permissions); let update_users = user_group.users != original_users; + let update_all = !all_diff.is_empty(); let update_permissions = user_group.permissions != original_permissions; // only add log after diff detected - if update_users || update_permissions { + if update_users || update_all || update_permissions { update.to_update += 1; update.log.push_str(&format!( "\n\n{}: user group: '{}'\n-------------------", colored("UPDATE", Color::Blue), bold(&user_group.name), )); + let mut lines = Vec::::new(); + if update_users { let adding = user_group .users @@ -230,12 +267,36 @@ pub async fn get_updates_for_view( muted("adding"), )) } + + if update_all { + let updates = all_diff + .into_iter() + .map(|(variant, (orig, incoming))| { + format!( + "{}: {} {} {}", + bold(variant), + colored(orig, Color::Red), + muted("->"), + colored(incoming, Color::Green) + ) + }) + .collect::>() + .join("\n"); + lines.push(format!( + "{}: 'base permission'\n{updates}", + muted("field"), + )) + } + if update_permissions { let adding = user_group .permissions .iter() .filter(|permission| { - !original_permissions.contains(permission) + // add if original has no exising permission on the target + !original_permissions + .iter() + .any(|p| p.target == permission.target) }) .map(|permission| format!("{permission:?}")) .collect::>(); @@ -244,10 +305,35 @@ pub async fn get_updates_for_view( } else { colored(&adding.join(", "), Color::Green) }; + let updating = user_group + .permissions + .iter() + .filter(|permission| { + // update if original has exising permission on the target with different level + let Some(level) = original_permissions + .iter() + .find(|p| p.target == permission.target) + .map(|p| p.level) + else { + return false; + }; + permission.level != level + }) + .map(|permission| format!("{permission:?}")) + .collect::>(); + let updating = if updating.is_empty() { + String::from("None") + } else { + colored(&updating.join(", "), Color::Blue) + }; let removing = original_permissions .iter() .filter(|permission| { - !user_group.permissions.contains(permission) + // remove if incoming has no permission on the target + !user_group + .permissions + .iter() + .any(|p| p.target == permission.target) }) .map(|permission| format!("{permission:?}")) .collect::>(); @@ -257,12 +343,14 @@ pub async fn get_updates_for_view( colored(&removing.join(", "), Color::Red) }; lines.push(format!( - "{}: 'permissions'\n{}: {removing}\n{}: {adding}", + "{}: 'permissions'\n{}: {removing}\n{}: {updating}\n{}: {adding}", muted("field"), muted("removing"), + muted("updating"), muted("adding"), )) } + update.log.push('\n'); update.log.push_str(&lines.join("\n-------------------\n")); } @@ -326,6 +414,22 @@ pub async fn get_updates_for_execution( .collect::>(); for mut user_group in user_groups { + user_group + .permissions + .retain(|p| p.level > PermissionLevel::None); + + user_group.permissions = expand_user_group_permissions( + user_group.permissions, + all_resources, + ) + .await + .with_context(|| { + format!( + "failed to expand user group {} permissions", + user_group.name + ) + })?; + let original = match map.get(&user_group.name).cloned() { Some(original) => original, None => { @@ -352,6 +456,7 @@ pub async fn get_updates_for_execution( .await .context("failed to query for existing UserGroup permissions")? .into_iter() + .filter(|p| p.level > PermissionLevel::None) .map(|mut p| { // replace the ids with names match &mut p.resource_target { @@ -430,19 +535,55 @@ pub async fn get_updates_for_execution( original_users.sort(); user_group.users.sort(); + let all_diff = diff_group_all(&original.all, &user_group.all); + user_group.permissions.sort_by(sort_permissions); original_permissions.sort_by(sort_permissions); let update_users = user_group.users != original_users; - let update_permissions = - user_group.permissions != original_permissions; - // only push update after failed diff - if update_users || update_permissions { + // Extend permissions with any existing that have no target in incoming + let to_remove = original_permissions + .iter() + .filter(|permission| { + !user_group + .permissions + .iter() + .any(|p| p.target == permission.target) + }) + .map(|permission| PermissionToml { + target: permission.target.clone(), + level: PermissionLevel::None, + }) + .collect::>(); + user_group.permissions.extend(to_remove); + + // remove any permissions that already exist on original + user_group.permissions.retain(|permission| { + let Some(level) = original_permissions + .iter() + .find(|p| p.target == permission.target) + .map(|p| p.level) + else { + // not in original, keep it + return true; + }; + // keep it if level doesn't match + level != permission.level + }); + + // only push update after diff detected + if update_users + || !all_diff.is_empty() + || !user_group.permissions.is_empty() + { to_update.push(UpdateItem { user_group, update_users, - update_permissions, + all_diff: all_diff + .into_iter() + .map(|(k, (_, v))| (k, v)) + .collect(), }); } } @@ -516,6 +657,13 @@ pub async fn run_updates( &mut has_error, ) .await; + run_update_all( + user_group.name.clone(), + user_group.all, + &mut log, + &mut has_error, + ) + .await; run_update_permissions( user_group.name, user_group.permissions, @@ -529,7 +677,7 @@ pub async fn run_updates( for UpdateItem { user_group, update_users, - update_permissions, + all_diff, } in to_update { if update_users { @@ -541,7 +689,16 @@ pub async fn run_updates( ) .await; } - if update_permissions { + if !all_diff.is_empty() { + run_update_all( + user_group.name.clone(), + all_diff, + &mut log, + &mut has_error, + ) + .await; + } + if !user_group.permissions.is_empty() { run_update_permissions( user_group.name, user_group.permissions, @@ -616,6 +773,41 @@ async fn set_users( } } +async fn run_update_all( + user_group: String, + all_diff: HashMap, + log: &mut String, + has_error: &mut bool, +) { + for (resource_type, permission) in all_diff { + if let Err(e) = State + .resolve( + UpdatePermissionOnResourceType { + user_target: UserTarget::UserGroup(user_group.clone()), + resource_type, + permission, + }, + sync_user().to_owned(), + ) + .await + { + *has_error = true; + log.push_str(&format!( + "\n{}: failed to set base permissions on {resource_type} in group {} | {e:#}", + colored("ERROR", Color::Red), + bold(&user_group) + )) + } else { + log.push_str(&format!( + "\n{}: {} user group '{}' base permissions on {resource_type}", + muted("INFO"), + colored("updated", Color::Blue), + bold(&user_group) + )) + } + } +} + async fn run_update_permissions( user_group: String, permissions: Vec, @@ -636,17 +828,188 @@ async fn run_update_permissions( { *has_error = true; log.push_str(&format!( - "\n{}: failed to set permssion in group {} | target: {target:?} | {e:#}", + "\n{}: failed to set permission in group {} | target: {target:?} | {e:#}", colored("ERROR", Color::Red), bold(&user_group) )) } else { log.push_str(&format!( - "\n{}: {} user group '{}' permissions", + "\n{}: {} user group '{}' permissions | {}: {target:?} | {}: {level}", muted("INFO"), colored("updated", Color::Blue), - bold(&user_group) + bold(&user_group), + muted("target"), + muted("level") )) } } } + +/// Expands any regex defined targets into the full list +async fn expand_user_group_permissions( + permissions: Vec, + all_resources: &AllResourcesById, +) -> anyhow::Result> { + let mut expanded = + Vec::::with_capacity(permissions.capacity()); + + for permission in permissions { + let (variant, id) = permission.target.extract_variant_id(); + if id.is_empty() { + continue; + } + if id.starts_with('\\') && id.ends_with('\\') { + let inner = &id[1..(id.len() - 1)]; + let regex = Regex::new(inner) + .with_context(|| format!("invalid regex. got: {inner}"))?; + match variant { + ResourceTargetVariant::Build => { + let permissions = all_resources + .builds + .values() + .filter(|resource| regex.is_match(&resource.name)) + .map(|resource| PermissionToml { + target: ResourceTarget::Build(resource.name.clone()), + level: permission.level, + }); + expanded.extend(permissions); + } + ResourceTargetVariant::Builder => { + let permissions = all_resources + .builders + .values() + .filter(|resource| regex.is_match(&resource.name)) + .map(|resource| PermissionToml { + target: ResourceTarget::Builder(resource.name.clone()), + level: permission.level, + }); + expanded.extend(permissions); + } + ResourceTargetVariant::Deployment => { + let permissions = all_resources + .deployments + .values() + .filter(|resource| regex.is_match(&resource.name)) + .map(|resource| PermissionToml { + target: ResourceTarget::Deployment( + resource.name.clone(), + ), + level: permission.level, + }); + expanded.extend(permissions); + } + ResourceTargetVariant::Server => { + let permissions = all_resources + .servers + .values() + .filter(|resource| regex.is_match(&resource.name)) + .map(|resource| PermissionToml { + target: ResourceTarget::Server(resource.name.clone()), + level: permission.level, + }); + expanded.extend(permissions); + } + ResourceTargetVariant::Repo => { + let permissions = all_resources + .repos + .values() + .filter(|resource| regex.is_match(&resource.name)) + .map(|resource| PermissionToml { + target: ResourceTarget::Repo(resource.name.clone()), + level: permission.level, + }); + expanded.extend(permissions); + } + ResourceTargetVariant::Alerter => { + let permissions = all_resources + .alerters + .values() + .filter(|resource| regex.is_match(&resource.name)) + .map(|resource| PermissionToml { + target: ResourceTarget::Alerter(resource.name.clone()), + level: permission.level, + }); + expanded.extend(permissions); + } + ResourceTargetVariant::Procedure => { + let permissions = all_resources + .procedures + .values() + .filter(|resource| regex.is_match(&resource.name)) + .map(|resource| PermissionToml { + target: ResourceTarget::Procedure( + resource.name.clone(), + ), + level: permission.level, + }); + expanded.extend(permissions); + } + ResourceTargetVariant::ServerTemplate => { + let permissions = all_resources + .templates + .values() + .filter(|resource| regex.is_match(&resource.name)) + .map(|resource| PermissionToml { + target: ResourceTarget::ServerTemplate( + resource.name.clone(), + ), + level: permission.level, + }); + expanded.extend(permissions); + } + ResourceTargetVariant::ResourceSync => { + let permissions = all_resources + .syncs + .values() + .filter(|resource| regex.is_match(&resource.name)) + .map(|resource| PermissionToml { + target: ResourceTarget::ResourceSync( + resource.name.clone(), + ), + level: permission.level, + }); + expanded.extend(permissions); + } + ResourceTargetVariant::System => {} + } + } else { + // No regex + expanded.push(permission); + } + } + + Ok(expanded) +} + +type AllDiff = + HashMap; + +/// diffs user_group.all +fn diff_group_all( + original: &HashMap, + incoming: &HashMap, +) -> AllDiff { + let mut to_update = HashMap::new(); + + // need to compare both forward and backward because either hashmap could be sparse. + + // forward direction + for (variant, level) in incoming { + let original_level = original.get(variant).unwrap_or_default(); + if level == original_level { + continue; + } + to_update.insert(*variant, (*original_level, *level)); + } + + // backward direction + for (variant, level) in original { + let incoming_level = incoming.get(variant).unwrap_or_default(); + if level == incoming_level { + continue; + } + to_update.insert(*variant, (*level, *incoming_level)); + } + + to_update +} diff --git a/bin/core/src/resource/mod.rs b/bin/core/src/resource/mod.rs index d815082f0..8d295d2a0 100644 --- a/bin/core/src/resource/mod.rs +++ b/bin/core/src/resource/mod.rs @@ -33,7 +33,7 @@ use crate::{ helpers::{ create_permission, flatten_document, query::{ - get_resource_ids_for_non_admin, get_tag, + get_resource_ids_for_user, get_tag, get_user_permission_on_resource, id_or_name_filter, }, update::{add_update, make_update}, @@ -211,7 +211,7 @@ pub async fn get_check_permissions( return Ok(resource); } let permissions = get_user_permission_on_resource( - &user.id, + user, T::resource_type(), &resource.id, ) @@ -265,13 +265,9 @@ async fn list_full_for_user_using_document( mut filters: Document, user: &User, ) -> anyhow::Result>> { - if !user.admin && !core_config().transparent_mode { - let ids = - get_resource_ids_for_non_admin(&user.id, T::resource_type()) - .await? - .into_iter() - .flat_map(|id| ObjectId::from_str(&id)) - .collect::>(); + if let Some(ids) = + get_resource_ids_for_user(user, T::resource_type()).await? + { filters.insert("_id", doc! { "$in": ids }); } find_collect( diff --git a/bin/core/src/ws.rs b/bin/core/src/ws.rs index c8e5e351c..61c9c970a 100644 --- a/bin/core/src/ws.rs +++ b/bin/core/src/ws.rs @@ -208,7 +208,7 @@ async fn user_can_see_update( } let (variant, id) = update_target.extract_variant_id(); let permissions = - get_user_permission_on_resource(&user.id, variant, id).await?; + get_user_permission_on_resource(user, variant, id).await?; if permissions > PermissionLevel::None { Ok(()) } else { diff --git a/bin/migrator/src/legacy/v0/user.rs b/bin/migrator/src/legacy/v0/user.rs index d6e37bc03..e8682ffa9 100644 --- a/bin/migrator/src/legacy/v0/user.rs +++ b/bin/migrator/src/legacy/v0/user.rs @@ -106,11 +106,8 @@ impl TryFrom for monitor_client::entities::user::User { create_server_permissions: value.create_server_permissions, create_build_permissions: value.create_build_permissions, last_update_view: Default::default(), - recent_servers: Vec::new(), - recent_deployments: Vec::new(), - recent_builds: Vec::new(), - recent_repos: Vec::new(), - recent_procedures: Vec::new(), + recents: Default::default(), + all: Default::default(), updated_at: unix_from_monitor_ts(&value.updated_at)?, }; Ok(user) diff --git a/client/core/rs/src/api/read/user.rs b/client/core/rs/src/api/read/user.rs index 12a5231c3..6a96d5e68 100644 --- a/client/core/rs/src/api/read/user.rs +++ b/client/core/rs/src/api/read/user.rs @@ -22,8 +22,8 @@ pub type ListApiKeysResponse = Vec; // -/// Gets list of api keys for the user at ID. /// **Admin only.** +/// Gets list of api keys for the user. /// Will still fail if you call for a user_id that isn't a service user. /// Response: [ListApiKeysForServiceUserResponse] #[typeshare] @@ -33,8 +33,9 @@ pub type ListApiKeysResponse = Vec; #[empty_traits(MonitorReadRequest)] #[response(ListApiKeysForServiceUserResponse)] pub struct ListApiKeysForServiceUser { - /// The id of the user. - pub user_id: String, + /// Id or username + #[serde(alias = "id", alias = "username")] + pub user: String, } #[typeshare] @@ -42,8 +43,28 @@ pub type ListApiKeysForServiceUserResponse = Vec; // -/// Gets list of monitor users. /// **Admin only.** +/// Find a user. +/// Response: [FindUserResponse] +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Default, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(FindUserResponse)] +pub struct FindUser { + /// Id or username + #[serde(alias = "id", alias = "username")] + pub user: String, +} + +#[typeshare] +pub type FindUserResponse = User; + +// + +/// **Admin only.** +/// Gets list of monitor users. /// Response: [ListUsersResponse] #[typeshare] #[derive( diff --git a/client/core/rs/src/api/write/permissions.rs b/client/core/rs/src/api/write/permissions.rs index 4fa6ac6a8..a187a3884 100644 --- a/client/core/rs/src/api/write/permissions.rs +++ b/client/core/rs/src/api/write/permissions.rs @@ -5,13 +5,13 @@ use typeshare::typeshare; use crate::entities::{ permission::{PermissionLevel, UserTarget}, - update::ResourceTarget, + update::{ResourceTarget, ResourceTargetVariant}, NoData, }; use super::MonitorWriteRequest; -/// Update a user or user groups permission on a resource. +/// **Admin only.** Update a user or user groups permission on a resource. /// Response: [NoData]. #[typeshare] #[derive( @@ -33,7 +33,29 @@ pub type UpdatePermissionOnTargetResponse = NoData; // -/// Update a user's "base" permissions, eg. "enabled". +/// **Admin only.** Update a user or user groups base permission level on a resource type. +/// Response: [NoData]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(UpdatePermissionOnResourceTypeResponse)] +pub struct UpdatePermissionOnResourceType { + /// Specify the user or user group. + pub user_target: UserTarget, + /// The resource type: eg. Server, Build, Deployment, etc. + pub resource_type: ResourceTargetVariant, + /// The base permission level. + pub permission: PermissionLevel, +} + +#[typeshare] +pub type UpdatePermissionOnResourceTypeResponse = NoData; + +// + +/// **Admin only.** Update a user's "base" permissions, eg. "enabled". /// Response: [NoData]. #[typeshare] #[derive( diff --git a/client/core/rs/src/entities/toml.rs b/client/core/rs/src/entities/toml.rs index dbb28e851..a7993ea57 100644 --- a/client/core/rs/src/entities/toml.rs +++ b/client/core/rs/src/entities/toml.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use serde::{Deserialize, Serialize}; use super::{ @@ -6,7 +8,7 @@ use super::{ permission::PermissionLevel, procedure::PartialProcedureConfig, repo::PartialRepoConfig, server::PartialServerConfig, server_template::PartialServerTemplateConfig, - sync::PartialResourceSyncConfig, update::ResourceTarget, + sync::PartialResourceSyncConfig, update::{ResourceTarget, ResourceTargetVariant}, variable::Variable, }; @@ -137,13 +139,26 @@ pub struct UserGroupToml { #[serde(default)] pub users: Vec, + /// Give the user group elevated permissions on all resources of a certain type + #[serde(default)] + pub all: HashMap, + /// Permissions given to the group - #[serde(default, rename = "permission")] + #[serde(default, alias = "permission")] pub permissions: Vec, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct PermissionToml { + /// Id can be: + /// - resource name. `id = "abcd-build"` + /// - regex matching resource names. `id = "\^(.+)-build-([0-9]+)$\"` pub target: ResourceTarget, + + /// The permission level: + /// - None + /// - Read + /// - Execute + /// - Write pub level: PermissionLevel, } diff --git a/client/core/rs/src/entities/update.rs b/client/core/rs/src/entities/update.rs index 29bd2e2fd..9577d2601 100644 --- a/client/core/rs/src/entities/update.rs +++ b/client/core/rs/src/entities/update.rs @@ -11,7 +11,8 @@ use crate::entities::{ use super::{ alerter::Alerter, build::Build, builder::Builder, deployment::Deployment, procedure::Procedure, repo::Repo, - server::Server, server_template::ServerTemplate, Version, + server::Server, server_template::ServerTemplate, + sync::ResourceSync, Version, }; /// Represents an action performed by Monitor. @@ -188,18 +189,16 @@ impl Log { /// Used to reference a specific resource across all resource types #[typeshare] #[derive( - Serialize, - Deserialize, Debug, Clone, PartialEq, Eq, Hash, + Serialize, + Deserialize, EnumVariants, )] #[variant_derive( - Serialize, - Deserialize, Debug, Clone, Copy, @@ -207,6 +206,9 @@ impl Log { Eq, PartialOrd, Ord, + Hash, + Serialize, + Deserialize, Display, EnumString, AsRefStr @@ -303,6 +305,12 @@ impl From<&ServerTemplate> for ResourceTarget { } } +impl From<&ResourceSync> for ResourceTarget { + fn from(resource_sync: &ResourceSync) -> Self { + Self::ResourceSync(resource_sync.id.clone()) + } +} + /// An update's status #[typeshare] #[derive( diff --git a/client/core/rs/src/entities/user.rs b/client/core/rs/src/entities/user.rs index dc7b8e7ca..194256cc6 100644 --- a/client/core/rs/src/entities/user.rs +++ b/client/core/rs/src/entities/user.rs @@ -1,10 +1,14 @@ -use std::sync::OnceLock; +use std::{collections::HashMap, sync::OnceLock}; use serde::{Deserialize, Serialize}; use typeshare::typeshare; use crate::entities::{MongoId, I64}; +use super::{ + permission::PermissionLevel, update::ResourceTargetVariant, +}; + #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Default)] #[cfg_attr( @@ -54,25 +58,13 @@ pub struct User { #[serde(default)] pub last_update_view: I64, - /// Recently viewed server ids + /// Recently viewed ids #[serde(default)] - pub recent_servers: Vec, + pub recents: HashMap>, - /// Recently viewed deployment ids + /// Give the user elevated permissions on all resources of a certain type #[serde(default)] - pub recent_deployments: Vec, - - /// Recently viewed build ids - #[serde(default)] - pub recent_builds: Vec, - - /// Recently viewed repo ids - #[serde(default)] - pub recent_repos: Vec, - - /// Recently viewed procedure ids - #[serde(default)] - pub recent_procedures: Vec, + pub all: HashMap, #[serde(default)] pub updated_at: I64, diff --git a/client/core/rs/src/entities/user_group.rs b/client/core/rs/src/entities/user_group.rs index 83b1f0807..1931e9852 100644 --- a/client/core/rs/src/entities/user_group.rs +++ b/client/core/rs/src/entities/user_group.rs @@ -1,8 +1,16 @@ +use std::collections::HashMap; + use serde::{Deserialize, Serialize}; use typeshare::typeshare; -use super::{MongoId, I64}; +use super::{permission::PermissionLevel, update::ResourceTargetVariant, MongoId, I64}; +/// Permission users at the group level. +/// +/// All users that are part of a group inherit the group's permissions. +/// A user can be a part of multiple groups. A user's permission on a particular resource +/// will be resolved to be the maximum permission level between the user's own permissions and +/// any groups they are a part of. #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Default)] #[cfg_attr( @@ -21,13 +29,19 @@ pub struct UserGroup { )] pub id: MongoId, + /// A name for the user group #[cfg_attr(feature = "mongo", unique_index)] pub name: String, - /// User ids + /// User ids of group members #[cfg_attr(feature = "mongo", index)] pub users: Vec, + /// Give the user group elevated permissions on all resources of a certain type + #[serde(default)] + pub all: HashMap, + + /// Unix time (ms) when user group last updated #[serde(default)] pub updated_at: I64, } diff --git a/client/core/ts/src/responses.ts b/client/core/ts/src/responses.ts index 12d87d300..163ac009a 100644 --- a/client/core/ts/src/responses.ts +++ b/client/core/ts/src/responses.ts @@ -23,6 +23,7 @@ export type ReadResponses = { // ==== USER ==== GetUsername: Types.GetUsernameResponse; GetPermissionLevel: Types.GetPermissionLevelResponse; + FindUser: Types.FindUserResponse; ListUsers: Types.ListUsersResponse; ListApiKeys: Types.ListApiKeysResponse; ListApiKeysForServiceUser: Types.ListApiKeysForServiceUserResponse; @@ -161,6 +162,7 @@ export type WriteResponses = { // ==== PERMISSIONS ==== UpdateUserBasePermissions: Types.UpdateUserBasePermissionsResponse; + UpdatePermissionOnResourceType: Types.UpdatePermissionOnResourceTypeResponse; UpdatePermissionOnTarget: Types.UpdatePermissionOnTargetResponse; // ==== DESCRIPTION ==== diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index 5c5d27761..ac5060763 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -45,6 +45,18 @@ export type UserConfig = export type I64 = number; +/** The levels of permission that a User or UserGroup can have on a resource. */ +export enum PermissionLevel { + /** No permissions. */ + None = "None", + /** Can see the rousource */ + Read = "Read", + /** Can execute actions on the resource */ + Execute = "Execute", + /** Can update the resource configuration */ + Write = "Write", +} + export interface User { /** * The Mongo ID of the User. @@ -66,16 +78,10 @@ export interface User { config: UserConfig; /** When the user last opened updates dropdown. */ last_update_view?: I64; - /** Recently viewed server ids */ - recent_servers?: string[]; - /** Recently viewed deployment ids */ - recent_deployments?: string[]; - /** Recently viewed build ids */ - recent_builds?: string[]; - /** Recently viewed repo ids */ - recent_repos?: string[]; - /** Recently viewed procedure ids */ - recent_procedures?: string[]; + /** Recently viewed ids */ + recents?: Record; + /** Give the user elevated permissions on all resources of a certain type */ + all?: Record; updated_at?: I64; } @@ -202,8 +208,6 @@ export type AlertData = name: string; /** The version that failed to build */ version: Version; - /** The reason build failed */ - err?: Log; }}; /** Representation of an alert in the system. */ @@ -697,18 +701,6 @@ export type UserTarget = /** UserGroup Id */ | { type: "UserGroup", id: string }; -/** The levels of permission that a User or UserGroup can have on a resource. */ -export enum PermissionLevel { - /** No permissions. */ - None = "None", - /** Can see the rousource */ - Read = "Read", - /** Can execute actions on the resource */ - Execute = "Execute", - /** Can update the resource configuration */ - Write = "Write", -} - /** Representation of a User or UserGroups permission on a resource. */ export interface Permission { /** The id of the permission document */ @@ -1460,8 +1452,18 @@ export type ListApiKeysResponse = ApiKey[]; export type ListApiKeysForServiceUserResponse = ApiKey[]; +export type FindUserResponse = User; + export type ListUsersResponse = User[]; +/** + * Permission users at the group level. + * + * All users that are part of a group inherit the group's permissions. + * A user can be a part of multiple groups. A user's permission on a particular resource + * will be resolved to be the maximum permission level between the user's own permissions and + * any groups they are a part of. + */ export interface UserGroup { /** * The Mongo ID of the UserGroup. @@ -1469,9 +1471,13 @@ export interface UserGroup { * `{ "_id": { "$oid": "..." }, ...(rest of serialized User) }` */ _id?: MongoId; + /** A name for the user group */ name: string; - /** User ids */ + /** User ids of group members */ users: string[]; + /** Give the user group elevated permissions on all resources of a certain type */ + all?: Record; + /** Unix time (ms) when user group last updated */ updated_at?: I64; } @@ -1528,6 +1534,8 @@ export type UpdateDescriptionResponse = NoData; export type UpdatePermissionOnTargetResponse = NoData; +export type UpdatePermissionOnResourceTypeResponse = NoData; + export type UpdateUserBasePermissionsResponse = NoData; export type CreateProcedureResponse = Procedure; @@ -2928,19 +2936,29 @@ export interface ListApiKeys { } /** - * Gets list of api keys for the user at ID. * **Admin only.** + * Gets list of api keys for the user. * Will still fail if you call for a user_id that isn't a service user. * Response: [ListApiKeysForServiceUserResponse] */ export interface ListApiKeysForServiceUser { - /** The id of the user. */ - user_id: string; + /** Id or username */ + user: string; } /** - * Gets list of monitor users. * **Admin only.** + * Find a user. + * Response: [FindUserResponse] + */ +export interface FindUser { + /** Id or username */ + user: string; +} + +/** + * **Admin only.** + * Gets list of monitor users. * Response: [ListUsersResponse] */ export interface ListUsers { @@ -3301,7 +3319,7 @@ export interface UpdateDescription { } /** - * Update a user or user groups permission on a resource. + * **Admin only.** Update a user or user groups permission on a resource. * Response: [NoData]. */ export interface UpdatePermissionOnTarget { @@ -3314,7 +3332,20 @@ export interface UpdatePermissionOnTarget { } /** - * Update a user's "base" permissions, eg. "enabled". + * **Admin only.** Update a user or user groups base permission level on a resource type. + * Response: [NoData]. + */ +export interface UpdatePermissionOnResourceType { + /** Specify the user or user group. */ + user_target: UserTarget; + /** The resource type: eg. Server, Build, Deployment, etc. */ + resource_type: ResourceTarget["type"]; + /** The base permission level. */ + permission: PermissionLevel; +} + +/** + * **Admin only.** Update a user's "base" permissions, eg. "enabled". * Response: [NoData]. */ export interface UpdateUserBasePermissions { @@ -4089,6 +4120,7 @@ export type ReadRequest = | { type: "GetAvailableAwsEcrLabels", params: GetAvailableAwsEcrLabels } | { type: "GetUsername", params: GetUsername } | { type: "GetPermissionLevel", params: GetPermissionLevel } + | { type: "FindUser", params: FindUser } | { type: "ListUsers", params: ListUsers } | { type: "ListApiKeys", params: ListApiKeys } | { type: "ListApiKeysForServiceUser", params: ListApiKeysForServiceUser } @@ -4193,6 +4225,7 @@ export type WriteRequest = | { type: "RemoveUserFromUserGroup", params: RemoveUserFromUserGroup } | { type: "SetUsersInUserGroup", params: SetUsersInUserGroup } | { type: "UpdateUserBasePermissions", params: UpdateUserBasePermissions } + | { type: "UpdatePermissionOnResourceType", params: UpdatePermissionOnResourceType } | { type: "UpdatePermissionOnTarget", params: UpdatePermissionOnTarget } | { type: "UpdateDescription", params: UpdateDescription } | { type: "CreateServer", params: CreateServer } diff --git a/docsite/docs/sync-resources.md b/docsite/docs/sync-resources.md index 49e6051fe..6ffa2e99c 100644 --- a/docsite/docs/sync-resources.md +++ b/docsite/docs/sync-resources.md @@ -12,7 +12,11 @@ The UI will display the computed sync actions and only execute them upon manual Or the sync execution Github webhook may be configured on the Github repo to automatically execute syncs upon pushes to the configured branch. -## Example file: +## Example Declarations + +### Server + +- [Server config schema](https://docs.rs/monitor_client/latest/monitor_client/entities/server/struct.ServerConfig.html) ```toml [[server]] # Declare a new server @@ -21,7 +25,30 @@ description = "the main mogh server" tags = ["monitor"] config.address = "http://localhost:8120" config.region = "AshburnDc1" -config.enabled = true +config.enabled = true # default: false +``` + +### Builder and build + +- [Builder config schema](https://docs.rs/monitor_client/latest/monitor_client/entities/builder/struct.BuilderConfig.html) +- [Build config schema](https://docs.rs/monitor_client/latest/monitor_client/entities/build/struct.BuildConfig.html) + +```toml +[[builder]] # Declare a builder +name = "builder-01" +tags = [] +config.type = "Aws" +config.params.region = "us-east-2" +config.params.ami_id = "ami-0e9bd154667944680" +# These things come from your specific setup +config.params.subnet_id = "subnet-xxxxxxxxxxxxxxxxxx" +config.params.key_pair_name = "xxxxxxxx" +config.params.assign_public_ip = true +config.params.use_public_ip = true +config.params.security_group_ids = [ + "sg-xxxxxxxxxxxxxxxxxx", + "sg-xxxxxxxxxxxxxxxxxx" +] ## @@ -40,9 +67,13 @@ config.labels = """ org.opencontainers.image.source = https://github.com/mbecker20/test_logger org.opencontainers.image.description = Logs randomly at INFO, WARN, ERROR levels to test logging setups org.opencontainers.image.licenses = GPL-3.0""" +``` -## +### Deployments +- [Deployment config schema](https://docs.rs/monitor_client/latest/monitor_client/entities/deployment/struct.DeploymentConfig.html) + +```toml [[variable]] # Declare variables name = "OTLP_ENDPOINT" value = "http://localhost:4317" @@ -94,21 +125,13 @@ VARIABLE_1 = value_1 VARIABLE_2 = value_2""" # Set Docker labels config.labels = "deployment.type = logger" +``` -## +### Procedure -[[repo]] -name = "monitor-periphery" -description = "Builds new versions of the periphery binary. Requires Rust installed on the host." -tags = ["monitor"] -config.server_id = "server-01" -config.repo = "mbecker20/monitor" -# Run an action after the repo is pulled -config.on_pull.path = "." -config.on_pull.command = "/root/.cargo/bin/cargo build -p monitor_periphery --release && cp ./target/release/periphery /root/periphery" - -## +- [Procedure config schema](https://docs.rs/monitor_client/latest/monitor_client/entities/procedure/struct.ProcedureConfig.html) +```toml [[procedure]] name = "test-procedure" description = "Do some things in a specific order" @@ -137,22 +160,40 @@ enabled = true executions = [ { execution.type = "Deploy", execution.params.deployment = "test-logger-02", enabled = true } ] - -## - -[[builder]] # Declare a builder -name = "builder-01" -tags = [] -config.type = "Aws" -config.params.region = "us-east-2" -config.params.ami_id = "ami-0e9bd154667944680" -# These things come from your specific setup -config.params.subnet_id = "subnet-xxxxxxxxxxxxxxxxxx" -config.params.key_pair_name = "xxxxxxxx" -config.params.assign_public_ip = true -config.params.use_public_ip = true -config.params.security_group_ids = [ - "sg-xxxxxxxxxxxxxxxxxx", - "sg-xxxxxxxxxxxxxxxxxx" -] ``` + +### Repo + +- [Repo config schema](https://docs.rs/monitor_client/latest/monitor_client/entities/repo/struct.RepoConfig.html) + +```toml +[[repo]] +name = "monitor-periphery" +description = "Builds new versions of the periphery binary. Requires Rust installed on the host." +tags = ["monitor"] +config.server_id = "server-01" +config.repo = "mbecker20/monitor" +# Run an action after the repo is pulled +config.on_pull.path = "." +config.on_pull.command = "/root/.cargo/bin/cargo build -p monitor_periphery --release && cp ./target/release/periphery /root/periphery" +``` + +### User Group: + +- [UserGroup schema](https://docs.rs/monitor_client/latest/monitor_client/entities/toml/struct.UserGroupToml.html) + +```toml +[[user_group]] +name = "groupo" +users = ["mbecker20", "karamvirsingh98"] +# Attach base level of Execute on all builds +all.Build = "Execute" +all.Alerter = "Write" +permissions = [ + # Attach permissions to specific resources by name + { target.type = "Repo", target.id = "monitor-periphery", level = "Execute" }, + # Attach permissions to many resources with name matching regex (this uses '^(.+)-(.+)$' as regex expression) + { target.type = "Server", target.id = "\\^(.+)-(.+)$\\", level = "Read" }, + { target.type = "Deployment", target.id = "\\^immich\\", level = "Execute" }, +] +``` \ No newline at end of file diff --git a/frontend/src/components/config/util.tsx b/frontend/src/components/config/util.tsx index b240d72ca..e7320d4dd 100644 --- a/frontend/src/components/config/util.tsx +++ b/frontend/src/components/config/util.tsx @@ -406,6 +406,7 @@ export const SystemCommand = ({ value={value?.command} onUpdate={(command) => set({ ...(value || {}), command })} triggerClassName="w-[200px] lg:w-[300px] xl:w-[400px]" + disabled={disabled} /> @@ -588,7 +589,7 @@ const REGISTRY_TYPES: Types.ImageRegistry["type"][] = [ "None", "DockerHub", "Ghcr", - "AwsEcr" + "AwsEcr", ]; const RegistryTypeSelector = ({ @@ -768,3 +769,33 @@ export const SecretSelector = ({ ); }; + +export const PermissionLevelSelector = ({ + level, + onSelect, +}: { + level: Types.PermissionLevel; + onSelect: (level: Types.PermissionLevel) => void; +}) => { + return ( + + ); +}; diff --git a/frontend/src/components/layouts.tsx b/frontend/src/components/layouts.tsx index 3aac29a71..d54f1ca55 100644 --- a/frontend/src/components/layouts.tsx +++ b/frontend/src/components/layouts.tsx @@ -130,7 +130,7 @@ export const Section = ({ children, }: SectionProps) => (
-
+
{title || icon ? (
{icon} diff --git a/frontend/src/components/resources/alerter/config/resources.tsx b/frontend/src/components/resources/alerter/config/resources.tsx index 06ae005c7..8d47c8794 100644 --- a/frontend/src/components/resources/alerter/config/resources.tsx +++ b/frontend/src/components/resources/alerter/config/resources.tsx @@ -34,6 +34,7 @@ export const ResourcesConfig = ({ const servers = useRead("ListServers", {}).data ?? []; const deployments = useRead("ListDeployments", {}).data ?? []; const builds = useRead("ListBuilds", {}).data ?? []; + const syncs = useRead("ListResourceSyncs", {}).data ?? []; const all_resources = [ ...servers.map((server) => { return { @@ -65,6 +66,16 @@ export const ResourcesConfig = ({ ? true : false, })), + ...syncs.map((sync) => ({ + type: "ResourceSync", + id: sync.id, + name: sync.name.toLowerCase(), + enabled: resources.find( + (r) => r.type === "ResourceSync" && r.id === sync.id + ) + ? true + : false, + })), ]; const searchSplit = search.split(" "); const filtered_resources = searchSplit.length diff --git a/frontend/src/components/users/hooks.ts b/frontend/src/components/users/hooks.ts index bd2743658..8e99fc734 100644 --- a/frontend/src/components/users/hooks.ts +++ b/frontend/src/components/users/hooks.ts @@ -13,6 +13,8 @@ export const useUserTargetPermissions = (user_target: Types.UserTarget) => { const procedures = useRead("ListProcedures", {}).data; const builders = useRead("ListBuilders", {}).data; const alerters = useRead("ListAlerters", {}).data; + const templates = useRead("ListServerTemplates", {}).data; + const syncs = useRead("ListResourceSyncs", {}).data; const perms: (Types.Permission & { name: string })[] = []; addPerms(user_target, permissions, "Server", servers, perms); addPerms(user_target, permissions, "Deployment", deployments, perms); @@ -21,6 +23,8 @@ export const useUserTargetPermissions = (user_target: Types.UserTarget) => { addPerms(user_target, permissions, "Procedure", procedures, perms); addPerms(user_target, permissions, "Builder", builders, perms); addPerms(user_target, permissions, "Alerter", alerters, perms); + addPerms(user_target, permissions, "ServerTemplate", templates, perms); + addPerms(user_target, permissions, "ResourceSync", syncs, perms); return perms; }; diff --git a/frontend/src/components/users/permissions-table.tsx b/frontend/src/components/users/permissions-table.tsx index 891bd9fce..e0d77cb42 100644 --- a/frontend/src/components/users/permissions-table.tsx +++ b/frontend/src/components/users/permissions-table.tsx @@ -19,6 +19,7 @@ import { SelectTrigger, SelectValue, } from "@ui/select"; +import { PermissionLevelSelector } from "@components/config/util"; export const PermissionsTable = ({ user_target, @@ -42,7 +43,7 @@ export const PermissionsTable = ({ }); return (
), cell: ({ row: { original: permission } }) => ( - + /> ), }, ]} diff --git a/frontend/src/components/users/resource-type-permissions.tsx b/frontend/src/components/users/resource-type-permissions.tsx new file mode 100644 index 000000000..270b9d0e4 --- /dev/null +++ b/frontend/src/components/users/resource-type-permissions.tsx @@ -0,0 +1,100 @@ +import { PermissionLevelSelector } from "@components/config/util"; +import { Section } from "@components/layouts"; +import { useInvalidate, useRead, useWrite } from "@lib/hooks"; +import { RESOURCE_TARGETS } from "@lib/utils"; +import { Types } from "@monitor/client"; +import { useToast } from "@ui/use-toast"; + +export const UserTargetPermissionsOnResourceTypes = ({ + user_target, +}: { + user_target: Types.UserTarget; +}) => { + const { toast } = useToast(); + const inv = useInvalidate(); + + const { mutate } = useWrite("UpdatePermissionOnResourceType", { + onSuccess: () => { + toast({ title: "Updated permissions on target" }); + if (user_target.type === "User") { + inv(["FindUser", { user: user_target.id }]); + } else if (user_target.type === "UserGroup") { + inv(["GetUserGroup", { user_group: user_target.id }]); + } + }, + }); + + const update = (resource_type, permission) => + mutate({ user_target, resource_type, permission }); + + if (user_target.type === "User") { + return ( + + ); + } else if (user_target.type === "UserGroup") { + return ( + + ); + } +}; + +const UserPermissionsOnResourceType = ({ + user_id, + update, +}: { + user_id: string; + update: ( + resource_type: Types.ResourceTarget["type"], + permission: Types.PermissionLevel + ) => void; +}) => { + const user = useRead("FindUser", { user: user_id }).data; + return ; +}; + +const UserGroupPermissionsOnResourceType = ({ + group_id, + update, +}: { + group_id: string; + update: ( + resource_type: Types.ResourceTarget["type"], + permission: Types.PermissionLevel + ) => void; +}) => { + const group = useRead("GetUserGroup", { user_group: group_id }).data; + return ; +}; + +const PermissionsOnResourceType = ({ + all, + update, +}: { + all: Types.User["all"]; + update: ( + resource_type: Types.ResourceTarget["type"], + permission: Types.PermissionLevel + ) => void; +}) => { + return ( +
+
+ {RESOURCE_TARGETS.map((type) => { + const level = all?.[type] ?? Types.PermissionLevel.None; + return ( +
+ {type}: + update(type, level)} + /> +
+ ); + })} +
+
+ ); +}; diff --git a/frontend/src/components/util.tsx b/frontend/src/components/util.tsx index fa9fb8f29..8b5fbed75 100644 --- a/frontend/src/components/util.tsx +++ b/frontend/src/components/util.tsx @@ -348,7 +348,7 @@ export const TextUpdateMenu = ({
diff --git a/frontend/src/pages/home/dashboard.tsx b/frontend/src/pages/home/dashboard.tsx index 8b552de8b..229a6c0b2 100644 --- a/frontend/src/pages/home/dashboard.tsx +++ b/frontend/src/pages/home/dashboard.tsx @@ -32,10 +32,7 @@ export const Dashboard = () => { }; const ResourceRow = ({ type }: { type: UsableResource }) => { - const recents = useUser().data?.[`recent_${type.toLowerCase()}s`]?.slice( - 0, - 6 - ) as string[] | undefined; + const recents = useUser().data?.recents?.[type]?.slice(0, 6); const resources = useRead(`List${type}s`, {}) .data?.filter((r) => !recents?.includes(r.id)) .map((r) => r.id); diff --git a/frontend/src/pages/user-group.tsx b/frontend/src/pages/user-group.tsx index a2b66f7fe..b50b56a89 100644 --- a/frontend/src/pages/user-group.tsx +++ b/frontend/src/pages/user-group.tsx @@ -1,3 +1,4 @@ +import { UserTargetPermissionsOnResourceTypes } from "@components/users/resource-type-permissions"; import { ExportButton } from "@components/export"; import { Page, Section } from "@components/layouts"; import { PermissionsTable } from "@components/users/permissions-table"; @@ -85,6 +86,9 @@ export const UserGroupPage = () => { } />
+
diff --git a/frontend/src/pages/user.tsx b/frontend/src/pages/user.tsx index aed3169c6..e5a75957a 100644 --- a/frontend/src/pages/user.tsx +++ b/frontend/src/pages/user.tsx @@ -1,3 +1,4 @@ +import { UserTargetPermissionsOnResourceTypes } from "@components/users/resource-type-permissions"; import { KeysTable } from "@components/keys/table"; import { Page, Section } from "@components/layouts"; import { PermissionsTable } from "@components/users/permissions-table"; @@ -10,8 +11,9 @@ import { useInvalidate, useRead, useWrite } from "@lib/hooks"; import { Label } from "@ui/label"; import { Switch } from "@ui/switch"; import { useToast } from "@ui/use-toast"; -import { Key, UserCheck, UserMinus } from "lucide-react"; -import { useParams } from "react-router-dom"; +import { Key, UserCheck, UserMinus, Users } from "lucide-react"; +import { Link, useParams } from "react-router-dom"; +import { Button } from "@ui/button"; export const UserPage = () => { const { toast } = useToast(); @@ -21,14 +23,10 @@ export const UserPage = () => { (user) => user._id?.$oid === user_id ); const { mutate } = useWrite("UpdateUserBasePermissions", { - onSuccess: () => inv(["ListUsers"]), - onError: (e) => { - console.log("update user permission failure", e); - toast({ - title: "Failed to update user permissions", - description: "See console for details", - variant: "destructive", - }); + onSuccess: () => { + inv(["FindUser", { user: user_id }]); + inv(["ListUsers"]); + toast({ title: "Modify user base permissions" }); }, }); const enabledClass = user?.enabled ? "text-green-500" : "text-red-500"; @@ -84,15 +82,21 @@ export const UserPage = () => { } > {user.config.type === "Service" && } - {!user.admin && ( - + {user.enabled && !user.admin && ( + <> + + + + )} ); }; const ApiKeysTable = ({ user_id }: { user_id: string }) => { - const keys = useRead("ListApiKeysForServiceUser", { user_id }).data; + const keys = useRead("ListApiKeysForServiceUser", { user: user_id }).data; return (
{
); }; + +const Groups = ({ user_id }: { user_id: string }) => { + const groups = useRead("ListUserGroups", {}).data?.filter((group) => + group.users.includes(user_id) + ); + if (!groups || groups.length === 0) { + return null; + } + return ( +
+
+ {groups.map((group) => ( + + + + ))} +
+
+ ); +};