Skip to content

[PM-22690] Create an initial CLI skeleton #349

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

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
383 changes: 377 additions & 6 deletions Cargo.lock

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions crates/bw/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,24 @@ repository.workspace = true
license-file.workspace = true

[dependencies]
base64 = ">=0.22.1, <0.23"
bat = { version = "0.25.0", features = [
"regex-fancy",
], default-features = false }
bitwarden-cli = { workspace = true }
bitwarden-core = { workspace = true }
bitwarden-generators = { workspace = true }
bitwarden-vault = { workspace = true }
clap = { version = "4.5.4", features = ["derive", "env"] }
clap_complete = "4.5.55"
color-eyre = "0.6.3"
env_logger = "0.11.1"
erased-serde = "0.4.6"
inquire = "0.7.0"
log = "0.4.20"
serde = { workspace = true }
serde_json = { workspace = true }
serde_yaml = "0.9.33"
tokio = { workspace = true, features = ["rt-multi-thread"] }

[lints]
Expand Down
9 changes: 9 additions & 0 deletions crates/bw/src/admin_console/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use clap::Subcommand;

#[derive(Subcommand, Clone)]
pub enum ConfirmCommand {
OrgMember {
#[arg(long, help = "Organization id for an organization object.")]
organizationid: String,

Check warning on line 7 in crates/bw/src/admin_console/mod.rs

View check run for this annotation

Codecov / codecov/patch

crates/bw/src/admin_console/mod.rs#L7

Added line #L7 was not covered by tests
},
}
103 changes: 102 additions & 1 deletion crates/bw/src/auth/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,103 @@
use bitwarden_cli::text_prompt_when_none;
use bitwarden_core::{auth::RegisterRequest, ClientSettings};
use clap::{Args, Subcommand};

mod login;
pub(crate) use login::{login_api_key, login_device, login_password};
use inquire::Password;

use crate::render::CommandResult;

// TODO(CLI): This is incompatible with the current node CLI
#[derive(Args, Clone)]
pub struct LoginArgs {
#[command(subcommand)]
pub command: LoginCommands,

#[arg(short = 's', long, global = true, help = "Server URL")]
pub server: Option<String>,
}

#[derive(Subcommand, Clone)]
pub enum LoginCommands {
Password {
#[arg(short = 'e', long, help = "Email address")]
email: Option<String>,
},
ApiKey {
client_id: Option<String>,
client_secret: Option<String>,
},
Device {
#[arg(short = 'e', long, help = "Email address")]
email: Option<String>,
device_identifier: Option<String>,
},
}

impl LoginArgs {
pub async fn run(self) -> CommandResult {
let settings = self.server.map(|server| ClientSettings {
api_url: format!("{server}/api"),
identity_url: format!("{server}/identity"),
..Default::default()
});
let client = bitwarden_core::Client::new(settings);

match self.command {

Check warning on line 46 in crates/bw/src/auth/mod.rs

View check run for this annotation

Codecov / codecov/patch

crates/bw/src/auth/mod.rs#L38-L46

Added lines #L38 - L46 were not covered by tests
// FIXME: Rust CLI will not support password login!
LoginCommands::Password { email } => {
login::login_password(client, email).await?;

Check warning on line 49 in crates/bw/src/auth/mod.rs

View check run for this annotation

Codecov / codecov/patch

crates/bw/src/auth/mod.rs#L48-L49

Added lines #L48 - L49 were not covered by tests
}
LoginCommands::ApiKey {
client_id,
client_secret,
} => login::login_api_key(client, client_id, client_secret).await?,

Check warning on line 54 in crates/bw/src/auth/mod.rs

View check run for this annotation

Codecov / codecov/patch

crates/bw/src/auth/mod.rs#L52-L54

Added lines #L52 - L54 were not covered by tests
LoginCommands::Device {
email,
device_identifier,
} => {
login::login_device(client, email, device_identifier).await?;

Check warning on line 59 in crates/bw/src/auth/mod.rs

View check run for this annotation

Codecov / codecov/patch

crates/bw/src/auth/mod.rs#L56-L59

Added lines #L56 - L59 were not covered by tests
}
}
Ok("Successfully logged in!".into())
}

Check warning on line 63 in crates/bw/src/auth/mod.rs

View check run for this annotation

Codecov / codecov/patch

crates/bw/src/auth/mod.rs#L62-L63

Added lines #L62 - L63 were not covered by tests
}

