diff --git a/CHANGELOG.md b/CHANGELOG.md index 0176d53be..3c8caba0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added - `phylum auth list-tokens` subcommand to list API tokens +- `phylum auth revoke-token` subcommand to revoke API tokens ## [5.6.0] - 2023-08-08 diff --git a/clap_markdown/src/lib.rs b/clap_markdown/src/lib.rs index e9461ccd9..848c87ce2 100644 --- a/clap_markdown/src/lib.rs +++ b/clap_markdown/src/lib.rs @@ -133,13 +133,17 @@ fn generate_argument(arg: &Arg) -> Option { } // Add arguments. - let min_required = arg.get_num_args().map_or(0, |num| num.min_values()); - let all_optional = arg.is_positional() && !arg.is_required_set(); - if let Some(value_names) = arg.get_value_names() { + if arg.get_num_args().map_or(false, |range| range.max_values() > 0) { + let default_name = [arg.get_id().to_string().into()]; + let value_names = arg.get_value_names().unwrap_or(&default_name); + if !markdown.is_empty() { markdown += " "; } + let min_required = arg.get_num_args().map_or(0, |num| num.min_values()); + let all_optional = arg.is_positional() && !arg.is_required_set(); + let delimiter = arg.get_value_delimiter().unwrap_or(' '); for (i, value_name) in value_names.iter().enumerate() { diff --git a/cli/src/api/endpoints.rs b/cli/src/api/endpoints.rs index 0706e63cb..ed1a0b534 100644 --- a/cli/src/api/endpoints.rs +++ b/cli/src/api/endpoints.rs @@ -174,6 +174,11 @@ pub fn list_tokens(api_uri: &str) -> Result { Ok(get_locksmith_path(api_uri)?.join("tokens")?) } +/// POST /revoke +pub fn revoke_token(api_uri: &str) -> Result { + Ok(get_locksmith_path(api_uri)?.join("revoke")?) +} + /// POST /reachability/vulnerabilities pub fn vulnreach(api_uri: &str) -> Result { Ok(parse_base_url(api_uri)?.join("reachability/vulnerabilities")?) diff --git a/cli/src/api/mod.rs b/cli/src/api/mod.rs index dd102d354..ef177d349 100644 --- a/cli/src/api/mod.rs +++ b/cli/src/api/mod.rs @@ -33,7 +33,7 @@ use crate::auth::{ use crate::config::{AuthInfo, Config}; use crate::types::{ HistoryJob, PingResponse, PolicyEvaluationRequest, PolicyEvaluationResponse, - PolicyEvaluationResponseRaw, UserToken, + PolicyEvaluationResponseRaw, RevokeTokenRequest, UserToken, }; pub mod endpoints; @@ -432,6 +432,14 @@ impl PhylumApi { self.get(url).await } + /// Revoke a locksmith token. + pub async fn revoke_token(&self, name: &str) -> Result<()> { + let url = endpoints::revoke_token(&self.config.connection.uri)?; + let body = RevokeTokenRequest { name }; + self.send_request_raw(Method::POST, url, Some(body)).await?; + Ok(()) + } + /// Get reachable vulnerabilities. #[cfg(feature = "vulnreach")] pub async fn vulnerabilities(&self, job: Job) -> Result> { diff --git a/cli/src/app.rs b/cli/src/app.rs index 24bc7512b..751f402f0 100644 --- a/cli/src/app.rs +++ b/cli/src/app.rs @@ -221,6 +221,7 @@ pub fn add_subcommands(command: Command) -> Command { .subcommand( Command::new("set-token").about("Set the current authentication token").arg( Arg::new("token") + .value_name("TOKEN") .action(ArgAction::Set) .required(false) .help("Authentication token to store (read from stdin if omitted)"), @@ -245,6 +246,14 @@ pub fn add_subcommands(command: Command) -> Command { .long("json") .help("Produce output in json format (default: false)"), ), + ) + .subcommand( + Command::new("revoke-token").about("Revoke an API token").arg( + Arg::new("token-name") + .value_name("TOKEN_NAME") + .action(ArgAction::Append) + .help("Unique token names which identify the tokens"), + ), ), ) .subcommand(Command::new("ping").about("Ping the remote system to verify it is available")) diff --git a/cli/src/auth/server.rs b/cli/src/auth/server.rs index 6bbd3a498..87f0ce301 100644 --- a/cli/src/auth/server.rs +++ b/cli/src/auth/server.rs @@ -64,7 +64,7 @@ async fn keycloak_callback_handler(request: Request) -> Result CommandResult { "version" => handle_version(&app_name, &ver), "parse" => parse::handle_parse(sub_matches), "ping" => handle_ping(Spinner::wrap(api).await?).await, - "project" => project::handle_project(&mut Spinner::wrap(api).await?, sub_matches).await, - "package" => { - packages::handle_get_package(&mut Spinner::wrap(api).await?, sub_matches).await - }, - "history" => jobs::handle_history(&mut Spinner::wrap(api).await?, sub_matches).await, - "group" => group::handle_group(&mut Spinner::wrap(api).await?, sub_matches).await, - "analyze" | "batch" => { - jobs::handle_submission(&mut Spinner::wrap(api).await?, &matches).await - }, + "project" => project::handle_project(&Spinner::wrap(api).await?, sub_matches).await, + "package" => packages::handle_get_package(&Spinner::wrap(api).await?, sub_matches).await, + "history" => jobs::handle_history(&Spinner::wrap(api).await?, sub_matches).await, + "group" => group::handle_group(&Spinner::wrap(api).await?, sub_matches).await, + "analyze" | "batch" => jobs::handle_submission(&Spinner::wrap(api).await?, &matches).await, "init" => init::handle_init(&Spinner::wrap(api).await?, sub_matches).await, "status" => status::handle_status(sub_matches).await, diff --git a/cli/src/commands/auth.rs b/cli/src/commands/auth.rs index 4810310b8..5533733a9 100644 --- a/cli/src/commands/auth.rs +++ b/cli/src/commands/auth.rs @@ -1,7 +1,9 @@ +use std::borrow::Cow; use std::path::Path; use anyhow::{anyhow, Context, Result}; use clap::ArgMatches; +use dialoguer::MultiSelect; use phylum_types::types::auth::RefreshToken; use tokio::io::{self, AsyncBufReadExt, BufReader}; @@ -10,7 +12,7 @@ use crate::auth::is_locksmith_token; use crate::commands::{CommandResult, ExitCode}; use crate::config::{save_config, Config}; use crate::format::Format; -use crate::{auth, print_user_success, print_user_warning}; +use crate::{auth, print_user_failure, print_user_success, print_user_warning}; /// Register a user. Opens a browser, and redirects the user to the oauth server /// registration page @@ -158,6 +160,54 @@ pub async fn handle_auth_list_tokens( Ok(ExitCode::Ok) } +/// Revoke the specified authentication token. +pub async fn handle_auth_revoke_token( + config: Config, + matches: &clap::ArgMatches, + timeout: Option, +) -> CommandResult { + // Create a client with our auth token attached. + let api = PhylumApi::new(config, timeout).await?; + + // If no name is provided, we show a simple selection UI. + let names = match matches.get_many::("token-name") { + Some(names) => names.into_iter().map(Cow::Borrowed).collect(), + None => { + // Get all available tokens from Locksmith API. + let tokens = api.list_tokens().await?; + let mut token_names = tokens.into_iter().map(|token| token.name).collect::>(); + + // Prompt user to select all tokens. + let prompt = "[SPACE] Select [ENTER] Confirm\nAPI tokens which will be revoked"; + let indices = MultiSelect::new().with_prompt(prompt).items(&token_names).interact()?; + + // Get names for all selected tokens. + indices + .into_iter() + .rev() + .map(|index| Cow::Owned(token_names.swap_remove(index))) + .collect::>() + }, + }; + + println!(); + + // Indicate to user why no action was taken. + if names.is_empty() { + print_user_warning!("Skipping revocation: No token selected"); + } + + // Revoke all selected tokens. + for name in names { + match api.revoke_token(&name).await { + Ok(()) => print_user_success!("Successfully revoked token {name:?}"), + Err(err) => print_user_failure!("Could not revoke token {name:?}: {err}"), + } + } + + Ok(ExitCode::Ok) +} + /// Handle the subcommands for the `auth` subcommand. pub async fn handle_auth( config: Config, @@ -184,6 +234,7 @@ pub async fn handle_auth( Some(("token", matches)) => handle_auth_token(&config, matches).await, Some(("set-token", matches)) => handle_auth_set_token(config, matches, config_path).await, Some(("list-tokens", matches)) => handle_auth_list_tokens(config, matches, timeout).await, + Some(("revoke-token", matches)) => handle_auth_revoke_token(config, matches, timeout).await, _ => unreachable!("invalid clap configuration"), } } diff --git a/cli/src/commands/group.rs b/cli/src/commands/group.rs index 71d13ab2e..9c49dd419 100644 --- a/cli/src/commands/group.rs +++ b/cli/src/commands/group.rs @@ -10,7 +10,7 @@ use crate::format::Format; use crate::{print_user_failure, print_user_success, print_user_warning}; /// Handle `phylum group` subcommand. -pub async fn handle_group(api: &mut PhylumApi, matches: &ArgMatches) -> CommandResult { +pub async fn handle_group(api: &PhylumApi, matches: &ArgMatches) -> CommandResult { match matches.subcommand() { Some(("list", matches)) => handle_group_list(api, matches).await, Some(("create", matches)) => handle_group_create(api, matches).await, @@ -31,7 +31,7 @@ pub async fn handle_group(api: &mut PhylumApi, matches: &ArgMatches) -> CommandR } /// Handle `phylum group create` subcommand. -pub async fn handle_group_create(api: &mut PhylumApi, matches: &ArgMatches) -> CommandResult { +pub async fn handle_group_create(api: &PhylumApi, matches: &ArgMatches) -> CommandResult { let group_name = matches.get_one::("group_name").unwrap(); match api.create_group(group_name).await { Ok(response) => { @@ -47,7 +47,7 @@ pub async fn handle_group_create(api: &mut PhylumApi, matches: &ArgMatches) -> C } /// Handle `phylum group delete` subcommand. -pub async fn handle_group_delete(api: &mut PhylumApi, matches: &ArgMatches) -> CommandResult { +pub async fn handle_group_delete(api: &PhylumApi, matches: &ArgMatches) -> CommandResult { let group_name = matches.get_one::("group_name").unwrap(); api.delete_group(group_name).await?; @@ -57,7 +57,7 @@ pub async fn handle_group_delete(api: &mut PhylumApi, matches: &ArgMatches) -> C } /// Handle `phylum group list` subcommand. -pub async fn handle_group_list(api: &mut PhylumApi, matches: &ArgMatches) -> CommandResult { +pub async fn handle_group_list(api: &PhylumApi, matches: &ArgMatches) -> CommandResult { let response = api.get_groups_list().await?; let pretty = !matches.get_flag("json"); @@ -68,7 +68,7 @@ pub async fn handle_group_list(api: &mut PhylumApi, matches: &ArgMatches) -> Com /// Handle `phylum group member add` subcommand. pub async fn handle_member_add( - api: &mut PhylumApi, + api: &PhylumApi, matches: &ArgMatches, group: &str, ) -> CommandResult { @@ -84,7 +84,7 @@ pub async fn handle_member_add( /// Handle `phylum group member remove` subcommand. pub async fn handle_member_remove( - api: &mut PhylumApi, + api: &PhylumApi, matches: &ArgMatches, group: &str, ) -> CommandResult { @@ -100,7 +100,7 @@ pub async fn handle_member_remove( /// Handle `phylum group member` subcommand. pub async fn handle_member_list( - api: &mut PhylumApi, + api: &PhylumApi, matches: &ArgMatches, group: &str, ) -> CommandResult { @@ -113,7 +113,7 @@ pub async fn handle_member_list( } /// Handle `phylum group transfer` subcommand. -pub async fn handle_group_transfer(api: &mut PhylumApi, matches: &ArgMatches) -> CommandResult { +pub async fn handle_group_transfer(api: &PhylumApi, matches: &ArgMatches) -> CommandResult { let group = matches.get_one::("group").unwrap(); let user = matches.get_one::("user").unwrap(); diff --git a/cli/src/commands/jobs.rs b/cli/src/commands/jobs.rs index 39b1f53bf..9c5011d11 100644 --- a/cli/src/commands/jobs.rs +++ b/cli/src/commands/jobs.rs @@ -24,7 +24,7 @@ use crate::{config, print_user_success, print_user_warning}; /// Output analysis job results. pub async fn print_job_status( - api: &mut PhylumApi, + api: &PhylumApi, job_id: &JobId, ignored_packages: impl Into>, pretty: bool, @@ -55,7 +55,7 @@ pub async fn print_job_status( /// This allows us to list last N job runs, list the projects, list runs /// associated with projects, and get the detailed run results for a specific /// job run. -pub async fn handle_history(api: &mut PhylumApi, matches: &clap::ArgMatches) -> CommandResult { +pub async fn handle_history(api: &PhylumApi, matches: &clap::ArgMatches) -> CommandResult { let pretty_print = !matches.get_flag("json"); if let Some(job_id) = matches.get_one::("JOB_ID") { @@ -87,7 +87,7 @@ pub async fn handle_history(api: &mut PhylumApi, matches: &clap::ArgMatches) -> /// Handles submission of packages to the system for analysis and /// displays summary information about the submitted package(s) -pub async fn handle_submission(api: &mut PhylumApi, matches: &clap::ArgMatches) -> CommandResult { +pub async fn handle_submission(api: &PhylumApi, matches: &clap::ArgMatches) -> CommandResult { let mut ignored_packages: Vec = vec![]; let mut packages = vec![]; let mut synch = false; // get status after submission @@ -124,7 +124,7 @@ pub async fn handle_submission(api: &mut PhylumApi, matches: &clap::ArgMatches) ); } - packages.extend(res.into_iter()); + packages.extend(res); } if let Some(base) = matches.get_one::("base") { @@ -227,7 +227,7 @@ pub async fn handle_submission(api: &mut PhylumApi, matches: &clap::ArgMatches) /// Perform vulnerability reachability analysis. #[cfg(feature = "vulnreach")] async fn vulnreach( - api: &mut PhylumApi, + api: &PhylumApi, matches: &clap::ArgMatches, packages: Vec, job_id: String, @@ -281,7 +281,7 @@ impl JobsProject { /// /// Assumes that the clap `matches` has a `project` and `group` arguments /// option. - async fn new(api: &mut PhylumApi, matches: &clap::ArgMatches) -> Result { + async fn new(api: &PhylumApi, matches: &clap::ArgMatches) -> Result { let current_project = phylum_project::get_current_project(); let lockfiles = config::lockfiles(matches, current_project.as_ref())?; diff --git a/cli/src/commands/packages.rs b/cli/src/commands/packages.rs index 9f5cddad1..39de42214 100644 --- a/cli/src/commands/packages.rs +++ b/cli/src/commands/packages.rs @@ -21,7 +21,7 @@ fn parse_package(matches: &ArgMatches) -> Result { } /// Handle the subcommands for the `package` subcommand. -pub async fn handle_get_package(api: &mut PhylumApi, matches: &clap::ArgMatches) -> CommandResult { +pub async fn handle_get_package(api: &PhylumApi, matches: &clap::ArgMatches) -> CommandResult { let pretty_print = !matches.get_flag("json"); let pkg = parse_package(matches)?; diff --git a/cli/src/commands/parse.rs b/cli/src/commands/parse.rs index dc33341c9..b88ff9c8f 100644 --- a/cli/src/commands/parse.rs +++ b/cli/src/commands/parse.rs @@ -64,7 +64,7 @@ pub fn handle_parse(matches: &clap::ArgMatches) -> CommandResult { for lockfile in lockfiles { let parsed_lockfile = parse_lockfile(lockfile.path, project_root, Some(&lockfile.lockfile_type))?; - pkgs.extend(parsed_lockfile.into_iter()); + pkgs.extend(parsed_lockfile); } serde_json::to_writer_pretty(&mut io::stdout(), &pkgs)?; diff --git a/cli/src/commands/project.rs b/cli/src/commands/project.rs index a0716de3e..0ad946dc7 100644 --- a/cli/src/commands/project.rs +++ b/cli/src/commands/project.rs @@ -15,7 +15,7 @@ use crate::{print_user_failure, print_user_success}; /// List the projects in this account. pub async fn get_project_list( - api: &mut PhylumApi, + api: &PhylumApi, pretty_print: bool, group: Option<&str>, ) -> Result<()> { @@ -27,7 +27,7 @@ pub async fn get_project_list( } /// Handle the project subcommand. -pub async fn handle_project(api: &mut PhylumApi, matches: &clap::ArgMatches) -> CommandResult { +pub async fn handle_project(api: &PhylumApi, matches: &clap::ArgMatches) -> CommandResult { if let Some(matches) = matches.subcommand_matches("create") { let name = matches.get_one::("name").unwrap(); let group = matches.get_one::("group").cloned(); diff --git a/cli/src/format.rs b/cli/src/format.rs index ec592620f..b8c39dc79 100644 --- a/cli/src/format.rs +++ b/cli/src/format.rs @@ -322,7 +322,9 @@ impl Format for Vec { impl Format for Vec { fn pretty(&self, writer: &mut W) { // Maximum length of the token name column. - const MAX_TOKEN_WIDTH: usize = 30; + // + // We use `47` here since it is the default CLI token length. + const MAX_TOKEN_WIDTH: usize = 47; let table = format_table:: String, _>(self, &[ ("Name", |token| print::truncate(&token.name, MAX_TOKEN_WIDTH).into_owned()), diff --git a/cli/src/types.rs b/cli/src/types.rs index 7ab9204c0..90ebcdf20 100644 --- a/cli/src/types.rs +++ b/cli/src/types.rs @@ -120,6 +120,12 @@ pub struct UserToken { pub expiry: Option>, } +/// Request body for `/locksmith/v1/revoke`. +#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] +pub struct RevokeTokenRequest<'a> { + pub name: &'a str, +} + #[cfg(test)] mod tests { use phylum_types::types::package::RiskLevel; diff --git a/doc_templates/phylum_auth_revoke-token.md b/doc_templates/phylum_auth_revoke-token.md new file mode 100644 index 000000000..18b2af3ca --- /dev/null +++ b/doc_templates/phylum_auth_revoke-token.md @@ -0,0 +1,13 @@ +{PH-HEADER} + +{PH-MARKDOWN} + +### Examples + +```sh +# Interactively select tokens to revoke. +$ phylum auth revoke-token + +# Revoke tokens "token1" and "token2". +$ phylum auth revoke-token "token1" "token2" +``` diff --git a/doc_templates/phylum_auth_set-token.md b/doc_templates/phylum_auth_set-token.md index e2f6344be..70af20f2b 100644 --- a/doc_templates/phylum_auth_set-token.md +++ b/doc_templates/phylum_auth_set-token.md @@ -11,9 +11,9 @@ hidden: false ```sh # Supply the token directly on the command line -$ phylum auth set-token eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjB9.49jV8bS3WGLP20VBpCDane-kjxfGmO8L6LHgE7mLO9I +$ phylum auth set-token ph0_UyqKk8yRmuO4gRx52os3obQevBluJTGsepQw0bLRmX0 # Supply the token on stdin $ phylum auth set-token -eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjB9.49jV8bS3WGLP20VBpCDane-kjxfGmO8L6LHgE7mLO9I +ph0_UyqKk8yRmuO4gRx52os3obQevBluJTGsepQw0bLRmX0 ``` diff --git a/docs/command_line_tool/phylum_auth.md b/docs/command_line_tool/phylum_auth.md index 1a15fda0d..3e95a20cf 100644 --- a/docs/command_line_tool/phylum_auth.md +++ b/docs/command_line_tool/phylum_auth.md @@ -29,3 +29,4 @@ Usage: phylum auth [OPTIONS] * [phylum auth set-token](./phylum_auth_set-token) * [phylum auth token](./phylum_auth_token) * [phylum auth list-tokens](./phylum_auth_list-tokens) +* [phylum auth revoke-token](./phylum_auth_revoke-token) diff --git a/docs/command_line_tool/phylum_auth_login.md b/docs/command_line_tool/phylum_auth_login.md index 38f8f9389..740861ef8 100644 --- a/docs/command_line_tool/phylum_auth_login.md +++ b/docs/command_line_tool/phylum_auth_login.md @@ -16,7 +16,7 @@ Usage: phylum auth login [OPTIONS] -r, --reauth   Force a login prompt --n, --token-name +-n, --token-name   Unique name for the new token that will be created -v, --verbose... diff --git a/docs/command_line_tool/phylum_auth_register.md b/docs/command_line_tool/phylum_auth_register.md index baa652310..9675fdc69 100644 --- a/docs/command_line_tool/phylum_auth_register.md +++ b/docs/command_line_tool/phylum_auth_register.md @@ -13,7 +13,7 @@ Usage: phylum auth register [OPTIONS] ### Options --n, --token-name +-n, --token-name   Unique name for the new token that will be created -v, --verbose... diff --git a/docs/command_line_tool/phylum_auth_revoke-token.md b/docs/command_line_tool/phylum_auth_revoke-token.md new file mode 100644 index 000000000..20e52bd7c --- /dev/null +++ b/docs/command_line_tool/phylum_auth_revoke-token.md @@ -0,0 +1,37 @@ +--- +title: phylum auth revoke-token +category: 6255e67693d5200013b1fa3e +hidden: false +--- + +Revoke an API token + +```sh +Usage: phylum auth revoke-token [OPTIONS] [TOKEN_NAME]... +``` + +### Arguments + +[TOKEN_NAME] +  Unique token names which identify the tokens + +### Options + +-v, --verbose... +  Increase the level of verbosity (the maximum is -vvv) + +-q, --quiet... +  Reduce the level of verbosity (the maximum is -qq) + +-h, --help +  Print help + +### Examples + +```sh +# Interactively select tokens to revoke. +$ phylum auth revoke-token + +# Revoke tokens "token1" and "token2". +$ phylum auth revoke-token "token1" "token2" +``` diff --git a/docs/command_line_tool/phylum_auth_set-token.md b/docs/command_line_tool/phylum_auth_set-token.md index 876dae7d8..2dd3900e9 100644 --- a/docs/command_line_tool/phylum_auth_set-token.md +++ b/docs/command_line_tool/phylum_auth_set-token.md @@ -8,12 +8,12 @@ hidden: false Set the current authentication token ```sh -Usage: phylum auth set-token [OPTIONS] [token] +Usage: phylum auth set-token [OPTIONS] [TOKEN] ``` ### Arguments - +[TOKEN]   Authentication token to store (read from stdin if omitted) ### Options @@ -31,9 +31,9 @@ Usage: phylum auth set-token [OPTIONS] [token] ```sh # Supply the token directly on the command line -$ phylum auth set-token eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjB9.49jV8bS3WGLP20VBpCDane-kjxfGmO8L6LHgE7mLO9I +$ phylum auth set-token ph0_UyqKk8yRmuO4gRx52os3obQevBluJTGsepQw0bLRmX0 # Supply the token on stdin $ phylum auth set-token -eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjB9.49jV8bS3WGLP20VBpCDane-kjxfGmO8L6LHgE7mLO9I +ph0_UyqKk8yRmuO4gRx52os3obQevBluJTGsepQw0bLRmX0 ```