Skip to content
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
10 changes: 9 additions & 1 deletion libs/ferriskey-cli-core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod client;
mod config;
mod context;
mod realm;

use config::StoredContext;
use ferriskey_commands::{Cli, Commands};
Expand All @@ -14,6 +15,8 @@ pub enum CliCoreError {
Client(#[from] client::ClientCommandError),
#[error(transparent)]
Context(#[from] context::ContextCommandError),
#[error(transparent)]
Realm(#[from] realm::RealmCommandError),
#[error("command '{0}' is not implemented yet")]
UnimplementedCommand(&'static str),
}
Expand All @@ -22,7 +25,12 @@ pub fn run(cli: Cli) -> Result<()> {
let inline_context = build_inline_context(&cli);
match cli.command {
Commands::Context(command) => Ok(context::run(cli.output.as_str(), command)?),
Commands::Realm(_) => Err(CliCoreError::UnimplementedCommand("realm")),
Commands::Realm(command) => Ok(realm::run(
cli.output.as_str(),
cli.context.as_deref(),
inline_context,
command,
)?),
Commands::Client(command) => Ok(client::run(
cli.output.as_str(),
cli.context.as_deref(),
Expand Down
260 changes: 260 additions & 0 deletions libs/ferriskey-cli-core/src/realm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
use ferriskey_client::{CreateRealmRequest, FerriskeyClient, FerriskeyClientError, Realm};
use ferriskey_commands::{RealmCommand, RealmNameArgs, RealmSubcommand};
use serde::Serialize;
use thiserror::Error;

use crate::config::{ConfigError, FileContextRepository, StoredContext};

type Result<T> = std::result::Result<T, RealmCommandError>;

pub fn run(
output_format: &str,
context_override: Option<&str>,
inline_context: Option<StoredContext>,
command: RealmCommand,
) -> Result<()> {
match command.command {
RealmSubcommand::List => list_realms(output_format, context_override, inline_context),
RealmSubcommand::Get(args) => get_realm(output_format, context_override, inline_context, args),
RealmSubcommand::Create(args) => {
create_realm(output_format, context_override, inline_context, args)
}
RealmSubcommand::Delete(args) => {
delete_realm(output_format, context_override, inline_context, args)
}
}
}

#[derive(Debug, Error)]
pub enum RealmCommandError {
#[error(transparent)]
Config(#[from] ConfigError),
#[error(transparent)]
Api(#[from] FerriskeyClientError),
#[error("context '{0}' does not exist")]
ContextNotFound(String),
#[error("no active context is configured")]
NoActiveContext,
#[error(
"auth realm is required: pass '--realm' or configure a default realm on the selected context"
)]
MissingAuthRealm,
#[error("realm '{0}' not found")]
RealmNotFound(String),
#[error("unsupported output format: {0}")]
UnsupportedOutputFormat(String),
#[error("failed to serialize JSON output")]
SerializeJson {
#[source]
source: serde_json::Error,
},
#[error("failed to serialize YAML output")]
SerializeYaml {
#[source]
source: serde_yaml::Error,
},
}

#[derive(Debug, Serialize)]
struct RealmView {
id: String,
name: String,
}

fn resolve_context(
context_override: Option<&str>,
inline_context: Option<StoredContext>,
) -> Result<StoredContext> {
if let Some(ctx) = inline_context {
return Ok(ctx);
}
let repository = FileContextRepository::new()?;
let store = repository.load()?;
let context_name = match context_override {
Some(name) => name.to_owned(),
None => store
.current_context
.clone()
.ok_or(RealmCommandError::NoActiveContext)?,
};
store
.contexts
.get(&context_name)
.cloned()
.ok_or(RealmCommandError::ContextNotFound(context_name))
}

fn auth_client(context: &StoredContext) -> Result<FerriskeyClient> {
let auth_realm = context
.realm
.as_deref()
.ok_or(RealmCommandError::MissingAuthRealm)?;
let unauthenticated = FerriskeyClient::new(context.url.clone(), "", "")?;
let token = unauthenticated.exchange_client_credentials(
auth_realm,
context.client_id.as_str(),
context.client_secret.as_str(),
)?;
Ok(FerriskeyClient::new(
context.url.clone(),
"",
token.access_token,
)?)
}

fn to_view(realm: Realm) -> RealmView {
RealmView {
id: realm.id,
name: realm.name,
}
}

fn list_realms(
output_format: &str,
context_override: Option<&str>,
inline_context: Option<StoredContext>,
) -> Result<()> {
let context = resolve_context(context_override, inline_context)?;
let auth_realm = context
.realm
.clone()
.ok_or(RealmCommandError::MissingAuthRealm)?;
let client = auth_client(&context)?;
let realms = client.list_realms(&auth_realm)?;
let views: Vec<RealmView> = realms.into_iter().map(to_view).collect();
render_realm_list(output_format, &views)
}

fn get_realm(
output_format: &str,
context_override: Option<&str>,
inline_context: Option<StoredContext>,
args: RealmNameArgs,
) -> Result<()> {
let context = resolve_context(context_override, inline_context)?;
let client = auth_client(&context)?;
let realm = client.get_realm(&args.name)?;
render_realm(output_format, to_view(realm))
}

fn create_realm(
output_format: &str,
context_override: Option<&str>,
inline_context: Option<StoredContext>,
args: RealmNameArgs,
) -> Result<()> {
let context = resolve_context(context_override, inline_context)?;
let client = auth_client(&context)?;
let request = CreateRealmRequest { name: args.name };
let realm = client.create_realm(&request)?;
render_realm(output_format, to_view(realm))
}

fn delete_realm(
output_format: &str,
context_override: Option<&str>,
inline_context: Option<StoredContext>,
args: RealmNameArgs,
) -> Result<()> {
let context = resolve_context(context_override, inline_context)?;
let client = auth_client(&context)?;
client.delete_realm(&args.name)?;
render_message(output_format, &format!("realm '{}' deleted", args.name))
}

fn render_realm_list(output_format: &str, realms: &[RealmView]) -> Result<()> {
match output_format {
"table" => {
let name_width = realms
.iter()
.map(|r| r.name.len())
.max()
.unwrap_or(0)
.max("NAME".len());
let id_width = realms
.iter()
.map(|r| r.id.len())
.max()
.unwrap_or(0)
.max("ID".len());

println!("{:<name_width$} {:<id_width$}", "NAME", "ID");
for r in realms {
println!("{:<name_width$} {:<id_width$}", r.name, r.id);
}
Ok(())
}
"json" => {
println!(
"{}",
serde_json::to_string_pretty(realms)
.map_err(|source| RealmCommandError::SerializeJson { source })?
);
Ok(())
}
"yaml" => {
println!(
"{}",
serde_yaml::to_string(realms)
.map_err(|source| RealmCommandError::SerializeYaml { source })?
);
Ok(())
}
_ => Err(RealmCommandError::UnsupportedOutputFormat(
output_format.to_owned(),
)),
}
}

fn render_realm(output_format: &str, realm: RealmView) -> Result<()> {
match output_format {
"table" => {
println!("id: {}", realm.id);
println!("name: {}", realm.name);
Ok(())
}
"json" => {
println!(
"{}",
serde_json::to_string_pretty(&realm)
.map_err(|source| RealmCommandError::SerializeJson { source })?
);
Ok(())
}
"yaml" => {
println!(
"{}",
serde_yaml::to_string(&realm)
.map_err(|source| RealmCommandError::SerializeYaml { source })?
);
Ok(())
}
_ => Err(RealmCommandError::UnsupportedOutputFormat(
output_format.to_owned(),
)),
}
}

fn render_message(output_format: &str, message: &str) -> Result<()> {
match output_format {
"table" => {
println!("{message}");
Ok(())
}
"json" => {
println!("{}", serde_json::json!({ "message": message }));
Ok(())
}
"yaml" => {
println!(
"{}",
serde_yaml::to_string(&serde_json::json!({ "message": message }))
.map_err(|source| RealmCommandError::SerializeYaml { source })?
);
Ok(())
}
_ => Err(RealmCommandError::UnsupportedOutputFormat(
output_format.to_owned(),
)),
}
}
13 changes: 7 additions & 6 deletions libs/ferriskey-commands/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,35 @@ pub use self::client::{
pub use self::context::{
ContextAddArgs, ContextCommand, ContextRemoveArgs, ContextSubcommand, ContextUseArgs,
};
pub use self::realm::{RealmCommand, RealmNameArgs, RealmSubcommand};
use clap::{Parser, Subcommand};

/// FerrisKey CLI.
#[derive(Debug, Parser)]
#[command(name = "ferriskey", about = "FerrisKey CLI")]
pub struct Cli {
/// Override the active context for this command.
#[arg(long)]
#[arg(long, global = true)]
pub context: Option<String>,

/// Output format.
#[arg(long, short = 'o', value_parser = ["table", "json", "yaml"], default_value = "table")]
#[arg(long, short = 'o', global = true, value_parser = ["table", "json", "yaml"], default_value = "table")]
pub output: String,

/// FerrisKey server URL (overrides context file).
#[arg(long, env = "FERRISKEY_URL")]
#[arg(long, global = true, env = "FERRISKEY_URL")]
pub url: Option<String>,

/// Client ID used for authentication (overrides context file).
#[arg(long, env = "FERRISKEY_CLIENT_ID")]
#[arg(long, global = true, env = "FERRISKEY_CLIENT_ID")]
pub client_id: Option<String>,

/// Client secret used for authentication (overrides context file).
#[arg(long, env = "FERRISKEY_CLIENT_SECRET")]
#[arg(long, global = true, env = "FERRISKEY_CLIENT_SECRET")]
pub client_secret: Option<String>,

/// Default realm (overrides context file).
#[arg(long, env = "FERRISKEY_REALM")]
#[arg(long, global = true, env = "FERRISKEY_REALM")]
pub realm: Option<String>,

/// Command to execute.
Expand Down