#[derive(Args, Clone)]
pub struct RegisterArgs {
#[arg(short = 'e', long, help = "Email address")]
email: Option<String>,

name: Option<String>,

password_hint: Option<String>,

#[arg(short = 's', long, global = true, help = "Server URL")]
server: Option<String>,
}

impl RegisterArgs {
pub async fn run(self) -> CommandResult {
let settings = self.server.map(|server| ClientSettings {
api_url: format!("{server}/api"),
identity_url: format!("{server}/identity"),
..Default::default()
});
let client = bitwarden_core::Client::new(settings);

Check warning on line 86 in crates/bw/src/auth/mod.rs

View check run for this annotation

Codecov / codecov/patch

crates/bw/src/auth/mod.rs#L80-L86

Added lines #L80 - L86 were not covered by tests

let email = text_prompt_when_none("Email", self.email)?;
let password = Password::new("Password").prompt()?;

Check warning on line 89 in crates/bw/src/auth/mod.rs

View check run for this annotation

Codecov / codecov/patch

crates/bw/src/auth/mod.rs#L88-L89

Added lines #L88 - L89 were not covered by tests

client
.auth()
.register(&RegisterRequest {
email,
name: self.name,
password,
password_hint: self.password_hint,
})
.await?;

Check warning on line 99 in crates/bw/src/auth/mod.rs

View check run for this annotation

Codecov / codecov/patch

crates/bw/src/auth/mod.rs#L91-L99

Added lines #L91 - L99 were not covered by tests

Ok("Successfully registered!".into())
}

Check warning on line 102 in crates/bw/src/auth/mod.rs

View check run for this annotation

Codecov / codecov/patch

crates/bw/src/auth/mod.rs#L101-L102

Added lines #L101 - L102 were not covered by tests
}
204 changes: 204 additions & 0 deletions crates/bw/src/command.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
use bitwarden_cli::Color;
use clap::{Args, Parser, Subcommand};

use crate::{
admin_console::ConfirmCommand,
auth::{LoginArgs, RegisterArgs},
platform::ConfigCommand,
render::Output,
tools::GeneratorCommands,
vault::{ItemCommands, TemplateCommands},
};

pub const SESSION_ENV: &str = "BW_SESSION";

#[derive(Parser, Clone)]
#[command(name = "Bitwarden CLI", version, about = "Bitwarden CLI", long_about = None, disable_version_flag = true)]
pub struct Cli {
// Optional as a workaround for https://github.com/clap-rs/clap/issues/3572
#[command(subcommand)]
pub command: Option<Commands>,

#[arg(short = 'o', long, global = true, value_enum, default_value_t = Output::JSON)]
pub output: Output,

Check warning on line 23 in crates/bw/src/command.rs

View check run for this annotation

Codecov / codecov/patch

crates/bw/src/command.rs#L22-L23

Added lines #L22 - L23 were not covered by tests

#[arg(short = 'c', long, global = true, value_enum, default_value_t = Color::Auto)]
pub color: Color,

Check warning on line 26 in crates/bw/src/command.rs

View check run for this annotation

Codecov / codecov/patch

crates/bw/src/command.rs#L25-L26

Added lines #L25 - L26 were not covered by tests

// TODO(CLI): Pretty/raw/response options
#[arg(
long,
global = true,
env = SESSION_ENV,
help = "The session key used to decrypt your vault data. Can be obtained with `bw login` or `bw unlock`."
)]
pub session: Option<String>,

#[arg(
long,
global = true,
help = "Exit with a success exit code (0) unless an error is thrown."
)]
pub cleanexit: bool,

Check warning on line 42 in crates/bw/src/command.rs

View check run for this annotation

Codecov / codecov/patch

crates/bw/src/command.rs#L42

Added line #L42 was not covered by tests

#[arg(
short = 'q',
long,
global = true,
help = "Don't return anything to stdout."
)]
pub quiet: bool,

Check warning on line 50 in crates/bw/src/command.rs

View check run for this annotation

Codecov / codecov/patch

crates/bw/src/command.rs#L50

Added line #L50 was not covered by tests

#[arg(
long,
global = true,
help = "Do not prompt for interactive user input."
)]
pub nointeraction: bool,

Check warning on line 57 in crates/bw/src/command.rs

View check run for this annotation

Codecov / codecov/patch

crates/bw/src/command.rs#L57

Added line #L57 was not covered by tests

// Clap uses uppercase V for the short flag by default, but we want lowercase v
// for compatibility with the node CLI:
// https://github.com/clap-rs/clap/issues/138
#[arg(short = 'v', long, action = clap::builder::ArgAction::Version)]
pub version: (),
}

