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
43 changes: 38 additions & 5 deletions libs/ferriskey-cli-core/src/client.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use ferriskey_cli_client::{
ClientRepresentation, CreateClientRequest, CreatedClient, FerriskeyClientError,
ClientRepresentation, CreateClientRequest, CreatedClient, FerriskeyClient, FerriskeyClientError,
};
use ferriskey_cli_commands::{
ClientCommand, ClientCreateArgs, ClientDeleteArgs, ClientGetArgs, ClientListArgs,
Expand Down Expand Up @@ -56,6 +56,10 @@ pub enum ClientCommandError {
"realm is required: pass '--realm' or configure a default realm on the selected context"
)]
MissingRealm,
#[error(
"auth realm is required: configure a default realm on the selected context ('ferris-ctl context add --realm <realm>')"
)]
MissingAuthRealm,
#[error("unsupported output format: {0}")]
UnsupportedOutputFormat(String),
#[error("failed to serialize JSON output")]
Expand Down Expand Up @@ -116,7 +120,7 @@ fn delete_client(
)?;
let context = resolve_context(context_override, inline_context)?;
let realm = resolve_realm(&context, args.realm.clone())?;
let client = session::authenticated_client(&context, &realm)?;
let client = auth_client(&context)?;
let found = client
.get_client(&realm, &args.client_id)?
.ok_or_else(|| ClientCommandError::ClientNotFound(args.client_id.clone()))?;
Expand All @@ -139,7 +143,7 @@ fn get_client(
) -> Result<()> {
let context = resolve_context(context_override, inline_context)?;
let realm = resolve_realm(&context, args.realm.clone())?;
let client = session::authenticated_client(&context, &realm)?;
let client = auth_client(&context)?;
let result = client
.get_client(&realm, &args.client_id)?
.ok_or_else(|| ClientCommandError::ClientNotFound(args.client_id.clone()))?;
Expand All @@ -155,7 +159,7 @@ fn list_clients(
) -> Result<()> {
let context = resolve_context(context_override, inline_context)?;
let realm = resolve_realm(&context, args.realm.clone())?;
let client = session::authenticated_client(&context, &realm)?;
let client = auth_client(&context)?;
let clients = client.list_clients(&realm)?;
let views = clients.into_iter().map(to_view).collect::<Vec<_>>();

Expand All @@ -170,7 +174,7 @@ fn create_client(
) -> Result<()> {
let context = resolve_context(context_override, inline_context)?;
let realm = resolve_realm(&context, args.realm.clone())?;
let client = session::authenticated_client(&context, &realm)?;
let client = auth_client(&context)?;
let request = build_create_client_request(args);
let created = client.create_client(&realm, &request)?;

Expand Down Expand Up @@ -211,6 +215,18 @@ fn resolve_realm(context: &StoredContext, realm: Option<String>) -> Result<Strin
.ok_or(ClientCommandError::MissingRealm)
}

/// Authenticate against the context's home realm — where the context's
/// client_id/client_secret are registered — regardless of which realm the
/// command targets via `--realm`. The server, not the CLI, decides whether
/// the resulting token can act on the target realm.
fn auth_client(context: &StoredContext) -> Result<FerriskeyClient> {
let auth_realm = context
.realm
.as_deref()
.ok_or(ClientCommandError::MissingAuthRealm)?;
Ok(session::authenticated_client(context, auth_realm)?)
}

fn to_view(client: ClientRepresentation) -> ClientView {
ClientView {
id: client.id.unwrap_or_default(),
Expand Down Expand Up @@ -458,6 +474,23 @@ mod tests {
use crate::config::StoredContext;
use std::collections::BTreeMap;

#[test]
fn auth_client_requires_realm_on_context() {
// Regression: auth_client must authenticate against the context's
// home realm, never a command's target `--realm` (which it doesn't
// even take as a parameter) — a context without a home realm can't
// authenticate at all, regardless of any target realm resolved
// elsewhere.
let context = StoredContext {
url: "http://localhost:3333".to_owned(),
client_id: "cli".to_owned(),
client_secret: Some("secret".to_owned()),
realm: None,
};
let err = auth_client(&context).expect_err("missing realm should error");
assert!(matches!(err, ClientCommandError::MissingAuthRealm));
}

#[test]
fn select_context_uses_active_context_by_default() {
let mut contexts = BTreeMap::new();
Expand Down
38 changes: 31 additions & 7 deletions libs/ferriskey-cli-core/src/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ pub enum UserCommandError {
"realm is required: pass '--realm' or configure a default realm on the selected context"
)]
MissingRealm,
#[error(
"auth realm is required: configure a default realm on the selected context ('ferris-ctl context add --realm <realm>')"
)]
MissingAuthRealm,
#[error("user '{0}' not found")]
UserNotFound(String),
#[error("role '{0}' not found in realm")]
Expand Down Expand Up @@ -114,8 +118,16 @@ fn resolve_realm(context: &StoredContext, realm: Option<String>) -> Result<Strin
.ok_or(UserCommandError::MissingRealm)
}

fn authenticate(context: &StoredContext, realm: &str) -> Result<FerriskeyClient> {
Ok(session::authenticated_client(context, realm)?)
/// Authenticate against the context's home realm — where the context's
/// client_id/client_secret are registered — regardless of which realm the
/// command targets via `--realm`. The server, not the CLI, decides whether
/// the resulting token can act on the target realm.
fn auth_client(context: &StoredContext) -> Result<FerriskeyClient> {
let auth_realm = context
.realm
.as_deref()
.ok_or(UserCommandError::MissingAuthRealm)?;
Ok(session::authenticated_client(context, auth_realm)?)
}

fn find_user(client: &FerriskeyClient, realm: &str, username: &str) -> Result<UserRepresentation> {
Expand Down Expand Up @@ -145,7 +157,7 @@ fn list_users(
) -> Result<()> {
let context = resolve_context(context_override, inline_context)?;
let realm = resolve_realm(&context, args.realm)?;
let client = authenticate(&context, &realm)?;
let client = auth_client(&context)?;
let users = client.list_users(&realm)?;
let views: Vec<UserView> = users.into_iter().map(to_view).collect();
render_user_list(output_format, &views)
Expand All @@ -159,7 +171,7 @@ fn get_user(
) -> Result<()> {
let context = resolve_context(context_override, inline_context)?;
let realm = resolve_realm(&context, args.realm)?;
let client = authenticate(&context, &realm)?;
let client = auth_client(&context)?;
let user = find_user(&client, &realm, &args.username)?;
render_user(output_format, to_view(user))
}
Expand All @@ -172,7 +184,7 @@ fn create_user(
) -> Result<()> {
let context = resolve_context(context_override, inline_context)?;
let realm = resolve_realm(&context, args.realm)?;
let client = authenticate(&context, &realm)?;
let client = auth_client(&context)?;
let request = CreateUserRequest {
username: args.username,
firstname: args.firstname,
Expand All @@ -196,7 +208,7 @@ fn delete_user(
)?;
let context = resolve_context(context_override, inline_context)?;
let realm = resolve_realm(&context, args.realm)?;
let client = authenticate(&context, &realm)?;
let client = auth_client(&context)?;
let user = find_user(&client, &realm, &args.username)?;
client.delete_user(&realm, &user.id)?;
render_message(output_format, &format!("user '{}' deleted", args.username))
Expand All @@ -210,7 +222,7 @@ fn assign_role(
) -> Result<()> {
let context = resolve_context(context_override, inline_context)?;
let realm = resolve_realm(&context, args.realm)?;
let client = authenticate(&context, &realm)?;
let client = auth_client(&context)?;
let user = find_user(&client, &realm, &args.username)?;
let role = client
.list_realm_roles(&realm)?
Expand Down Expand Up @@ -355,6 +367,18 @@ mod tests {
}
}

#[test]
fn auth_client_requires_realm_on_context() {
// Regression: auth_client must authenticate against the context's
// home realm, never a command's target `--realm` (which it doesn't
// even take as a parameter) — a context without a home realm can't
// authenticate at all, regardless of any target realm resolved
// elsewhere.
let context = make_context(None);
let err = auth_client(&context).expect_err("missing realm should error");
assert!(matches!(err, UserCommandError::MissingAuthRealm));
}

#[test]
fn resolve_realm_prefers_explicit_argument() {
let context = make_context(Some("master"));
Expand Down
Loading