diff --git a/libs/ferriskey-cli-client/src/lib.rs b/libs/ferriskey-cli-client/src/lib.rs index 77e8e7a..867f5d4 100644 --- a/libs/ferriskey-cli-client/src/lib.rs +++ b/libs/ferriskey-cli-client/src/lib.rs @@ -499,6 +499,53 @@ impl FerriskeyClient { self.get_list(&self.endpoint(&format!("realms/{realm}/roles"))) } + pub fn delete_role(&self, realm: &str, role_id: &str) -> Result<(), FerriskeyClientError> { + let response = self + .http + .delete(self.endpoint(&format!("realms/{realm}/roles/{role_id}"))) + .bearer_auth(&self.token) + .send()?; + + if !response.status().is_success() { + let status = response.status(); + let body = response.text().unwrap_or_default(); + return Err(FerriskeyClientError::Api { status, body }); + } + + Ok(()) + } + + pub fn list_client_roles( + &self, + realm: &str, + client_uuid: &str, + ) -> Result, FerriskeyClientError> { + self.get_list(&self.endpoint(&format!("realms/{realm}/clients/{client_uuid}/roles"))) + } + + pub fn delete_client_role( + &self, + realm: &str, + client_uuid: &str, + role_id: &str, + ) -> Result<(), FerriskeyClientError> { + let response = self + .http + .delete(self.endpoint(&format!( + "realms/{realm}/clients/{client_uuid}/roles/{role_id}" + ))) + .bearer_auth(&self.token) + .send()?; + + if !response.status().is_success() { + let status = response.status(); + let body = response.text().unwrap_or_default(); + return Err(FerriskeyClientError::Api { status, body }); + } + + Ok(()) + } + pub fn create_client_role( &self, realm: &str, diff --git a/libs/ferriskey-cli-commands/src/lib.rs b/libs/ferriskey-cli-commands/src/lib.rs index 11f5878..4a40391 100644 --- a/libs/ferriskey-cli-commands/src/lib.rs +++ b/libs/ferriskey-cli-commands/src/lib.rs @@ -15,7 +15,8 @@ pub use self::context::{ pub use self::login::LoginCommand; pub use self::realm::{ ImportSource, RealmCommand, RealmDeleteArgs, RealmImportArgs, RealmNameArgs, RealmRoleCommand, - RealmRoleCreateArgs, RealmRoleListArgs, RealmRoleSubcommand, RealmSubcommand, + RealmRoleCreateArgs, RealmRoleDeleteArgs, RealmRoleGetArgs, RealmRoleListArgs, + RealmRoleSubcommand, RealmSubcommand, }; pub use self::source::{ SourceAddArgs, SourceCommand, SourceKind, SourceRemoveArgs, SourceSubcommand, diff --git a/libs/ferriskey-cli-commands/src/realm.rs b/libs/ferriskey-cli-commands/src/realm.rs index 631bca7..1448c51 100644 --- a/libs/ferriskey-cli-commands/src/realm.rs +++ b/libs/ferriskey-cli-commands/src/realm.rs @@ -38,10 +38,14 @@ pub struct RealmRoleCommand { /// Available realm role commands. #[derive(Debug, Subcommand)] pub enum RealmRoleSubcommand { - /// Create a realm role. + /// Create a realm or client role. Create(RealmRoleCreateArgs), - /// List realm roles. + /// List realm or client roles. List(RealmRoleListArgs), + /// Show a realm or client role's details. + Get(RealmRoleGetArgs), + /// Delete a realm or client role. + Delete(RealmRoleDeleteArgs), } /// Arguments for `realm role create`. @@ -61,6 +65,10 @@ pub struct RealmRoleCreateArgs { /// Realm name. Defaults to the selected context realm. #[arg(long)] pub realm: Option, + + /// Create a client role instead of a realm role, scoped to this client id. + #[arg(long)] + pub client: Option, } /// Arguments for `realm role list`. @@ -69,6 +77,44 @@ pub struct RealmRoleListArgs { /// Realm name. Defaults to the selected context realm. #[arg(long)] pub realm: Option, + + /// List roles of this client instead of realm roles. + #[arg(long)] + pub client: Option, +} + +/// Arguments for `realm role get`. +#[derive(Debug, Args)] +pub struct RealmRoleGetArgs { + /// Role name. + pub name: String, + + /// Realm name. Defaults to the selected context realm. + #[arg(long)] + pub realm: Option, + + /// Look up a client role instead of a realm role, scoped to this client id. + #[arg(long)] + pub client: Option, +} + +/// Arguments for `realm role delete`. +#[derive(Debug, Args)] +pub struct RealmRoleDeleteArgs { + /// Role name. + pub name: String, + + /// Realm name. Defaults to the selected context realm. + #[arg(long)] + pub realm: Option, + + /// Delete a client role instead of a realm role, scoped to this client id. + #[arg(long)] + pub client: Option, + + /// Skip the confirmation prompt (required in non-interactive shells). + #[arg(long, short = 'f', default_value_t = false)] + pub force: bool, } /// Arguments using a realm name. diff --git a/libs/ferriskey-cli-core/src/realm.rs b/libs/ferriskey-cli-core/src/realm.rs index bcae8d6..fb186bf 100644 --- a/libs/ferriskey-cli-core/src/realm.rs +++ b/libs/ferriskey-cli-core/src/realm.rs @@ -4,7 +4,8 @@ use ferriskey_cli_client::{ }; use ferriskey_cli_commands::{ RealmCommand, RealmDeleteArgs, RealmImportArgs, RealmNameArgs, RealmRoleCommand, - RealmRoleCreateArgs, RealmRoleListArgs, RealmRoleSubcommand, RealmSubcommand, + RealmRoleCreateArgs, RealmRoleDeleteArgs, RealmRoleGetArgs, RealmRoleListArgs, + RealmRoleSubcommand, RealmSubcommand, }; use serde::Serialize; use thiserror::Error; @@ -59,6 +60,12 @@ fn run_role( RealmRoleSubcommand::List(args) => { list_roles(output_format, context_override, inline_context, args) } + RealmRoleSubcommand::Get(args) => { + get_role(output_format, context_override, inline_context, args) + } + RealmRoleSubcommand::Delete(args) => { + delete_role(output_format, context_override, inline_context, args) + } } } @@ -82,6 +89,10 @@ pub enum RealmCommandError { MissingAuthRealm, #[error("realm '{0}' not found")] RealmNotFound(String), + #[error("client '{0}' not found in realm")] + ClientNotFound(String), + #[error("role '{0}' not found")] + RoleNotFound(String), #[error(transparent)] Import(Box), #[error("unsupported output format: {0}")] @@ -223,6 +234,31 @@ fn delete_realm( render_message(output_format, &format!("realm '{}' deleted", args.name)) } +/// Resolve a client id (e.g. `my-app`) to the client's uuid, as required by +/// the client-role endpoints. +fn resolve_client_uuid(client: &FerriskeyClient, realm: &str, client_id: &str) -> Result { + client + .get_client(realm, client_id)? + .and_then(|found| found.id) + .ok_or_else(|| RealmCommandError::ClientNotFound(client_id.to_owned())) +} + +/// List roles in the scope selected by `--client`: a specific client's roles +/// when given, realm roles otherwise. +fn list_roles_in_scope( + client: &FerriskeyClient, + realm: &str, + client_id: Option<&str>, +) -> Result> { + match client_id { + Some(client_id) => { + let uuid = resolve_client_uuid(client, realm, client_id)?; + Ok(client.list_client_roles(realm, &uuid)?) + } + None => Ok(client.list_realm_roles(realm)?), + } +} + fn create_role( output_format: &str, context_override: Option<&str>, @@ -237,7 +273,13 @@ fn create_role( description: args.description, permissions: args.permissions, }; - let role = client.create_role(&realm, &request)?; + let role = match args.client { + Some(client_id) => { + let uuid = resolve_client_uuid(&client, &realm, &client_id)?; + client.create_client_role(&realm, &uuid, &request)? + } + None => client.create_role(&realm, &request)?, + }; render_role(output_format, to_role_view(role)) } @@ -250,11 +292,57 @@ fn list_roles( let context = resolve_context(context_override, inline_context)?; let realm = resolve_realm(&context, args.realm)?; let client = auth_client(&context)?; - let roles = client.list_realm_roles(&realm)?; + let roles = list_roles_in_scope(&client, &realm, args.client.as_deref())?; let views: Vec = roles.into_iter().map(to_role_view).collect(); render_role_list(output_format, &views) } +fn get_role( + output_format: &str, + context_override: Option<&str>, + inline_context: Option, + args: RealmRoleGetArgs, +) -> Result<()> { + let context = resolve_context(context_override, inline_context)?; + let realm = resolve_realm(&context, args.realm)?; + let client = auth_client(&context)?; + let role = list_roles_in_scope(&client, &realm, args.client.as_deref())? + .into_iter() + .find(|r| r.name == args.name) + .ok_or_else(|| RealmCommandError::RoleNotFound(args.name.clone()))?; + render_role(output_format, to_role_view(role)) +} + +fn delete_role( + output_format: &str, + context_override: Option<&str>, + inline_context: Option, + args: RealmRoleDeleteArgs, +) -> Result<()> { + confirm(&format!("Delete role '{}'?", args.name), args.force)?; + let context = resolve_context(context_override, inline_context)?; + let realm = resolve_realm(&context, args.realm)?; + let client = auth_client(&context)?; + let client_uuid = args + .client + .as_deref() + .map(|client_id| resolve_client_uuid(&client, &realm, client_id)) + .transpose()?; + let roles = match &client_uuid { + Some(uuid) => client.list_client_roles(&realm, uuid)?, + None => client.list_realm_roles(&realm)?, + }; + let role = roles + .into_iter() + .find(|r| r.name == args.name) + .ok_or_else(|| RealmCommandError::RoleNotFound(args.name.clone()))?; + match &client_uuid { + Some(uuid) => client.delete_client_role(&realm, uuid, &role.id)?, + None => client.delete_role(&realm, &role.id)?, + } + render_message(output_format, &format!("role '{}' deleted", args.name)) +} + fn import_realm( output_format: &str, context_override: Option<&str>,