Skip to content

Commit

Permalink
Add org support for init/project/analyze/history (#1490)
Browse files Browse the repository at this point in the history
This patch uses the `Config::org` function to add support for org
support in all subcommands that were still missing org support.

Generally when an org is linked, the org mode is always preferred. Only
in scenarios where no group is present at all, like `phylum project
delete <NAME>`, the personal projects will be used instead.

Since this could potentially lead to a lot of confusion with the project
management subcommands, all project subcommands have been changed to
print projects in a fixed format that lists the corresponding org and
group:

```
✅ Successfully deleted project cli (org: phylum, group: org-group)
```

```
✅ Successfully deleted project demo (org: -, group: -)
```

Closes #1482.
  • Loading branch information
cd-work authored Sep 10, 2024
1 parent 907fa31 commit f890fea
Show file tree
Hide file tree
Showing 19 changed files with 353 additions and 188 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Added

- Organization management under the `phylum org` subcommand
- Organization support for existing subcommands
- `phylum project update --default-label` option to set a project's default label
- `phylum project list --no-group` flag to only show personal projects
- Organization support for `phylum group` subcommands

### Removed

Expand Down
73 changes: 46 additions & 27 deletions cli/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ use crate::auth::{
};
use crate::config::{AuthInfo, Config};
use crate::types::{
AddOrgUserRequest, AnalysisPackageDescriptor, CreateProjectRequest, GetProjectResponse,
HistoryJob, ListUserGroupsResponse, OrgGroup, OrgGroupsResponse, OrgMembersResponse,
AddOrgUserRequest, AnalysisPackageDescriptor, ApiOrgGroup, CreateProjectRequest,
GetProjectResponse, HistoryJob, ListUserGroupsResponse, OrgGroupsResponse, OrgMembersResponse,
OrgsResponse, PackageSpecifier, PackageSubmitResponse, Paginated, PingResponse,
PolicyEvaluationRequest, PolicyEvaluationResponse, PolicyEvaluationResponseRaw,
ProjectListEntry, RevokeTokenRequest, SubmitPackageRequest, UpdateProjectRequest, UserToken,
Expand Down Expand Up @@ -261,14 +261,21 @@ impl PhylumApi {
pub async fn create_project(
&self,
name: impl Into<String>,
org: Option<&str>,
group: Option<String>,
repository_url: Option<String>,
) -> Result<ProjectId> {
let group_name = match (org, group) {
(Some(org), Some(group)) => Some(format!("{org}/{group}")),
(None, Some(group)) => Some(group),
(Some(_), None) | (None, None) => None,
};

let url = endpoints::create_project(&self.config.connection.uri)?;
let body = CreateProjectRequest {
repository_url,
group_name,
default_label: None,
group_name: group,
name: name.into(),
};
let response: CreateProjectResponse = self.post(url, body).await?;
Expand All @@ -279,18 +286,21 @@ impl PhylumApi {
pub async fn update_project(
&self,
project_id: &str,
org: Option<String>,
group: Option<String>,
name: impl Into<String>,
repository_url: Option<String>,
default_label: Option<String>,
) -> Result<ProjectId> {
let url = endpoints::project(&self.config.connection.uri, project_id)?;
let body = UpdateProjectRequest {
repository_url,
default_label,
name: name.into(),
group_name: group,
let group_name = match (org, group) {
(Some(org), Some(group)) => Some(format!("{org}/{group}")),
(None, Some(group)) => Some(group),
(Some(_), None) | (None, None) => None,
};

let url = endpoints::project(&self.config.connection.uri, project_id)?;
let body =
UpdateProjectRequest { repository_url, default_label, name: name.into(), group_name };
let response: CreateProjectResponse = self.put(url, body).await?;
Ok(response.id)
}
Expand All @@ -312,14 +322,24 @@ impl PhylumApi {
/// equivalent to filtering with [`str::contains`].
pub async fn get_projects(
&self,
org: Option<&str>,
group: Option<&str>,
name_filter: Option<&str>,
) -> Result<Vec<ProjectListEntry>> {
let mut uri = endpoints::projects(&self.config.connection.uri)?;

// Add filter query parameters.
if let Some(group) = group {
uri.query_pairs_mut().append_pair("filter.group", group);
match (org, group) {
(Some(org), Some(group)) => {
uri.query_pairs_mut().append_pair("filter.group", &format!("{org}/{group}"));
},
(Some(org), None) => {
uri.query_pairs_mut().append_pair("filter.organization", org);
},
(None, Some(group)) => {
uri.query_pairs_mut().append_pair("filter.group", group);
},
(None, None) => (),
}
if let Some(name_filter) = name_filter {
uri.query_pairs_mut().append_pair("filter.name", name_filter);
Expand Down Expand Up @@ -415,33 +435,32 @@ impl PhylumApi {
pub async fn get_project_history(
&self,
project_name: &str,
group_name: Option<&str>,
org: Option<&str>,
group: Option<&str>,
) -> Result<Vec<HistoryJob>> {
let project_id = self.get_project_id(project_name, group_name).await?.to_string();

let url = match group_name {
Some(group_name) => endpoints::get_group_project_history(
&self.config.connection.uri,
&project_id,
group_name,
)?,
None => endpoints::get_project_history(&self.config.connection.uri, &project_id)?,
};

let project_id = self.get_project_id(project_name, org, group).await?.to_string();
let url = endpoints::get_project_history(&self.config.connection.uri, &project_id)?;
self.get(url).await
}

/// Resolve a Project Name to a Project ID
pub async fn get_project_id(
&self,
project_name: &str,
group_name: Option<&str>,
org: Option<&str>,
group: Option<&str>,
) -> Result<ProjectId> {
let projects = self.get_projects(group_name, Some(project_name)).await?;
let (org, group, combined_format) = match (org, group) {
(Some(org), Some(group)) => (Some(org), Some(group), Some(format!("{org}/{group}"))),
(None, Some(group)) => (None, Some(group), Some(group.to_string())),
(_, None) => (None, None, None),
};

let projects = self.get_projects(org, group, Some(project_name)).await?;

projects
.iter()
.find(|project| project.name == project_name)
.find(|project| project.name == project_name && project.group_name == combined_format)
.ok_or_else(|| anyhow!("No project found with name {:?}", project_name).into())
.map(|project| project.id)
}
Expand Down Expand Up @@ -504,7 +523,7 @@ impl PhylumApi {
/// Create a new organization group.
pub async fn org_create_group(&self, org_name: &str, group_name: &str) -> Result<()> {
let url = endpoints::org_groups(&self.config.connection.uri, org_name)?;
let body = OrgGroup { name: group_name.into() };
let body = ApiOrgGroup { name: group_name.into() };
self.send_request_raw(Method::POST, url, Some(body)).await?;
Ok(())
}
Expand Down
9 changes: 5 additions & 4 deletions cli/src/bin/phylum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,13 +135,14 @@ async fn handle_commands() -> CommandResult {
"parse-sandboxed" => parse::handle_parse_sandboxed(sub_matches),
"ping" => handle_ping(Spinner::wrap(api).await?).await,
"project" => {
project::handle_project(&Spinner::wrap(api).await?, app_helper, sub_matches).await
project::handle_project(&Spinner::wrap(api).await?, app_helper, sub_matches, config)
.await
},
"package" => packages::handle_get_package(&Spinner::wrap(api).await?, sub_matches).await,
"history" => jobs::handle_history(&Spinner::wrap(api).await?, sub_matches).await,
"history" => jobs::handle_history(&Spinner::wrap(api).await?, sub_matches, config).await,
"group" => group::handle_group(&Spinner::wrap(api).await?, sub_matches, config).await,
"analyze" => jobs::handle_analyze(&Spinner::wrap(api).await?, sub_matches).await,
"init" => init::handle_init(&Spinner::wrap(api).await?, sub_matches).await,
"analyze" => jobs::handle_analyze(&Spinner::wrap(api).await?, sub_matches, config).await,
"init" => init::handle_init(&Spinner::wrap(api).await?, sub_matches, config).await,
"status" => status::handle_status(sub_matches).await,
"org" => org::handle_org(&Spinner::wrap(api).await?, sub_matches, config).await,

Expand Down
16 changes: 11 additions & 5 deletions cli/src/commands/extensions/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,12 +168,15 @@ async fn analyze(
#[string] project: Option<String>,
#[string] group: Option<String>,
#[string] label: Option<String>,
#[string] organization: Option<String>,
) -> Result<JobId> {
let state = ExtensionState::from(op_state);
let api = state.api().await?;

let (project, group) = match (project, group) {
(Some(project), group) => (api.get_project_id(&project, group.as_deref()).await?, None),
(Some(project), group) => {
(api.get_project_id(&project, organization.as_deref(), group.as_deref()).await?, None)
},
(None, _) => {
if let Some(p) = phylum_project::get_current_project() {
(p.id, p.group_name)
Expand Down Expand Up @@ -329,11 +332,12 @@ async fn get_groups(op_state: Rc<RefCell<OpState>>) -> Result<ListUserGroupsResp
async fn get_projects(
op_state: Rc<RefCell<OpState>>,
#[string] group: Option<String>,
#[string] organization: Option<String>,
) -> Result<Vec<ProjectListEntry>> {
let state = ExtensionState::from(op_state);
let api = state.api().await?;

api.get_projects(group.as_deref(), None).await.map_err(Error::from)
api.get_projects(organization.as_deref(), group.as_deref(), None).await.map_err(Error::from)
}

#[derive(Serialize)]
Expand All @@ -356,15 +360,16 @@ async fn create_project(
#[string] name: String,
#[string] group: Option<String>,
#[string] repository_url: Option<String>,
#[string] organization: Option<String>,
) -> Result<CreatedProject> {
let state = ExtensionState::from(op_state);
let api = state.api().await?;

// Retrieve the id if the project already exists, otherwise return the id or the
// error.
match api.create_project(&name, group.clone(), repository_url).await {
match api.create_project(&name, organization.as_deref(), group.clone(), repository_url).await {
Err(PhylumApiError::Response(ResponseError { code: StatusCode::CONFLICT, .. })) => api
.get_project_id(&name, group.as_deref())
.get_project_id(&name, organization.as_deref(), group.as_deref())
.await
.map(|id| CreatedProject { id, status: CreatedProjectStatus::Exists })
.map_err(|e| e.into()),
Expand All @@ -379,11 +384,12 @@ async fn delete_project(
op_state: Rc<RefCell<OpState>>,
#[string] name: String,
#[string] group: Option<String>,
#[string] organization: Option<String>,
) -> Result<()> {
let state = ExtensionState::from(op_state);
let api = state.api().await?;

let project_id = api.get_project_id(&name, group.as_deref()).await?;
let project_id = api.get_project_id(&name, organization.as_deref(), group.as_deref()).await?;
api.delete_project(project_id).await.map_err(|e| e.into())
}

Expand Down
30 changes: 19 additions & 11 deletions cli/src/commands/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ use reqwest::StatusCode;

use crate::api::{PhylumApi, PhylumApiError, ResponseError};
use crate::commands::{project, CommandResult, ExitCode};
use crate::types::UserGroup;
use crate::{config, print_user_success, print_user_warning};
use crate::config::{self, Config};
use crate::{print_user_success, print_user_warning};

/// Handle `phylum init` subcommand.
pub async fn handle_init(api: &PhylumApi, matches: &ArgMatches) -> CommandResult {
pub async fn handle_init(api: &PhylumApi, matches: &ArgMatches, config: Config) -> CommandResult {
// Prompt for confirmation if a linked project is already in this directory.
if !matches.get_flag("force") && phylum_project::find_project_conf(".", false).is_some() {
print_user_warning!("Workspace is already linked to a Phylum project");
Expand All @@ -41,7 +41,15 @@ pub async fn handle_init(api: &PhylumApi, matches: &ArgMatches) -> CommandResult
let cli_group = matches.get_one::<String>("group");

// Get available groups from API.
let groups = api.get_groups_list().await?.groups;
let org = config.org();
let groups: Vec<_> = match org {
Some(org) => {
api.org_groups(org).await?.groups.into_iter().map(|group| group.name).collect()
},
None => {
api.get_groups_list().await?.groups.into_iter().map(|group| group.group_name).collect()
},
};

// Interactively prompt for missing project information.
let (project, group, repository_url) =
Expand All @@ -51,12 +59,13 @@ pub async fn handle_init(api: &PhylumApi, matches: &ArgMatches) -> CommandResult
let depfiles = prompt_depfiles(cli_depfiles, cli_depfile_type)?;

// Attempt to create the project.
let result = project::create_project(api, &project, group.clone(), repository_url).await;
let result = project::create_project(api, &project, org, group.clone(), repository_url).await;

let mut project_config = match result {
// If project already exists, try looking it up to link to it.
Err(PhylumApiError::Response(ResponseError { code: StatusCode::CONFLICT, .. })) => {
let uuid = project::lookup_project(api, &project, group.as_deref())
let uuid = api
.get_project_id(&project, org, group.as_deref())
.await
.context(format!("Could not find project {project:?}"))?;
ProjectConfig::new(uuid, project, group)
Expand All @@ -78,7 +87,7 @@ pub async fn handle_init(api: &PhylumApi, matches: &ArgMatches) -> CommandResult

/// Interactively ask for missing project information.
async fn prompt_project(
groups: &[UserGroup],
groups: &[String],
cli_project: Option<&String>,
cli_group: Option<&String>,
cli_repository_url: Option<&String>,
Expand Down Expand Up @@ -138,16 +147,15 @@ fn prompt_project_name() -> io::Result<String> {
}

/// Ask for the desired group.
pub fn prompt_group(groups: &[UserGroup]) -> anyhow::Result<Option<String>> {
pub fn prompt_group(groups: &[String]) -> anyhow::Result<Option<String>> {
// Skip group selection if user has none.
if groups.is_empty() {
return Ok(None);
}

// Map groups to their name.
let group_names = iter::once("[None]")
.chain(groups.iter().map(|group| group.group_name.as_str()))
.collect::<Vec<_>>();
let no_group = String::from("[None]");
let group_names = iter::once(&no_group).chain(groups).collect::<Vec<_>>();

println!();

Expand Down
32 changes: 25 additions & 7 deletions cli/src/commands/jobs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ use crate::api::PhylumApi;
#[cfg(feature = "vulnreach")]
use crate::auth::jwt::RealmRole;
use crate::commands::{parse, CommandResult, ExitCode};
use crate::config::{self, Config};
use crate::format::Format;
use crate::types::AnalysisPackageDescriptor;
#[cfg(feature = "vulnreach")]
use crate::vulnreach;
use crate::{config, print_user_failure, print_user_success, print_user_warning};
use crate::{print_user_failure, print_user_success, print_user_warning};

/// Output analysis job results.
pub async fn print_job_status(
Expand Down Expand Up @@ -57,7 +58,11 @@ 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: &PhylumApi, matches: &clap::ArgMatches) -> CommandResult {
pub async fn handle_history(
api: &PhylumApi,
matches: &clap::ArgMatches,
config: Config,
) -> CommandResult {
let pretty_print = !matches.get_flag("json");

if let Some(job_id) = matches.get_one::<String>("JOB_ID") {
Expand All @@ -66,7 +71,9 @@ pub async fn handle_history(api: &PhylumApi, matches: &clap::ArgMatches) -> Comm
return print_job_status(api, &job_id, [], pretty_print).await;
} else if let Some(project) = matches.get_one::<String>("project") {
let group = matches.get_one::<String>("group").map(String::as_str);
let history = api.get_project_history(project, group).await?;

let history = api.get_project_history(project, config.org(), group).await?;

history.write_stdout(pretty_print);
} else {
let resp = match api.get_status().await {
Expand All @@ -88,13 +95,17 @@ pub async fn handle_history(api: &PhylumApi, matches: &clap::ArgMatches) -> Comm
}

/// Handle `phylum analyze` subcommand.
pub async fn handle_analyze(api: &PhylumApi, matches: &clap::ArgMatches) -> CommandResult {
pub async fn handle_analyze(
api: &PhylumApi,
matches: &clap::ArgMatches,
config: Config,
) -> CommandResult {
let sandbox_generation = !matches.get_flag("skip-sandbox");
let generate_lockfiles = !matches.get_flag("no-generation");
let label = matches.get_one::<String>("label");
let pretty_print = !matches.get_flag("json");

let jobs_project = JobsProject::new(api, matches).await?;
let jobs_project = JobsProject::new(api, matches, config).await?;

// Get .phylum_project path.
let current_project = phylum_project::get_current_project();
Expand Down Expand Up @@ -251,15 +262,22 @@ impl JobsProject {
///
/// Assumes that the clap `matches` has a `project` and `group` arguments
/// option.
async fn new(api: &PhylumApi, matches: &clap::ArgMatches) -> Result<JobsProject> {
async fn new(
api: &PhylumApi,
matches: &clap::ArgMatches,
config: Config,
) -> Result<JobsProject> {
let current_project = phylum_project::get_current_project();
let depfiles = config::depfiles(matches, current_project.as_ref())?;

match matches.get_one::<String>("project") {
// Prefer `--project` and `--group` if they were specified.
Some(project_name) => {
let group = matches.get_one::<String>("group").cloned();
let project = api.get_project_id(project_name, group.as_deref()).await?;

let project =
api.get_project_id(project_name, config.org(), group.as_deref()).await?;

Ok(Self { project_id: project, group, depfiles })
},
// Retrieve the project from the `.phylum_project` file.
Expand Down
Loading

0 comments on commit f890fea

Please sign in to comment.