From 0d0dc3e6440afc1640e8f42dbbb380e377615aaa Mon Sep 17 00:00:00 2001 From: Florian Amsallem Date: Fri, 15 Nov 2024 18:22:22 +0100 Subject: [PATCH] editoast: add group and user cli Signed-off-by: Florian Amsallem --- editoast/src/client/group.rs | 175 +++++++++++++++++++++++++++++++++++ editoast/src/client/mod.rs | 8 ++ editoast/src/client/user.rs | 79 ++++++++++++++++ editoast/src/main.rs | 32 +++++++ 4 files changed, 294 insertions(+) create mode 100644 editoast/src/client/group.rs create mode 100644 editoast/src/client/user.rs diff --git a/editoast/src/client/group.rs b/editoast/src/client/group.rs new file mode 100644 index 00000000000..3dae6b0f3f1 --- /dev/null +++ b/editoast/src/client/group.rs @@ -0,0 +1,175 @@ +use anyhow::anyhow; +use anyhow::bail; +use clap::Args; +use clap::Subcommand; +use diesel::delete; +use diesel::insert_into; +use diesel::prelude::*; +use diesel_async::scoped_futures::ScopedFutureExt; +use diesel_async::RunQueryDsl; +use editoast_authz::authorizer::StorageDriver; +use editoast_authz::BuiltinRole; +use editoast_models::tables::authn_group; +use editoast_models::tables::authn_group_membership; +use editoast_models::tables::authn_subject; +use editoast_models::DbConnectionPoolV2; +use std::ops::DerefMut; +use std::sync::Arc; + +use crate::models::auth::PgAuthDriver; + +#[derive(Debug, Subcommand)] +pub enum GroupCommand { + /// Create a group + Create(CreateArgs), + /// List groups + List, + /// Add members to a group + Include(IncludeArgs), + /// Remove members to a group + Exclude(ExcludeArgs), +} + +#[derive(Debug, Args)] +pub struct CreateArgs { + /// Group name + name: String, +} + +#[derive(Debug, Args)] +pub struct IncludeArgs { + /// Group name + group_name: String, + /// Users to add + users: Vec, +} + +#[derive(Debug, Args)] +pub struct ExcludeArgs { + /// Group name + group_name: String, + /// Users to remove + users: Vec, +} + +pub async fn create_group(args: CreateArgs, pool: Arc) -> anyhow::Result<()> { + let conn = pool.get().await?; + + conn.transaction(|conn| { + async move { + let id: i64 = insert_into(authn_subject::table) + .default_values() + .returning(authn_subject::id) + .get_result(&mut conn.clone().write().await) + .await?; + insert_into(authn_group::table) + .values((authn_group::id.eq(id), authn_group::name.eq(&args.name))) + .execute(conn.write().await.deref_mut()) + .await?; + println!(r#"Group "{}" created with id [{}]"#, &args.name, id); + Ok(()) + } + .scope_boxed() + }) + .await +} + +pub async fn list_group(pool: Arc) -> anyhow::Result<()> { + let conn = pool.get().await?; + + let groups = authn_group::table + .select(authn_group::all_columns) + .load::<(i64, String)>(conn.write().await.deref_mut()) + .await?; + if groups.is_empty() { + println!("No group found."); + return Ok(()); + } + for (id, name) in &groups { + println!("[{}]: {}", id, name); + } + Ok(()) +} + +/// Exclude users from a group +pub async fn exclude_group(args: ExcludeArgs, pool: Arc) -> anyhow::Result<()> { + if args.users.is_empty() { + bail!("No user specified"); + } + + let conn = pool.get().await?; + + let gid = authn_group::table + .select(authn_group::id) + .filter(authn_group::name.eq(&args.group_name)) + .first::(conn.write().await.deref_mut()) + .await?; + + let driver = PgAuthDriver::::new(pool); + let mut conds = vec![]; + for user in &args.users { + let uid = if let Ok(id) = user.parse::() { + id + } else { + let uid = driver.get_user_id(user).await?; + uid.ok_or_else(|| anyhow!("No user with identity '{user}' found"))? + }; + conds.push( + authn_group_membership::group + .eq(gid) + .and(authn_group_membership::user.eq(uid)), + ); + } + let mut expr = delete(authn_group_membership::table) + .filter(conds[0]) + .into_boxed(); + + for cond in conds.iter().skip(1) { + expr = expr.or_filter(*cond); + } + + let deleted = expr.execute(conn.write().await.deref_mut()).await?; + println!( + "{} user(s) removed from group '{}'", + deleted, args.group_name + ); + + Ok(()) +} + +/// Include users in a group +pub async fn include_group(args: IncludeArgs, pool: Arc) -> anyhow::Result<()> { + if args.users.is_empty() { + bail!("No user specified"); + } + + let conn = pool.get().await?; + + let gid = authn_group::table + .select(authn_group::id) + .filter(authn_group::name.eq(&args.group_name)) + .first::(conn.write().await.deref_mut()) + .await?; + + let driver = PgAuthDriver::::new(pool); + let mut values = vec![]; + for user in &args.users { + let uid = if let Ok(id) = user.parse::() { + id + } else { + let uid = driver.get_user_id(user).await?; + uid.ok_or_else(|| anyhow!("No user with identity '{user}' found"))? + }; + values.push(( + authn_group_membership::user.eq(uid), + authn_group_membership::group.eq(gid), + )); + } + + insert_into(authn_group_membership::table) + .values(values) + .execute(conn.write().await.deref_mut()) + .await?; + + Ok(()) +} diff --git a/editoast/src/client/mod.rs b/editoast/src/client/mod.rs index 7cb710a56d7..6c91a73d705 100644 --- a/editoast/src/client/mod.rs +++ b/editoast/src/client/mod.rs @@ -1,4 +1,5 @@ pub mod electrical_profiles_commands; +pub mod group; pub mod healthcheck; pub mod import_rolling_stock; pub mod infra_commands; @@ -9,6 +10,7 @@ pub mod search_commands; pub mod stdcm_search_env_commands; mod telemetry_config; pub mod timetables_commands; +pub mod user; mod valkey_config; use std::env; @@ -20,6 +22,7 @@ use clap::Subcommand; use clap::ValueEnum; use derivative::Derivative; use editoast_derive::EditoastError; +use group::GroupCommand; use import_rolling_stock::ImportRollingStockArgs; use infra_commands::InfraCommands; pub use postgres_config::PostgresConfig; @@ -33,6 +36,7 @@ pub use telemetry_config::TelemetryKind; use thiserror::Error; use timetables_commands::TimetablesCommands; use url::Url; +use user::UserCommand; pub use valkey_config::ValkeyConfig; use crate::error::Result; @@ -90,6 +94,10 @@ pub enum Commands { STDCMSearchEnv(StdcmSearchEnvCommands), #[command(subcommand, about, long_about = "Roles related commands")] Roles(RolesCommand), + #[command(subcommand, about, long_about = "Group related commands")] + Group(GroupCommand), + #[command(subcommand, about, long_about = "User related commands")] + User(UserCommand), #[command(about, long_about = "Healthcheck")] Healthcheck(CoreArgs), } diff --git a/editoast/src/client/user.rs b/editoast/src/client/user.rs new file mode 100644 index 00000000000..102767c6ca1 --- /dev/null +++ b/editoast/src/client/user.rs @@ -0,0 +1,79 @@ +use clap::Args; +use clap::Subcommand; +use diesel::{ExpressionMethods, QueryDsl}; +use diesel_async::RunQueryDsl; +use editoast_authz::authorizer::{StorageDriver, UserInfo}; +use editoast_authz::BuiltinRole; +use editoast_models::tables::authn_group_membership; +use editoast_models::tables::authn_user; +use editoast_models::DbConnectionPoolV2; +use std::ops::DerefMut; +use std::sync::Arc; + +use crate::models::auth::PgAuthDriver; + +#[derive(Debug, Subcommand)] +pub enum UserCommand { + /// List users + List(ListArgs), + /// Add a user + Add(AddArgs), +} + +#[derive(Debug, Args)] +pub struct ListArgs { + /// Filter out users that are already in a group + #[arg(long)] + without_groups: bool, +} + +#[derive(Debug, Args)] +pub struct AddArgs { + /// Identity of the user + identity: String, + /// Name of the user + name: Option, +} + +/// List users +pub async fn list_user(args: ListArgs, pool: Arc) -> anyhow::Result<()> { + let conn = pool.get().await?; + let users_query = authn_user::table + .left_join(authn_group_membership::table) + .select(authn_user::all_columns); + + let users = if args.without_groups { + users_query + .filter(authn_group_membership::user.is_null()) + .load::<(i64, String, Option)>(conn.write().await.deref_mut()) + .await? + } else { + users_query + .load::<(i64, String, Option)>(conn.write().await.deref_mut()) + .await? + }; + for (id, identity, name) in &users { + let display = match name { + Some(name) => format!("{} ({})", identity, name), + None => identity.to_string(), + }; + println!("[{}]: {}", id, display); + } + if users.is_empty() { + println!("No user found"); + } + Ok(()) +} + +/// Add a user +pub async fn add_user(args: AddArgs, pool: Arc) -> anyhow::Result<()> { + let driver = PgAuthDriver::::new(pool); + + let user_info = UserInfo { + identity: args.identity, + name: args.name.unwrap_or_default(), + }; + let subject_id = driver.ensure_user(&user_info).await?; + println!("User added with id: {}", subject_id); + Ok(()) +} diff --git a/editoast/src/main.rs b/editoast/src/main.rs index 7041fd1eb04..c4763077a02 100644 --- a/editoast/src/main.rs +++ b/editoast/src/main.rs @@ -13,6 +13,8 @@ mod views; use clap::Parser; use client::electrical_profiles_commands::*; +use client::group; +use client::group::GroupCommand; use client::healthcheck::healthcheck_cmd; use client::import_rolling_stock::*; use client::infra_commands::*; @@ -23,6 +25,8 @@ use client::runserver::runserver; use client::search_commands::*; use client::stdcm_search_env_commands::handle_stdcm_search_env_command; use client::timetables_commands::*; +use client::user; +use client::user::UserCommand; use client::Client; use client::Color; use client::Commands; @@ -231,6 +235,34 @@ async fn run() -> Result<(), Box> { .map_err(Into::into) } }, + Commands::Group(group_command) => match group_command { + GroupCommand::Create(create_args) => { + group::create_group(create_args, Arc::new(db_pool)) + .await + .map_err(Into::into) + } + GroupCommand::List => group::list_group(Arc::new(db_pool)) + .await + .map_err(Into::into), + GroupCommand::Include(include_args) => { + group::include_group(include_args, Arc::new(db_pool)) + .await + .map_err(Into::into) + } + GroupCommand::Exclude(exclude_args) => { + group::exclude_group(exclude_args, Arc::new(db_pool)) + .await + .map_err(Into::into) + } + }, + Commands::User(user_command) => match user_command { + UserCommand::List(list_args) => user::list_user(list_args, Arc::new(db_pool)) + .await + .map_err(Into::into), + UserCommand::Add(add_args) => user::add_user(add_args, Arc::new(db_pool)) + .await + .map_err(Into::into), + }, Commands::Healthcheck(core_config) => { healthcheck_cmd(db_pool.into(), valkey_config, core_config) .await