Skip to content
Open
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
12 changes: 12 additions & 0 deletions cli-engine/docs/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,18 @@ When a CLI registers auth providers or configures a default provider, `cli_engin
These commands are implemented with the same `CommandSpec`, middleware, output envelope, and renderers
as application commands.

### Human views

`auth status` and `auth login` render their human output through the views `auth:status` and `auth:login` (`AUTH_STATUS_VIEW_ID`, `AUTH_LOGIN_VIEW_ID`). The engine registers `default_auth_status_view()` and `default_auth_login_view()` under those ids: identity first, then expiry, environment, provider, and the remaining fields.

To change which columns show by default, set the command's default fields with `CliConfig::with_auth_status_default_fields` / `with_auth_login_default_fields`.

```rust
let config = config.with_auth_status_default_fields("identity,expires_at,env,expired,refreshable,scopes");
```

To change the columns themselves, register a `HumanViewDef` under the same id with `CliConfig::with_view` (or `register_global_human_view`). The built-in group is kept, and a consumer view always wins over the default. `default_auth_status_view().columns` is a convenient starting point.

## Transport Injectors

Transport injectors implement [`AuthInjector`](../src/transport/injector.rs) and mutate outbound
Expand Down
64 changes: 60 additions & 4 deletions cli-engine/src/auth/commands.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,73 @@
use clap::{Arg, ArgAction};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};

use super::Dispatcher;
use crate::{
CliCoreError, CommandContext, CommandResult, CommandSpec, Credential, GroupSpec, Result,
RuntimeCommandSpec, RuntimeGroupSpec, Tier,
CliCoreError, CommandContext, CommandResult, CommandSpec, Credential, GroupSpec, HumanViewDef,
Result, RuntimeCommandSpec, RuntimeGroupSpec, TableColumn, Tier,
};

/// Human view id the built-in `auth status` command renders with.
///
/// Register a [`HumanViewDef`] under this id (via
/// [`CliConfig::with_view`](crate::CliConfig::with_view) or
/// [`register_global_human_view`](crate::register_global_human_view)) to
/// replace the engine's [`default_auth_status_view`].
pub const AUTH_STATUS_VIEW_ID: &str = "auth:status";

/// Human view id the built-in `auth login` command renders with. See
/// [`AUTH_STATUS_VIEW_ID`].
pub const AUTH_LOGIN_VIEW_ID: &str = "auth:login";

/// The engine's default human view for `auth status`: identity and expiry
/// first, then the environment and provider they belong to.
///
/// A consumer overriding [`AUTH_STATUS_VIEW_ID`] can start from this and
/// drop or reorder columns, for example to hide `provider` when only one is
/// registered.
#[must_use]
pub fn default_auth_status_view() -> HumanViewDef {
HumanViewDef::new(
AUTH_STATUS_VIEW_ID,
vec![
TableColumn::new("identity", "Identity"),
TableColumn::new("expires_at", "Expires"),
TableColumn::new("env", "Env"),
TableColumn::new("provider", "Provider"),
TableColumn::new("expired", "Expired"),
TableColumn::new("refreshable", "Refreshable"),
TableColumn::new("scopes", "Scopes"),
],
)
}

/// The engine's default human view for `auth login`. See
/// [`default_auth_status_view`].
#[must_use]
pub fn default_auth_login_view() -> HumanViewDef {
HumanViewDef::new(
AUTH_LOGIN_VIEW_ID,
vec![
TableColumn::new("identity", "Identity"),
TableColumn::new("expires_at", "Expires"),
TableColumn::new("env", "Env"),
TableColumn::new("provider", "Provider"),
TableColumn::new("refreshable", "Refreshable"),
TableColumn::new("scopes", "Scopes"),
],
)
}

/// Data rendered after a successful `auth login`.
///
/// Built by [`login_and_build`]/[`login_and_build_with_scopes`] from the
/// dispatcher's [`Credential`]. Consumer code receives it as a command
/// result and should not construct it directly — `#[non_exhaustive]` lets
/// this framework add fields (as it did for [`refreshable`](Self::refreshable))
/// without another breaking release.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[non_exhaustive]
pub struct AuthLoginResult {
/// Provider used for login.
Expand Down Expand Up @@ -46,7 +98,7 @@ pub struct AuthLoginResult {
/// result and should not construct it directly — `#[non_exhaustive]` lets
/// this framework add fields (as it did for [`refreshable`](Self::refreshable))
/// without another breaking release.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[non_exhaustive]
pub struct AuthStatusEntry {
/// Provider name.
Expand Down Expand Up @@ -86,6 +138,8 @@ pub fn auth_command_group(default_provider: &str, registered_names: &[String]) -
.with_tier(Tier::Mutate)
.mutates(true)
.no_auth(true)
.with_view_id(AUTH_LOGIN_VIEW_ID)
.with_json_schema::<AuthLoginResult>()
.with_arg(provider_arg(&effective_default, registered_names))
.with_arg(Arg::new("env").long("env").value_name("ENV"))
.with_arg(
Expand Down Expand Up @@ -115,6 +169,8 @@ pub fn auth_command_group(default_provider: &str, registered_names: &[String]) -
CommandSpec::new("status", "Show cached credential status")
.with_system("auth")
.no_auth(true)
.with_view_id(AUTH_STATUS_VIEW_ID)
.with_json_schema::<AuthStatusEntry>()
.with_arg(provider_arg(&effective_default, registered_names))
.with_arg(Arg::new("env").long("env").value_name("ENV")),
async |context| {
Expand Down
3 changes: 2 additions & 1 deletion cli-engine/src/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ pub mod storage;
use async_trait::async_trait;

pub use commands::{
AuthLoginResult, AuthStatusEntry, auth_command_group, login_and_build,
AUTH_LOGIN_VIEW_ID, AUTH_STATUS_VIEW_ID, AuthLoginResult, AuthStatusEntry, auth_command_group,
default_auth_login_view, default_auth_status_view, login_and_build,
login_and_build_with_scopes, logout_result, status_result, to_status_entry,
};
pub use credential::{CACHE_TTL, Credential};
Expand Down
24 changes: 24 additions & 0 deletions cli-engine/src/cli/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@ pub struct CliConfig {
/// the built-in group is built, so the built-ins are never lost or
/// overwritten.
pub auth_extra_commands: Vec<RuntimeCommandSpec>,
/// Default `--fields` for the built-in `auth status`. Populate via
/// [`CliConfig::with_auth_status_default_fields`].
pub auth_status_default_fields: Option<String>,
/// Default `--fields` for the built-in `auth login`. Populate via
/// [`CliConfig::with_auth_login_default_fields`].
pub auth_login_default_fields: Option<String>,
/// Global guide entries mounted under `guide`.
pub guides: Vec<GuideEntry>,
/// Global human output views.
Expand Down Expand Up @@ -497,6 +503,24 @@ impl CliConfig {
self
}

/// Sets the default `--fields` of the built-in `auth status`.
///
/// Same semantics as [`CommandSpec::with_default_fields`](crate::CommandSpec::with_default_fields): the fields
/// shown (and their order) when `--fields` is absent.
#[must_use]
pub fn with_auth_status_default_fields(mut self, fields: impl Into<String>) -> Self {
self.auth_status_default_fields = Some(fields.into());
self
}

/// Sets the default `--fields` of the built-in `auth login`. See
/// [`CliConfig::with_auth_status_default_fields`].
#[must_use]
pub fn with_auth_login_default_fields(mut self, fields: impl Into<String>) -> Self {
self.auth_login_default_fields = Some(fields.into());
self
}

/// Adds one global guide.
#[must_use]
pub fn with_guide(mut self, guide: GuideEntry) -> Self {
Expand Down
9 changes: 9 additions & 0 deletions cli-engine/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,15 @@ impl Cli {
.subcommand(Command::new("tree").about("Display full command tree"))
.subcommand(completion_command())
.subcommand(search_command());
// The flag is only a default-on behavior for a CLI that opted into TTY
// auto-detection, so say so only then.
if config.auto_interactive {
root = root.mut_arg("interactive", |arg| {
arg.help(
"Force interactive prompts for missing inputs (default when a TTY is detected)",
)
});
}
if let Some(register_flags) = &config.register_flags {
root = register_flags(root);
}
Expand Down
33 changes: 31 additions & 2 deletions cli-engine/src/cli/registration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,19 @@ use clap::builder::PossibleValuesParser;

use super::{
Cli,
config::{BUILTIN_COMMAND_NAMES, DEFAULT_ADMIN_CATEGORY},
config::{BUILTIN_COMMAND_NAMES, CliConfig, DEFAULT_ADMIN_CATEGORY},
help::{ModuleHelpEntry, build_root_long},
lookup::has_subcommand,
schema_tree::{
prune_feature_flag_tree, register_runtime_group_metadata,
runtime_group_clap_command_with_schema_help,
},
};
use crate::{FeatureFlag, RuntimeGroupSpec, auth::commands::auth_command_group};
use crate::auth::commands::{
auth_command_group, default_auth_login_view, default_auth_status_view,
};
use crate::output::HumanViewRegistry;
use crate::{FeatureFlag, RuntimeGroupSpec};

/// Lists the auto-registered `auth` command under the admin help category so
/// it is never uncategorized once clap's auto subcommand list is suppressed.
Expand Down Expand Up @@ -182,6 +186,7 @@ pub(super) fn ensure_auth_command(cli: &mut Cli) {
return;
}
let mut group = auth_command_group(&default_provider, &registered_names);
apply_auth_default_fields(&mut group, &cli.config);
let mut seen_names: std::collections::HashSet<String> =
group.commands.iter().map(|c| c.spec.name.clone()).collect();
for extra in cli.config.auth_extra_commands.clone() {
Expand All @@ -202,6 +207,7 @@ pub(super) fn ensure_auth_command(cli: &mut Cli) {
&mut cli.middleware.schema_registry,
&mut cli.middleware.human_views,
);
register_default_auth_views(&mut cli.middleware.human_views);
Comment thread
jpage-godaddy marked this conversation as resolved.
let mut prefix = Vec::new();
group.register_commands(&mut prefix, &mut cli.commands);
let mut prefix = Vec::new();
Expand All @@ -221,6 +227,29 @@ pub(super) fn ensure_auth_command(cli: &mut Cli) {
register_auth_help_entry(cli);
}

/// Applies the consumer's default `--fields` to the built-in auth commands.
fn apply_auth_default_fields(group: &mut RuntimeGroupSpec, config: &CliConfig) {
for command in &mut group.commands {
let fields = match command.spec.name.as_str() {
"status" => config.auth_status_default_fields.as_ref(),
"login" => config.auth_login_default_fields.as_ref(),
_ => None,
};
if let Some(fields) = fields {
command.spec = command.spec.clone().with_default_fields(fields.clone());
}
}
}

/// Registers the engine's default `auth status`/`auth login` views.
fn register_default_auth_views(views: &mut HumanViewRegistry) {
for default in [default_auth_status_view(), default_auth_login_view()] {
if !views.has_view(&default.schema_id) {
views.register(default);
}
}
}

/// Mounts the built-in `config` command group and files it under the admin
/// help category. Idempotent and yields to a consumer-defined `config`
/// subcommand if one already exists.
Expand Down
9 changes: 8 additions & 1 deletion cli-engine/src/cli/schema_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,11 @@ fn apply_fields_arg(
let mut help = String::from(
"Comma-separated fields to include in output (use 'all' or '*' for everything)",
);
// Clap appends its own `[default: ...]` after the last line of help, which
// lands on the end of the field table's final row; render it up front instead.
if let Some(default_value) = default_value {
help.push_str(&format!(" [default: {default_value}]"));
}
if let Some(table) = &table {
help.push_str("\n\n");
help.push_str(table.trim_end());
Expand All @@ -377,7 +382,9 @@ fn apply_fields_arg(
.display_order(crate::flags::global_flag_order::FIELDS)
.help(help);
if let Some(default_value) = default_value {
arg = arg.default_value(default_value.to_owned());
arg = arg
.default_value(default_value.to_owned())
.hide_default_value(true);
}
command.arg(arg)
}
Expand Down
2 changes: 1 addition & 1 deletion cli-engine/src/flags/register.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ pub fn register_global_flags(command: Command) -> Command {
.action(ArgAction::SetTrue)
.conflicts_with("non-interactive")
.display_order(global_flag_order::INTERACTIVE)
.help("Force interactive prompts for missing inputs (default when TTY is detected)"),
.help("Force interactive prompts for missing inputs"),
)
.arg(
Arg::new("non-interactive")
Expand Down
5 changes: 3 additions & 2 deletions cli-engine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,9 @@ pub use auth::storage::{
CredentialKey, CredentialStorage, FileStorage, default_storage, storage_for,
};
pub use auth::{
AuthLoginResult, AuthProvider, AuthStatusEntry, CACHE_TTL, Credential, CredentialRequest,
Dispatcher, SingleProvider, StatusEntry, auth_command_group, login_and_build,
AUTH_LOGIN_VIEW_ID, AUTH_STATUS_VIEW_ID, AuthLoginResult, AuthProvider, AuthStatusEntry,
CACHE_TTL, Credential, CredentialRequest, Dispatcher, SingleProvider, StatusEntry,
auth_command_group, default_auth_login_view, default_auth_status_view, login_and_build,
login_and_build_with_scopes, logout_result, status_result, to_status_entry,
};
pub use cli::{
Expand Down
12 changes: 11 additions & 1 deletion cli-engine/src/middleware/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -497,11 +497,21 @@ impl Middleware {
// the full payload — the data is not projected, which would
// otherwise blank out the kept columns. Everywhere else (JSON/TOON,
// or generic human output) it projects the output data.
// `--expr` hands the caller control of the result's shape, so
// neither the command's `default_fields` nor its view describe the
// result any longer. Fall back to no projection (not `default_fields`,
// whose names the expression's result need not contain — which would
// silently project to `{}`), and render generically so an explicit
// `--fields` is validated and projected against the result, as for
// JSON. An explicit `--fields` still applies. `--filter` keeps both.
let effective_fields = if self.fields_explicit {
self.fields.as_str()
} else {
} else if self.expr.is_empty() {
default_fields
} else {
""
};
let view_id = if self.expr.is_empty() { view_id } else { "" };
let human_view = output_format == OutputFormat::Human && self.human_views.has_view(view_id);
// `apply_pipeline` never sees `effective_fields` for a registered
// view (`projection_fields` below is forced to `""` so the view reads
Expand Down
Loading
Loading