Skip to content

Commit

Permalink
Merge branch 'main' into use_relative_paths
Browse files Browse the repository at this point in the history
  • Loading branch information
ejortega committed Aug 7, 2023
2 parents 68663a9 + 3ee4001 commit 64931b0
Show file tree
Hide file tree
Showing 15 changed files with 322 additions and 83 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Include lockfile paths when analyzing projects
- Remove root path from lockfile paths

### Fixed
- Search for manifests' lockfiles in parent, rather than child directories

## [5.5.0] - 2023-07-18

### Added
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ deno_ast = { version = "0.27.2", features = ["transpiling"] }
birdcage = { version = "0.2.0" }
libc = "0.2.135"
ignore = { version = "0.4.20", optional = true }
uuid = "1.4.1"

[dev-dependencies]
assert_cmd = "2.0.4"
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 @@ -160,6 +160,11 @@ pub fn oidc_discovery(api_uri: &str) -> Result<Url, BaseUriError> {
Ok(get_api_path(api_uri)?.join(".well-known/openid-configuration")?)
}

/// GET /.well-known/locksmith-configuration
pub fn locksmith_discovery(api_uri: &str) -> Result<Url, BaseUriError> {
Ok(get_api_path(api_uri)?.join(".well-known/locksmith-configuration")?)
}