#[derive(Subcommand, Clone)]
pub enum Commands {
// Auth commands
#[command(long_about = "Log into a user account.")]
Login(LoginArgs),

#[command(long_about = "Log out of the current user account.")]
Logout,

#[command(long_about = "Register a new user account.")]
Register(RegisterArgs),

// KM commands
#[command(long_about = "Unlock the vault and return a session key.")]
Unlock(UnlockArgs),

// Platform commands
#[command(long_about = "Pull the latest vault data from server.")]
Sync {
#[arg(short = 'f', long, help = "Force a full sync.")]
force: bool,

Check warning on line 86 in crates/bw/src/command.rs

View check run for this annotation

Codecov / codecov/patch

crates/bw/src/command.rs#L86

Added line #L86 was not covered by tests

#[arg(long, help = "Get the last sync date.")]
last: bool,

Check warning on line 89 in crates/bw/src/command.rs

View check run for this annotation

Codecov / codecov/patch

crates/bw/src/command.rs#L89

Added line #L89 was not covered by tests
},

#[command(long_about = "Base 64 encode stdin.")]
Encode,

#[command(long_about = "Configure CLI settings.")]
Config {
#[command(subcommand)]
command: ConfigCommand,
},

#[command(long_about = "Check for updates.")]
Update {
#[arg(long, help = "Return only the download URL for the update.")]
raw: bool,

Check warning on line 104 in crates/bw/src/command.rs

View check run for this annotation

Codecov / codecov/patch

crates/bw/src/command.rs#L104

Added line #L104 was not covered by tests
},

#[command(long_about = "Generate shell completions.")]
Completion {
#[arg(long, help = "The shell to generate completions for.")]
shell: Option<clap_complete::Shell>,
},

#[command(
long_about = "Show server, last sync, user information, and vault status.",
after_help = r#"Example return value:
{
"serverUrl": "https://bitwarden.example.com",
"lastSync": "2020-06-16T06:33:51.419Z",
"userEmail": "[email protected]",
"userId": "00000000-0000-0000-0000-000000000000",
"status": "locked"
}

Notes:
`status` is one of:
- `unauthenticated` when you are not logged in
- `locked` when you are logged in and the vault is locked
- `unlocked` when you are logged in and the vault is unlocked
"#
)]
Status,

// Vault commands
#[command(long_about = "Manage vault objects.")]
Item {
#[command(subcommand)]
command: ItemCommands,
},
#[command(long_about = "Get the available templates")]
Template {
#[command(subcommand)]
command: TemplateCommands,
},

// These are the old style action-name commands, to be replaced by name-action commands in the
// future
#[command(long_about = "List an array of objects from the vault.")]
List,
#[command(long_about = "Get an object from the vault.")]
Get,
#[command(long_about = "Create an object in the vault.")]
Create,
#[command(long_about = "Edit an object from the vault.")]
Edit,
#[command(long_about = "Delete an object from the vault.")]
Delete,
#[command(long_about = "Restores an object from the trash.")]
Restore,
#[command(long_about = "Move an item to an organization.")]
Move,

// Admin console commands
#[command(long_about = "Confirm an object to the organization.")]
Confirm {
#[command(subcommand)]
command: ConfirmCommand,
},

// Tools commands
#[command(long_about = "Generate a password/passphrase.")]
Generate {
#[command(subcommand)]
command: GeneratorCommands,
},
#[command(long_about = "Import vault data from a file.")]
Import,
#[command(long_about = "Export vault data to a CSV, JSON or ZIP file.")]
Export,
#[command(long_about = "--DEPRECATED-- Move an item to an organization.")]
Share,
#[command(
long_about = "Work with Bitwarden sends. A Send can be quickly created using this command or subcommands can be used to fine-tune the Send."
)]
Send,
#[command(long_about = "Access a Bitwarden Send from a url.")]
Receive,
}

#[derive(Args, Clone)]
pub struct UnlockArgs {
pub password: Option<String>,

#[arg(long, help = "Environment variable storing your password.")]
pub passwordenv: Option<String>,

#[arg(
long,
help = "Path to a file containing your password as its first line."
)]
pub passwordfile: Option<String>,

#[arg(long, help = "Only return the session key.")]
pub raw: bool,

Check warning on line 203 in crates/bw/src/command.rs

View check run for this annotation

Codecov / codecov/patch

crates/bw/src/command.rs#L203

Added line #L203 was not covered by tests
}
Loading
Loading