Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add group cli to editoast #9734

Merged
merged 1 commit into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 175 additions & 0 deletions editoast/src/client/group.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
}

#[derive(Debug, Args)]
pub struct ExcludeArgs {
/// Group name
group_name: String,
/// Users to remove
users: Vec<String>,
}

pub async fn create_group(args: CreateArgs, pool: Arc<DbConnectionPoolV2>) -> 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<DbConnectionPoolV2>) -> 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<DbConnectionPoolV2>) -> 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::<i64>(conn.write().await.deref_mut())
.await?;

let driver = PgAuthDriver::<BuiltinRole>::new(pool);
let mut conds = vec![];
for user in &args.users {
let uid = if let Ok(id) = user.parse::<i64>() {
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<DbConnectionPoolV2>) -> 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::<i64>(conn.write().await.deref_mut())
.await?;

let driver = PgAuthDriver::<BuiltinRole>::new(pool);
let mut values = vec![];
for user in &args.users {
let uid = if let Ok(id) = user.parse::<i64>() {
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(())
}
8 changes: 8 additions & 0 deletions editoast/src/client/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod electrical_profiles_commands;
pub mod group;
pub mod healthcheck;
pub mod import_rolling_stock;
pub mod infra_commands;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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),
}
Expand Down
79 changes: 79 additions & 0 deletions editoast/src/client/user.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
}

/// List users
pub async fn list_user(args: ListArgs, pool: Arc<DbConnectionPoolV2>) -> 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<String>)>(conn.write().await.deref_mut())
.await?
} else {
users_query
.load::<(i64, String, Option<String>)>(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<DbConnectionPoolV2>) -> anyhow::Result<()> {
let driver = PgAuthDriver::<BuiltinRole>::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(())
}
32 changes: 32 additions & 0 deletions editoast/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand All @@ -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;
Expand Down Expand Up @@ -231,6 +235,34 @@ async fn run() -> Result<(), Box<dyn Error + Send + Sync>> {
.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
Expand Down
Loading