Skip to content

Commit

Permalink
Add phylum auth revoke-token subcommand (#1181)
Browse files Browse the repository at this point in the history
This adds a new subcommand to the `auth` subcommand which allows
revoking Locksmith API tokens.

It is possible to either specify one or more tokens as CLI arguments, or
alternatively interactively select which tokens should be revoked.

Closes #1176.
  • Loading branch information
cd-work authored Aug 10, 2023
1 parent 8bc7337 commit 58a543c
Show file tree
Hide file tree
Showing 22 changed files with 175 additions and 42 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 7 additions & 3 deletions clap_markdown/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,17 @@ fn generate_argument(arg: &Arg) -> Option<String> {
}

// 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() {
Expand Down
5 changes: 5 additions & 0 deletions cli/src/api/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,11 @@ pub fn list_tokens(api_uri: &str) -> Result<Url, BaseUriError> {
Ok(get_locksmith_path(api_uri)?.join("tokens")?)
}

/// POST /revoke
pub fn revoke_token(api_uri: &str) -> Result<Url, BaseUriError> {
Ok(get_locksmith_path(api_uri)?.join("revoke")?)
}

/// POST /reachability/vulnerabilities
pub fn vulnreach(api_uri: &str) -> Result<Url, BaseUriError> {
Ok(parse_base_url(api_uri)?.join("reachability/vulnerabilities")?)
Expand Down
10 changes: 9 additions & 1 deletion cli/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Vec<Vulnerability>> {
Expand Down
9 changes: 9 additions & 0 deletions cli/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)"),
Expand All @@ -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"))
Expand Down
2 changes: 1 addition & 1 deletion cli/src/auth/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ async fn keycloak_callback_handler(request: Request<Body>) -> Result<Response<Bo
.uri()
.query()
.map(|v| url::form_urlencoded::parse(v.as_bytes()).into_owned().collect())
.unwrap_or_else(HashMap::new);
.unwrap_or_default();

// Check that XSRF prevention state was properly returned.
match query_parameters.get("state") {
Expand Down
14 changes: 5 additions & 9 deletions cli/src/bin/phylum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,15 +142,11 @@ async fn handle_commands() -> 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,

Expand Down
53 changes: 52 additions & 1 deletion cli/src/commands/auth.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand All @@ -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
Expand Down Expand Up @@ -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<u64>,
) -> 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::<String>("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::<Vec<_>>();

// 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::<Vec<_>>()
},
};

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,
Expand All @@ -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"),
}
}
16 changes: 8 additions & 8 deletions cli/src/commands/group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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::<String>("group_name").unwrap();
match api.create_group(group_name).await {
Ok(response) => {
Expand All @@ -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::<String>("group_name").unwrap();
api.delete_group(group_name).await?;

Expand All @@ -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");
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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::<String>("group").unwrap();
let user = matches.get_one::<String>("user").unwrap();

Expand Down
12 changes: 6 additions & 6 deletions cli/src/commands/jobs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<PackageDescriptor>>,
pretty: bool,
Expand Down Expand Up @@ -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::<String>("JOB_ID") {
Expand Down Expand Up @@ -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<PackageDescriptor> = vec![];
let mut packages = vec![];
let mut synch = false; // get status after submission
Expand Down Expand Up @@ -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::<String>("base") {
Expand Down Expand Up @@ -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<PackageDescriptor>,
job_id: String,
Expand Down Expand Up @@ -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<JobsProject> {
async fn new(api: &PhylumApi, matches: &clap::ArgMatches) -> Result<JobsProject> {
let current_project = phylum_project::get_current_project();
let lockfiles = config::lockfiles(matches, current_project.as_ref())?;

Expand Down
2 changes: 1 addition & 1 deletion cli/src/commands/packages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ fn parse_package(matches: &ArgMatches) -> Result<PackageSpecifier> {
}

/// 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)?;
Expand Down
2 changes: 1 addition & 1 deletion cli/src/commands/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Expand Down
4 changes: 2 additions & 2 deletions cli/src/commands/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<()> {
Expand All @@ -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::<String>("name").unwrap();
let group = matches.get_one::<String>("group").cloned();
Expand Down
4 changes: 3 additions & 1 deletion cli/src/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,9 @@ impl Format for Vec<HistoryJob> {
impl Format for Vec<UserToken> {
fn pretty<W: Write>(&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::<fn(&UserToken) -> String, _>(self, &[
("Name", |token| print::truncate(&token.name, MAX_TOKEN_WIDTH).into_owned()),
Expand Down
6 changes: 6 additions & 0 deletions cli/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ pub struct UserToken {
pub expiry: Option<DateTime<Utc>>,
}

/// 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;
Expand Down
13 changes: 13 additions & 0 deletions doc_templates/phylum_auth_revoke-token.md
Original file line number Diff line number Diff line change
@@ -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"
```
Loading

0 comments on commit 58a543c

Please sign in to comment.