/// POST /reachability/vulnerabilities
pub fn vulnreach(api_uri: &str) -> Result<Url, BaseUriError> {
Ok(parse_base_url(api_uri)?.join("reachability/vulnerabilities")?)
Expand Down
51 changes: 30 additions & 21 deletions cli/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ use std::collections::HashSet;
use std::time::Duration;

use anyhow::{anyhow, Context};
use phylum_types::types::auth::TokenResponse;
use phylum_types::types::common::{JobId, ProjectId};
use phylum_types::types::group::{
CreateGroupRequest, CreateGroupResponse, ListGroupMembersResponse, ListUserGroupsResponse,
Expand All @@ -28,7 +27,8 @@ use crate::api::endpoints::BaseUriError;
use crate::app::USER_AGENT;
use crate::auth::jwt::RealmRole;
use crate::auth::{
fetch_oidc_server_settings, handle_auth_flow, handle_refresh_tokens, jwt, AuthAction, UserInfo,
fetch_locksmith_server_settings, handle_auth_flow, handle_refresh_tokens, jwt, AuthAction,
UserInfo,
};
use crate::config::{AuthInfo, Config};
use crate::types::{
Expand Down Expand Up @@ -164,26 +164,30 @@ impl PhylumApi {
pub async fn new(mut config: Config, request_timeout: Option<u64>) -> Result<Self> {
// Do we have a refresh token?
let ignore_certs = config.ignore_certs();
let tokens: TokenResponse = match &config.auth_info.offline_access() {
Some(refresh_token) => {
handle_refresh_tokens(refresh_token, ignore_certs, &config.connection.uri)
.await
.context("Token refresh failed")?
let refresh_token = match config.auth_info.offline_access() {
Some(refresh_token) => refresh_token.clone(),
None => {
let refresh_token =
handle_auth_flow(AuthAction::Login, None, ignore_certs, &config.connection.uri)
.await
.context("User login failed")?;
config.auth_info.set_offline_access(refresh_token.clone());
refresh_token
},
None => handle_auth_flow(AuthAction::Login, ignore_certs, &config.connection.uri)
.await
.context("User login failed")?,
};

config.auth_info.set_offline_access(tokens.refresh_token.clone());
let access_token =
handle_refresh_tokens(&refresh_token, ignore_certs, &config.connection.uri)
.await
.context("Token refresh failed")?;

let mut headers = HeaderMap::new();
// the cli runs a command or a few short commands then exits, so we do
// not need to worry about refreshing the access token. We just set it
// here and be done.
headers.insert(
"Authorization",
HeaderValue::from_str(&format!("Bearer {}", tokens.access_token)).unwrap(),
HeaderValue::from_str(&format!("Bearer {}", access_token)).unwrap(),
);
headers.insert("Accept", HeaderValue::from_str("application/json").unwrap());

Expand All @@ -195,7 +199,7 @@ impl PhylumApi {
.build()?;

// Try to parse token's roles.
let roles = jwt::user_roles(tokens.access_token.as_str()).unwrap_or_default();
let roles = jwt::user_roles(access_token.as_str()).unwrap_or_default();

Ok(Self { config, client, roles })
}
Expand All @@ -205,13 +209,14 @@ impl PhylumApi {
/// credentials. It is the duty of the calling code to save any changes
pub async fn login(
mut auth_info: AuthInfo,
token_name: Option<String>,
ignore_certs: bool,
api_uri: &str,
reauth: bool,
) -> Result<AuthInfo> {
let action = if reauth { AuthAction::Reauth } else { AuthAction::Login };
let tokens = handle_auth_flow(action, ignore_certs, api_uri).await?;
auth_info.set_offline_access(tokens.refresh_token);
let refresh_token = handle_auth_flow(action, token_name, ignore_certs, api_uri).await?;
auth_info.set_offline_access(refresh_token);
Ok(auth_info)
}

Expand All @@ -220,11 +225,13 @@ impl PhylumApi {
/// credentials. It is the duty of the calling code to save any changes
pub async fn register(
mut auth_info: AuthInfo,
token_name: Option<String>,
ignore_certs: bool,
api_uri: &str,
) -> Result<AuthInfo> {
let tokens = handle_auth_flow(AuthAction::Register, ignore_certs, api_uri).await?;
auth_info.set_offline_access(tokens.refresh_token);
let refresh_token =
handle_auth_flow(AuthAction::Register, token_name, ignore_certs, api_uri).await?;
auth_info.set_offline_access(refresh_token);
Ok(auth_info)
}

Expand All @@ -238,10 +245,12 @@ impl PhylumApi {

/// Get information about the authenticated user
pub async fn user_info(&self) -> Result<UserInfo> {
let oidc_settings =
fetch_oidc_server_settings(self.config.ignore_certs(), &self.config.connection.uri)
.await?;
self.get(oidc_settings.userinfo_endpoint).await
let locksmith_settings = fetch_locksmith_server_settings(
self.config.ignore_certs(),
&self.config.connection.uri,
)
.await?;
self.get(locksmith_settings.userinfo_endpoint).await
}

/// Create a new project
Expand Down
31 changes: 24 additions & 7 deletions cli/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,16 +188,33 @@ pub fn add_subcommands(command: Command) -> Command {
.about("Manage authentication, registration, and API keys")
.arg_required_else_help(true)
.subcommand_required(true)
.subcommand(Command::new("register").about("Register a new account"))
.subcommand(
Command::new("login").about("Login to an existing account").arg(
Arg::new("reauth")
.action(ArgAction::SetTrue)
.short('r')
.long("reauth")
.help("Force a login prompt"),
Command::new("register").about("Register a new account").arg(
Arg::new("token-name")
.action(ArgAction::Set)
.short('n')
.long("token-name")
.help("API token name"),
),
)
.subcommand(
Command::new("login")
.about("Login to an existing account")
.arg(
Arg::new("reauth")
.action(ArgAction::SetTrue)
.short('r')
.long("reauth")
.help("Force a login prompt"),
)
.arg(
Arg::new("token-name")
.action(ArgAction::Set)
.short('n')
.long("token-name")
.help("API token name"),
),
)
.subcommand(
Command::new("status").about("Return the current authentication status"),
)
Expand Down
4 changes: 4 additions & 0 deletions cli/src/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ mod ip_addr_ext;
pub mod jwt;
mod oidc;
mod server;

pub fn is_locksmith_token(token: impl AsRef<str>) -> bool {
token.as_ref().starts_with("ph0_")
}
84 changes: 68 additions & 16 deletions cli/src/auth/oidc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ use anyhow::{anyhow, Context, Result};
use base64::engine::general_purpose;
use base64::Engine as _;
use maplit::hashmap;
use phylum_types::types::auth::{AuthorizationCode, RefreshToken, TokenResponse};
use phylum_types::types::auth::{AccessToken, AuthorizationCode, RefreshToken, TokenResponse};
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
use reqwest::Url;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use uuid::Uuid;

use super::ip_addr_ext::IpAddrExt;
use super::is_locksmith_token;
use crate::api::endpoints;
use crate::app::USER_AGENT;

Expand All @@ -25,6 +27,9 @@ pub const OIDC_SCOPES: [&str; 2] = ["openid", "offline_access"];
/// OIDC Client id used to identify this client to the oidc server
pub const OIDC_CLIENT_ID: &str = "phylum_cli";

/// Client ID for Locksmith tokens
pub const LOCKSMITH_CLIENT_ID: &str = "locksmith";

#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum AuthAction {
Login,
Expand Down Expand Up @@ -94,26 +99,32 @@ impl<'a> From<&'a CodeVerifier> for &'a str {
/// We only deserialize the ones we care about.
#[derive(Debug, Serialize, Deserialize)]
pub struct OidcServerSettings {
pub issuer: Url,
pub token_endpoint: Url,
}

/// Locksmith URLs
#[derive(Debug, Serialize, Deserialize)]
pub struct LocksmithServerSettings {
pub authorization_endpoint: Url,
pub token_endpoint: Url,
pub userinfo_endpoint: Url,
}

/// Using config information, build the url for the keycloak login page.
pub fn build_auth_url(
action: AuthAction,
oidc_settings: &OidcServerSettings,
locksmith_settings: &LocksmithServerSettings,
callback_url: &Url,
code_challenge: &ChallengeCode,
state: impl AsRef<str>,
) -> Result<Url> {
let mut auth_url = match action {
// Login uses the oidc defined /auth endpoint as is
AuthAction::Login | AuthAction::Reauth => oidc_settings.authorization_endpoint.to_owned(),
AuthAction::Login | AuthAction::Reauth => {
locksmith_settings.authorization_endpoint.to_owned()
},
// Register uses the non-standard /registrations endpoint
AuthAction::Register => {
let mut auth_url = oidc_settings.authorization_endpoint.to_owned();
let mut auth_url = locksmith_settings.authorization_endpoint.to_owned();
auth_url
.path_segments_mut()
.map_err(|_| anyhow!("Can not be base url"))?
Expand All @@ -126,7 +137,7 @@ pub fn build_auth_url(
auth_url
.query_pairs_mut()
.clear()
.append_pair("client_id", OIDC_CLIENT_ID)
.append_pair("client_id", LOCKSMITH_CLIENT_ID)
.append_pair("code_challenge", code_challenge.into())
.append_pair("code_challenge_method", "S256")
.append_pair("redirect_uri", callback_url.as_ref())
Expand Down Expand Up @@ -175,18 +186,42 @@ pub async fn fetch_oidc_server_settings(
}
}

pub async fn fetch_locksmith_server_settings(
ignore_certs: bool,
api_uri: &str,
) -> Result<LocksmithServerSettings> {
let client = reqwest::Client::builder()
.user_agent(USER_AGENT.as_str())
.danger_accept_invalid_certs(ignore_certs)
.build()?;
let response = client
.get(endpoints::locksmith_discovery(api_uri)?)
.header("Accept", "application/json")
.timeout(Duration::from_secs(5))
.send()
.await?;

if let Err(error) = response.error_for_status_ref() {
Err(anyhow!(response.text().await?)).context(error)
} else {
Ok(response.json::<LocksmithServerSettings>().await?)
}
}

fn build_grant_type_auth_code_post_body(
redirect_url: &Url,
authorization_code: &AuthorizationCode,
code_verfier: &CodeVerifier,
token_name: Option<String>,
) -> Result<HashMap<String, String>> {
let body = hashmap! {
"client_id".to_owned() => OIDC_CLIENT_ID.to_owned(),
"client_id".to_owned() => LOCKSMITH_CLIENT_ID.to_owned(),
"code".to_owned() => authorization_code.into(),
"code_verifier".to_owned() => code_verfier.into(),
"grant_type".to_owned() => "authorization_code".to_owned(),
// Must match previous request to /authorize but not redirected to by server
"redirect_uri".to_owned() => redirect_url.to_string(),
"name".to_owned() => token_name.unwrap_or_else(|| format!("phylum-cli-{}", Uuid::new_v4().as_hyphenated())),
};
Ok(body)
}
Expand All @@ -204,16 +239,21 @@ fn build_grant_type_refresh_token_post_body(

/// Acquire tokens with the auth code
pub async fn acquire_tokens(
oidc_settings: &OidcServerSettings,
locksmith_settings: &LocksmithServerSettings,
redirect_url: &Url,
authorization_code: &AuthorizationCode,
code_verifier: &CodeVerifier,
token_name: Option<String>,
ignore_certs: bool,
) -> Result<TokenResponse> {
let token_url = oidc_settings.token_endpoint.clone();
) -> Result<LocksmithTokenResponse> {
let token_url = locksmith_settings.token_endpoint.clone();

let body =
build_grant_type_auth_code_post_body(redirect_url, authorization_code, code_verifier)?;
let body = build_grant_type_auth_code_post_body(
redirect_url,
authorization_code,
code_verifier,
token_name,
)?;

let client = reqwest::Client::builder()
.user_agent(USER_AGENT.as_str())
Expand Down Expand Up @@ -243,7 +283,7 @@ pub async fn acquire_tokens(

err
} else {
Ok(response.json::<TokenResponse>().await?)
Ok(response.json::<LocksmithTokenResponse>().await?)
}
}

Expand Down Expand Up @@ -281,9 +321,21 @@ pub async fn handle_refresh_tokens(
refresh_token: &RefreshToken,
ignore_certs: bool,
api_uri: &str,
) -> Result<TokenResponse> {
) -> Result<AccessToken> {
// Locksmith tokens are their own access tokens
if is_locksmith_token(refresh_token) {
return Ok(AccessToken::new(refresh_token));
}

let oidc_settings = fetch_oidc_server_settings(ignore_certs, api_uri).await?;
refresh_tokens(&oidc_settings, refresh_token, ignore_certs).await
refresh_tokens(&oidc_settings, refresh_token, ignore_certs)
.await
.map(|token| token.access_token)
}

#[derive(Debug, Clone, Deserialize)]
pub struct LocksmithTokenResponse {
pub token: RefreshToken,
}

/// Represents the userdata stored for an authentication token.
Expand Down
Loading

0 comments on commit 64931b0

Please sign in to comment.