Skip to content

Commit 75dd801

Browse files
committed
editoast: add CLI to interact with roles
Signed-off-by: Leo Valais <[email protected]>
1 parent 3495b31 commit 75dd801

File tree

6 files changed

+188
-4
lines changed

6 files changed

+188
-4
lines changed

editoast/Cargo.lock

+2-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

editoast/Cargo.toml

+1-2
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,7 @@ utoipa = { version = "4.2.3", features = ["chrono", "uuid"] }
8080
uuid = { version = "1.10.0", features = ["serde", "v4"] }
8181

8282
[dependencies]
83-
# For batch dependency updates, see editoast/README.md
84-
83+
anyhow = "1.0"
8584
async-trait = "0.1.82"
8685
axum = { version = "0.7.7", default-features = false, features = [
8786
"multipart",

editoast/editoast_authz/src/builtin_role.rs

+15-1
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,27 @@
11
use serde::Deserialize;
22
use serde::Serialize;
33
use strum::AsRefStr;
4+
use strum::Display;
5+
use strum::EnumIter;
46
use strum::EnumString;
57
use utoipa::ToSchema;
68

79
use crate::roles::BuiltinRoleSet;
810

911
#[derive(
10-
Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, EnumString, AsRefStr, ToSchema,
12+
Debug,
13+
Clone,
14+
Copy,
15+
PartialEq,
16+
Eq,
17+
Hash,
18+
Serialize,
19+
Deserialize,
20+
EnumString,
21+
AsRefStr,
22+
EnumIter,
23+
Display,
24+
ToSchema,
1125
)]
1226
pub enum BuiltinRole {
1327
/// A user with this role short-circuits all role and permission checks

editoast/src/client/mod.rs

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
mod postgres_config;
22
mod redis_config;
3+
pub mod roles;
34
pub mod stdcm_search_env_commands;
45
mod telemetry_config;
56

@@ -14,6 +15,7 @@ use derivative::Derivative;
1415
use editoast_derive::EditoastError;
1516
pub use postgres_config::PostgresConfig;
1617
pub use redis_config::RedisConfig;
18+
use roles::RolesCommand;
1719
use stdcm_search_env_commands::StdcmSearchEnvCommands;
1820
pub use telemetry_config::TelemetryConfig;
1921
pub use telemetry_config::TelemetryKind;
@@ -71,6 +73,8 @@ pub enum Commands {
7173
long_about = "STDCM search environment management commands"
7274
)]
7375
STDCMSearchEnv(StdcmSearchEnvCommands),
76+
#[command(subcommand, about, long_about = "Roles related commands")]
77+
Roles(RolesCommand),
7478
}
7579

7680
#[derive(Subcommand, Debug)]

editoast/src/client/roles.rs

