Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add phylum auth revoke-token subcommand #1181

Merged
merged 10 commits into from
Aug 10, 2023
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
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