Skip to content

Commit

Permalink
[SM-1287] Refactor out command logic from main.rs in bws (#809)
Browse files Browse the repository at this point in the history
## 🎟️ Tracking

[SM-1287](https://bitwarden.atlassian.net/browse/SM-1287)

## 📔 Objective

<!-- Describe what the purpose of this PR is, for example what bug
you're fixing or new feature you're adding. -->

The goal of this PR is to refactor `bws/src/main.rs` and the
`process_commands()` function. This PR moves the actual CLI interaction
for individual commands to the `command` module and appropriate
sub-modules.

There are plenty of other additions and changes to be made, but I'm
trying to keep this PR a clean refactor and we can address other items
in separate PR's.

Deprecation of the legacy CLI commands will happen in
[SM-1175](https://bitwarden.atlassian.net/browse/SM-1175).

## ⏰ Reminders before review

- Contributor guidelines followed
- All formatters and local linters executed and passed
- Written new unit and / or integration tests where applicable
- Protected functional changes with optionality (feature flags)
- Used internationalization (i18n) for all UI strings
- CI builds passed
- Communicated to DevOps any deployment requirements
- Updated any necessary documentation (Confluence, contributing docs) or
informed the documentation
  team

## 🦮 Reviewer guidelines

<!-- Suggested interactions but feel free to use (or not) as you desire!
-->

- 👍 (`:+1:`) or similar for great changes
- 📝 (`:memo:`) or ℹ️ (`:information_source:`) for notes or general info
- ❓ (`:question:`) for questions
- 🤔 (`:thinking:`) or 💭 (`:thought_balloon:`) for more open inquiry
that's not quite a confirmed
  issue and could potentially benefit from discussion
- 🎨 (`:art:`) for suggestions / improvements
- ❌ (`:x:`) or ⚠️ (`:warning:`) for more significant problems or
concerns needing attention
- 🌱 (`:seedling:`) or ♻️ (`:recycle:`) for future improvements or
indications of technical debt
- ⛏ (`:pick:`) for minor or nitpick changes


[SM-1287]:
https://bitwarden.atlassian.net/browse/SM-1287?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[SM-1175]:
https://bitwarden.atlassian.net/browse/SM-1175?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
  • Loading branch information
coltonhurst authored Jun 21, 2024
1 parent 00b2120 commit 4b1d6a1
Show file tree
Hide file tree
Showing 6 changed files with 406 additions and 222 deletions.
59 changes: 59 additions & 0 deletions crates/bws/src/command/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
pub(crate) mod project;
pub(crate) mod secret;

use std::{path::PathBuf, str::FromStr};

use bitwarden::auth::AccessToken;
use clap::CommandFactory;
use clap_complete::Shell;
use color_eyre::eyre::{bail, Result};

use crate::{config, Cli, ProfileKey};

pub(crate) fn completions(shell: Option<Shell>) -> Result<()> {
let Some(shell) = shell.or_else(Shell::from_env) else {
bail!("Couldn't autodetect a valid shell. Run `bws completions --help` for more info.");
};

let mut cmd = Cli::command();
let name = cmd.get_name().to_string();
clap_complete::generate(shell, &mut cmd, name, &mut std::io::stdout());

Ok(())
}

pub(crate) fn config(
name: Option<ProfileKey>,
value: Option<String>,
delete: bool,
profile: Option<String>,
access_token: Option<String>,
config_file: Option<PathBuf>,
) -> Result<()> {
let profile = if let Some(profile) = profile {
profile
} else if let Some(access_token) = access_token {
AccessToken::from_str(&access_token)?
.access_token_id
.to_string()
} else {
String::from("default")
};

if delete {
config::delete_profile(config_file.as_deref(), profile)?;
println!("Profile deleted successfully!");
} else {
let (name, value) = match (name, value) {
(None, None) => bail!("Missing `name` and `value`"),
(None, Some(_)) => bail!("Missing `value`"),
(Some(_), None) => bail!("Missing `name`"),
(Some(name), Some(value)) => (name, value),
};

config::update_profile(config_file.as_deref(), profile, name, value)?;
println!("Profile updated successfully!");
};

Ok(())
}
116 changes: 116 additions & 0 deletions crates/bws/src/command/project.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
use bitwarden::{
secrets_manager::projects::{
ProjectCreateRequest, ProjectGetRequest, ProjectPutRequest, ProjectsDeleteRequest,
ProjectsListRequest,
},
Client,
};
use color_eyre::eyre::{bail, Result};
use uuid::Uuid;

use crate::render::{serialize_response, OutputSettings};

pub(crate) async fn list(
client: Client,
organization_id: Uuid,
output_settings: OutputSettings,
) -> Result<()> {
let projects = client
.projects()
.list(&ProjectsListRequest { organization_id })
.await?
.data;
serialize_response(projects, output_settings);

Ok(())
}

pub(crate) async fn get(
client: Client,
project_id: Uuid,
output_settings: OutputSettings,
) -> Result<()> {
let project = client
.projects()
.get(&ProjectGetRequest { id: project_id })
.await?;
serialize_response(project, output_settings);

Ok(())
}

pub(crate) async fn create(
client: Client,
organization_id: Uuid,
name: String,
output_settings: OutputSettings,
) -> Result<()> {
let project = client
.projects()
.create(&ProjectCreateRequest {
organization_id,
name,
})
.await?;
serialize_response(project, output_settings);

Ok(())
}

pub(crate) async fn edit(
client: Client,
organization_id: Uuid,
project_id: Uuid,
name: String,
output_settings: OutputSettings,
) -> Result<()> {
let project = client
.projects()
.update(&ProjectPutRequest {
id: project_id,
organization_id,
name,
})
.await?;
serialize_response(project, output_settings);

Ok(())
}

pub(crate) async fn delete(client: Client, project_ids: Vec<Uuid>) -> Result<()> {
let count = project_ids.len();

let result = client
.projects()
.delete(ProjectsDeleteRequest { ids: project_ids })
.await?;

let projects_failed: Vec<(Uuid, String)> = result
.data
.into_iter()
.filter_map(|r| r.error.map(|e| (r.id, e)))
.collect();
let deleted_projects = count - projects_failed.len();

match deleted_projects {
2.. => println!("{} projects deleted successfully.", deleted_projects),
1 => println!("{} project deleted successfully.", deleted_projects),
_ => (),
}

match projects_failed.len() {
2.. => eprintln!("{} projects had errors:", projects_failed.len()),
1 => eprintln!("{} project had an error:", projects_failed.len()),
_ => (),
}

for project in &projects_failed {
eprintln!("{}: {}", project.0, project.1);
}

if !projects_failed.is_empty() {
bail!("Errors when attempting to delete projects.");
}

Ok(())
}
163 changes: 163 additions & 0 deletions crates/bws/src/command/secret.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
use bitwarden::{
secrets_manager::secrets::{
SecretCreateRequest, SecretGetRequest, SecretIdentifiersByProjectRequest,
SecretIdentifiersRequest, SecretPutRequest, SecretsDeleteRequest, SecretsGetRequest,
},
Client,
};
use color_eyre::eyre::{bail, Result};
use uuid::Uuid;

use crate::render::{serialize_response, OutputSettings};

#[derive(Debug)]
pub(crate) struct SecretCreateCommandModel {
pub(crate) key: String,
pub(crate) value: String,
pub(crate) note: Option<String>,
pub(crate) project_id: Uuid,
}

#[derive(Debug)]
pub(crate) struct SecretEditCommandModel {
pub(crate) id: Uuid,
pub(crate) key: Option<String>,
pub(crate) value: Option<String>,
pub(crate) note: Option<String>,
pub(crate) project_id: Option<Uuid>,
}

pub(crate) async fn list(
client: Client,
organization_id: Uuid,
project_id: Option<Uuid>,
output_settings: OutputSettings,
) -> Result<()> {
let res = if let Some(project_id) = project_id {
client
.secrets()
.list_by_project(&SecretIdentifiersByProjectRequest { project_id })
.await?
} else {
client
.secrets()
.list(&SecretIdentifiersRequest { organization_id })
.await?
};

let secret_ids = res.data.into_iter().map(|e| e.id).collect();
let secrets = client
.secrets()
.get_by_ids(SecretsGetRequest { ids: secret_ids })
.await?
.data;
serialize_response(secrets, output_settings);

Ok(())
}

pub(crate) async fn get(
client: Client,
secret_id: Uuid,
output_settings: OutputSettings,
) -> Result<()> {
let secret = client
.secrets()
.get(&SecretGetRequest { id: secret_id })
.await?;
serialize_response(secret, output_settings);

Ok(())
}

pub(crate) async fn create(
client: Client,
organization_id: Uuid,
secret: SecretCreateCommandModel,
output_settings: OutputSettings,
) -> Result<()> {
let secret = client
.secrets()
.create(&SecretCreateRequest {
organization_id,
key: secret.key,
value: secret.value,
note: secret.note.unwrap_or_default(),
project_ids: Some(vec![secret.project_id]),
})
.await?;
serialize_response(secret, output_settings);

Ok(())
}

pub(crate) async fn edit(
client: Client,
organization_id: Uuid,
secret: SecretEditCommandModel,
output_settings: OutputSettings,
) -> Result<()> {
let old_secret = client
.secrets()
.get(&SecretGetRequest { id: secret.id })
.await?;

let new_secret = client
.secrets()
.update(&SecretPutRequest {
id: secret.id,
organization_id,
key: secret.key.unwrap_or(old_secret.key),
value: secret.value.unwrap_or(old_secret.value),
note: secret.note.unwrap_or(old_secret.note),
project_ids: match secret.project_id {
Some(id) => Some(vec![id]),
None => match old_secret.project_id {
Some(id) => Some(vec![id]),
None => bail!("Editing a secret requires a project_id."),
},
},
})
.await?;
serialize_response(new_secret, output_settings);

Ok(())
}

pub(crate) async fn delete(client: Client, secret_ids: Vec<Uuid>) -> Result<()> {
let count = secret_ids.len();

let result = client
.secrets()
.delete(SecretsDeleteRequest { ids: secret_ids })
.await?;

let secrets_failed: Vec<(Uuid, String)> = result
.data
.into_iter()
.filter_map(|r| r.error.map(|e| (r.id, e)))
.collect();
let deleted_secrets = count - secrets_failed.len();

match deleted_secrets {
2.. => println!("{} secrets deleted successfully.", deleted_secrets),
1 => println!("{} secret deleted successfully.", deleted_secrets),
_ => (),
}

match secrets_failed.len() {
2.. => eprintln!("{} secrets had errors:", secrets_failed.len()),
1 => eprintln!("{} secret had an error:", secrets_failed.len()),
_ => (),
}

for secret in &secrets_failed {
eprintln!("{}: {}", secret.0, secret.1);
}

if !secrets_failed.is_empty() {
bail!("Errors when attempting to delete secrets.");
}

Ok(())
}
4 changes: 2 additions & 2 deletions crates/bws/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ impl Profile {
return Ok(format!("{base}/api"));
}

bail!("Profile has no `server_base` or `server_api`")
bail!("Profile has no `server_base` or `server_api`");
}

pub(crate) fn identity_url(&self) -> Result<String> {
Expand All @@ -141,7 +141,7 @@ impl Profile {
return Ok(format!("{base}/identity"));
}

bail!("Profile has no `server_base` or `server_identity`")
bail!("Profile has no `server_base` or `server_identity`");
}
}

Expand Down
Loading

0 comments on commit 4b1d6a1

Please sign in to comment.