+136
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
use std::collections::HashSet;
2+
3+
use anyhow::{anyhow, bail};
4+
use clap::{Args, Subcommand};
5+
use editoast_authz::{
6+
authorizer::{StorageDriver, UserInfo},
7+
BuiltinRole,
8+
};
9+
use editoast_models::DbConnection;
10+
use itertools::Itertools as _;
11+
use strum::IntoEnumIterator;
12+
use tracing::info;
13+
14+
use crate::models::auth::PgAuthDriver;
15+
16+
#[derive(Debug, Subcommand)]
17+
pub enum RolesCommand {
18+
/// Lists the builtin roles supported by editoast
19+
ListRoles,
20+
/// Lists the roles assigned to a subject
21+
List(ListArgs),
22+
/// Grants builtin roles to a subject
23+
Add(AddArgs),
24+
/// Revokes builtin roles from a subject
25+
Remove(RemoveArgs),
26+
}
27+
28+
#[derive(Debug, Args)]
29+
pub struct ListArgs {
30+
/// A subject ID or user identity
31+
subject: String,
32+
}
33+
34+
#[derive(Debug, Args)]
35+
pub struct AddArgs {
36+
/// A subject ID or user identity
37+
subject: String,
38+
/// A non-empty list of builtin roles
39+
roles: Vec<String>,
40+
}
41+
42+
#[derive(Debug, Args)]
43+
pub struct RemoveArgs {
44+
/// A subject ID or user identity
45+
subject: String,
46+
/// A non-empty list of builtin roles
47+
roles: Vec<String>,
48+
}
49+
50+
pub fn list_roles() {
51+
BuiltinRole::iter().for_each(|role| println!("{role}"));
52+
}
53+
54+
async fn parse_and_fetch_subject_id(
55+
subject: &String,
56+
driver: &PgAuthDriver<BuiltinRole>,
57+
) -> anyhow::Result<i64> {
58+
if let Ok(id) = subject.parse::<i64>() {
59+
if let Some(UserInfo { identity, name }) = driver.get_user_info(id).await? {
60+
info!("{name} ({identity}) found with ID {id}");
61+
Ok(id)
62+
} else {
63+
bail!("No subject found with ID {id}");
64+
}
65+
} else {
66+
let uid = driver.get_user_id(&subject).await?;
67+
uid.ok_or_else(|| anyhow!("No subject {subject} found for"))
68+
}
69+
}
70+
71+
pub async fn list_subject_roles(
72+
ListArgs { subject }: ListArgs,
73+
conn: DbConnection,
74+
) -> anyhow::Result<()> {
75+
let driver = PgAuthDriver::<BuiltinRole>::new(conn);
76+
let subject_id = parse_and_fetch_subject_id(&subject, &driver).await?;
77+
let roles = driver.fetch_subject_roles(subject_id).await?;
78+
if roles.is_empty() {
79+
println!("Subject {subject} has no roles assigned");
80+
return Ok(());
81+
}
82+
for role in roles {
83+
println!("{role}");
84+
}
85+
Ok(())
86+
}
87+
88+
pub async fn add_roles(
89+
AddArgs { subject, roles }: AddArgs,
90+
conn: DbConnection,
91+
) -> anyhow::Result<()> {
92+
let driver = PgAuthDriver::<BuiltinRole>::new(conn);
93+
let subject_id = parse_and_fetch_subject_id(&subject, &driver).await?;
94+
let roles = roles
95+
.iter()
96+
.map(|role| {
97+
role.parse::<BuiltinRole>()
98+
.map_err(|_| anyhow!("Invalid role tag {role}"))
99+
})
100+
.collect::<Result<HashSet<_>, _>>()?;
101+
info!(
102+
"Adding roles {} to subject {subject_id}",
103+
roles
104+
.iter()
105+
.map(|role| role.to_string())
106+
.collect_vec()
107+
.join(", "),
108+
);
109+
driver.ensure_subject_roles(subject_id, roles).await?;
110+
Ok(())
111+
}
112+
113+
pub async fn remove_roles(
114+
RemoveArgs { subject, roles }: RemoveArgs,
115+
conn: DbConnection,
116+
) -> anyhow::Result<()> {
117+
let driver = PgAuthDriver::<BuiltinRole>::new(conn);
118+
let subject_id = parse_and_fetch_subject_id(&subject, &driver).await?;
119+
let roles = roles
120+
.iter()
121+
.map(|role| {
122+
role.parse::<BuiltinRole>()
123+
.map_err(|_| anyhow!("Invalid role tag {role}"))
124+
})
125+
.collect::<Result<HashSet<_>, _>>()?;
126+
info!(
127+
"Removing roles {} from subject {subject_id}",
128+
roles
129+
.iter()
130+
.map(|role| role.to_string())
131+
.collect_vec()
132+
.join(", "),
133+
);
134+
driver.remove_subject_roles(subject_id, roles).await?;
135+
Ok(())
136+
}

editoast/src/main.rs

+30
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ use axum::{Router, ServiceExt};
2020
use axum_tracing_opentelemetry::middleware::OtelAxumLayer;
2121
use chashmap::CHashMap;
2222
use clap::Parser;
23+
use client::roles;
24+
use client::roles::RolesCommand;
2325
use client::stdcm_search_env_commands::handle_stdcm_search_env_command;
2426
use client::{
2527
ClearArgs, Client, Color, Commands, DeleteProfileSetArgs, ElectricalProfilesCommands,
@@ -245,6 +247,25 @@ async fn run() -> Result<(), Box<dyn Error + Send + Sync>> {
245247
Commands::STDCMSearchEnv(subcommand) => {
246248
handle_stdcm_search_env_command(subcommand, db_pool).await
247249
}
250+
Commands::Roles(roles_command) => match roles_command {
251+
RolesCommand::ListRoles => {
252+
roles::list_roles();
253+
Ok(())
254+
}
255+
RolesCommand::List(list_args) => {
256+
roles::list_subject_roles(list_args, db_pool.get().await?)
257+
.await
258+
.map_err(Into::into)
259+
}
260+
RolesCommand::Add(add_args) => roles::add_roles(add_args, db_pool.get().await?)
261+
.await
262+
.map_err(Into::into),
263+
RolesCommand::Remove(remove_args) => {
264+
roles::remove_roles(remove_args, db_pool.get().await?)
265+
.await
266+
.map_err(Into::into)
267+
}
268+
},
248269
}
249270
}
250271

@@ -892,6 +913,15 @@ impl CliError {
892913
}
893914
}
894915

916+
impl From<anyhow::Error> for CliError {
917+
fn from(err: anyhow::Error) -> Self {
918+
CliError {
919+
exit_code: 1,
920+
message: format!("❌ {err}"),
921+
}
922+
}
923+
}
924+
895925
#[cfg(test)]
896926
mod tests {
897927
use super::*;

0 commit comments

Comments
 (0)