Skip to content

Commit 12fdb4d

Browse files
authored
[PM-22690] Create an initial CLI skeleton (#349)
## 🎟️ Tracking https://bitwarden.atlassian.net/browse/PM-22690 ## 📔 Objective Create an initial CLI skeleton, trying to maintain compatibility with the node CLI - Moved the main command struct to a separate file as it's getting big now - Created team based files for the command code - Created some utilities for formatting the output - The command structure mostly matches the Node CLI now, with some differences around Login - Implemented some basic commands like encode and generate, which match the Node CLI, the rest are unimplemented ## ⏰ 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
1 parent 824c1cf commit 12fdb4d

File tree

10 files changed

+1173
-217
lines changed

10 files changed

+1173
-217
lines changed

Cargo.lock

Lines changed: 458 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/bw/Cargo.toml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,25 @@ repository.workspace = true
1515
license-file.workspace = true
1616

1717
[dependencies]
18+
base64 = ">=0.22.1, <0.23"
19+
bat = { version = "0.25.0", features = [
20+
"regex-fancy",
21+
], default-features = false }
1822
bitwarden-cli = { workspace = true }
1923
bitwarden-core = { workspace = true }
2024
bitwarden-generators = { workspace = true }
25+
bitwarden-pm = { workspace = true }
2126
bitwarden-vault = { workspace = true }
2227
clap = { version = "4.5.4", features = ["derive", "env"] }
28+
clap_complete = "4.5.55"
2329
color-eyre = "0.6.3"
2430
env_logger = "0.11.1"
25-
inquire = "0.7.0"
31+
erased-serde = "0.4.6"
32+
inquire = "0.9.1"
2633
log = "0.4.20"
34+
serde = { workspace = true }
35+
serde_json = { workspace = true }
36+
serde_yaml = "0.9.33"
2737
tokio = { workspace = true, features = ["rt-multi-thread"] }
2838

2939
[lints]

crates/bw/src/admin_console/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
use clap::Subcommand;
2+
3+
#[derive(Subcommand, Clone)]
4+
pub enum ConfirmCommand {
5+
OrgMember {
6+
#[arg(long, help = "Organization id for an organization object.")]
7+
organizationid: String,
8+
},
9+
}

crates/bw/src/auth/mod.rs

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,94 @@
1+
use bitwarden_cli::text_prompt_when_none;
2+
use bitwarden_core::ClientSettings;
3+
use clap::{Args, Subcommand};
4+
15
mod login;
2-
pub(crate) use login::{login_api_key, login_device, login_password};
6+
use inquire::Password;
7+
8+
use crate::render::CommandResult;
9+
10+
// TODO(CLI): This is incompatible with the current node CLI
11+
#[derive(Args, Clone)]
12+
pub struct LoginArgs {
13+
#[command(subcommand)]
14+
pub command: LoginCommands,
15+
16+
#[arg(short = 's', long, global = true, help = "Server URL")]
17+
pub server: Option<String>,
18+
}
19+
20+
#[derive(Subcommand, Clone)]
21+
pub enum LoginCommands {
22+
Password {
23+
#[arg(short = 'e', long, help = "Email address")]
24+
email: Option<String>,
25+
},
26+
ApiKey {
27+
client_id: Option<String>,
28+
client_secret: Option<String>,
29+
},
30+
Device {
31+
#[arg(short = 'e', long, help = "Email address")]
32+
email: Option<String>,
33+
device_identifier: Option<String>,
34+
},
35+
}
36+
37+
impl LoginArgs {
38+
pub async fn run(self) -> CommandResult {
39+
let settings = self.server.map(|server| ClientSettings {
40+
api_url: format!("{server}/api"),
41+
identity_url: format!("{server}/identity"),
42+
..Default::default()
43+
});
44+
let client = bitwarden_core::Client::new(settings);
45+
46+
match self.command {
47+
// FIXME: Rust CLI will not support password login!
48+
LoginCommands::Password { email } => {
49+
login::login_password(client, email).await?;
50+
}
51+
LoginCommands::ApiKey {
52+
client_id,
53+
client_secret,
54+
} => login::login_api_key(client, client_id, client_secret).await?,
55+
LoginCommands::Device {
56+
email,
57+
device_identifier,
58+
} => {
59+
login::login_device(client, email, device_identifier).await?;
60+
}
61+
}
62+
Ok("Successfully logged in!".into())
63+
}
64+
}
65+
66+
#[derive(Args, Clone)]
67+
pub struct RegisterArgs {
68+
#[arg(short = 'e', long, help = "Email address")]
69+
email: Option<String>,
70+
71+
name: Option<String>,
72+
73+
password_hint: Option<String>,
74+
75+
#[arg(short = 's', long, global = true, help = "Server URL")]
76+
server: Option<String>,
77+
}
78+
79+
impl RegisterArgs {
80+
#[allow(unused_variables, clippy::unused_async)]
81+
pub async fn run(self) -> CommandResult {
82+
let settings = self.server.map(|server| ClientSettings {
83+
api_url: format!("{server}/api"),
84+
identity_url: format!("{server}/identity"),
85+
..Default::default()
86+
});
87+
let client = bitwarden_core::Client::new(settings);
88+
89+
let email = text_prompt_when_none("Email", self.email)?;
90+
let password = Password::new("Password").prompt()?;
91+
92+
unimplemented!("Registration is not yet implemented");
93+
}
94+
}

crates/bw/src/command.rs

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
use bitwarden_cli::Color;
2+
use clap::{Args, Parser, Subcommand};
3+
4+
use crate::{
5+
admin_console::ConfirmCommand,
6+
auth::{LoginArgs, RegisterArgs},
7+
platform::ConfigCommand,
8+
render::Output,
9+
tools::GenerateArgs,
10+
vault::{ItemCommands, TemplateCommands},
11+
};
12+
13+
pub const SESSION_ENV: &str = "BW_SESSION";
14+
15+
#[derive(Parser, Clone)]
16+
#[command(name = "Bitwarden CLI", version, about = "Bitwarden CLI", long_about = None, disable_version_flag = true)]
17+
pub struct Cli {
18+
// Optional as a workaround for https://github.com/clap-rs/clap/issues/3572
19+
#[command(subcommand)]
20+
pub command: Option<Commands>,
21+
22+
#[arg(short = 'o', long, global = true, value_enum, default_value_t = Output::JSON)]
23+
pub output: Output,
24+
25+
#[arg(short = 'c', long, global = true, value_enum, default_value_t = Color::Auto)]
26+
pub color: Color,
27+
28+
// TODO(CLI): Pretty/raw/response options
29+
#[arg(
30+
long,
31+
global = true,
32+
env = SESSION_ENV,
33+
help = "The session key used to decrypt your vault data. Can be obtained with `bw login` or `bw unlock`."
34+
)]
35+
pub session: Option<String>,
36+
37+
#[arg(
38+
long,
39+
global = true,
40+
help = "Exit with a success exit code (0) unless an error is thrown."
41+
)]
42+
pub cleanexit: bool,
43+
44+
#[arg(
45+
short = 'q',
46+
long,
47+
global = true,
48+
help = "Don't return anything to stdout."
49+
)]
50+
pub quiet: bool,
51+
52+
#[arg(
53+
long,
54+
global = true,
55+
help = "Do not prompt for interactive user input."
56+
)]
57+
pub nointeraction: bool,
58+
59+
// Clap uses uppercase V for the short flag by default, but we want lowercase v
60+
// for compatibility with the node CLI:
61+
// https://github.com/clap-rs/clap/issues/138
62+
#[arg(short = 'v', long, action = clap::builder::ArgAction::Version)]
63+
pub version: (),
64+
}
65+
66+
#[derive(Subcommand, Clone)]
67+
pub enum Commands {
68+
// Auth commands
69+
#[command(long_about = "Log into a user account.")]
70+
Login(LoginArgs),
71+
72+
#[command(long_about = "Log out of the current user account.")]
73+
Logout,
74+
75+
#[command(long_about = "Register a new user account.")]
76+
Register(RegisterArgs),
77+
78+
// KM commands
79+
#[command(long_about = "Unlock the vault and return a session key.")]
80+
Unlock(UnlockArgs),
81+
82+
// Platform commands
83+
#[command(long_about = "Pull the latest vault data from server.")]
84+
Sync {
85+
#[arg(short = 'f', long, help = "Force a full sync.")]
86+
force: bool,
87+
88+
#[arg(long, help = "Get the last sync date.")]
89+
last: bool,
90+
},
91+
92+
#[command(long_about = "Base 64 encode stdin.")]
93+
Encode,
94+
95+
#[command(long_about = "Configure CLI settings.")]
96+
Config {
97+
#[command(subcommand)]
98+
command: ConfigCommand,
99+
},
100+
101+
#[command(long_about = "Check for updates.")]
102+
Update {
103+
#[arg(long, help = "Return only the download URL for the update.")]
104+
raw: bool,
105+
},
106+
107+
#[command(long_about = "Generate shell completions.")]
108+
Completion {
109+
#[arg(long, help = "The shell to generate completions for.")]
110+
shell: Option<clap_complete::Shell>,
111+
},
112+
113+
#[command(
114+
long_about = "Show server, last sync, user information, and vault status.",
115+
after_help = r#"Example return value:
116+
{
117+
"serverUrl": "https://bitwarden.example.com",
118+
"lastSync": "2020-06-16T06:33:51.419Z",
119+
"userEmail": "[email protected]",
120+
"userId": "00000000-0000-0000-0000-000000000000",
121+
"status": "locked"
122+
}
123+
124+
Notes:
125+
`status` is one of:
126+
- `unauthenticated` when you are not logged in
127+
- `locked` when you are logged in and the vault is locked
128+
- `unlocked` when you are logged in and the vault is unlocked
129+
"#
130+
)]
131+
Status,
132+
133+
// Vault commands
134+
#[command(long_about = "Manage vault objects.")]
135+
Item {
136+
#[command(subcommand)]
137+
command: ItemCommands,
138+
},
139+
#[command(long_about = "Get the available templates")]
140+
Template {
141+
#[command(subcommand)]
142+
command: TemplateCommands,
143+
},
144+
145+
// These are the old style action-name commands, to be replaced by name-action commands in the
146+
// future
147+
#[command(long_about = "List an array of objects from the vault.")]
148+
List,
149+
#[command(long_about = "Get an object from the vault.")]
150+
Get,
151+
#[command(long_about = "Create an object in the vault.")]
152+
Create,
153+
#[command(long_about = "Edit an object from the vault.")]
154+
Edit,
155+
#[command(long_about = "Delete an object from the vault.")]
156+
Delete,
157+
#[command(long_about = "Restores an object from the trash.")]
158+
Restore,
159+
#[command(long_about = "Move an item to an organization.")]
160+
Move,
161+
162+
// Admin console commands
163+
#[command(long_about = "Confirm an object to the organization.")]
164+
Confirm {
165+
#[command(subcommand)]
166+
command: ConfirmCommand,
167+
},
168+
169+
// Tools commands
170+
#[command(long_about = "Generate a password/passphrase.")]
171+
#[command(after_help = r#"Notes:
172+
Default options are `-uln --length 14`.
173+
Minimum `length` is 5.
174+
Minimum `words` is 3.
175+
176+
Examples:
177+
bw generate
178+
bw generate -u -l --length 18
179+
bw generate -ulns --length 25
180+
bw generate -ul
181+
bw generate -p --separator _
182+
bw generate -p --words 5 --separator space
183+
bw generate -p --words 5 --separator empty
184+
"#)]
185+
Generate(GenerateArgs),
186+
#[command(long_about = "Import vault data from a file.")]
187+
Import,
188+
#[command(long_about = "Export vault data to a CSV, JSON or ZIP file.")]
189+
Export,
190+
#[command(long_about = "--DEPRECATED-- Move an item to an organization.")]
191+
Share,
192+
#[command(
193+
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."
194+
)]
195+
Send,
196+
#[command(long_about = "Access a Bitwarden Send from a url.")]
197+
Receive,
198+
}
199+
200+
#[derive(Args, Clone)]
201+
pub struct UnlockArgs {
202+
pub password: Option<String>,
203+
204+
#[arg(long, help = "Environment variable storing your password.")]
205+
pub passwordenv: Option<String>,
206+
207+
#[arg(
208+
long,
209+
help = "Path to a file containing your password as its first line."
210+
)]
211+
pub passwordfile: Option<String>,
212+
213+
#[arg(long, help = "Only return the session key.")]
214+
pub raw: bool,
215+
}

0 commit comments

Comments
 (0)