bw/
command.rs

1use bitwarden_cli::Color;
2use clap::{Parser, Subcommand};
3
4use crate::{
5    admin_console::{ConfirmCommand, MoveArgs},
6    auth::LoginArgs,
7    key_management::UnlockArgs,
8    platform::{ConfigCommand, SyncArgs},
9    render::Output,
10    tools::{ExportArgs, GenerateArgs, ImportArgs, ReceiveArgs, SendArgs},
11    vault::{RestoreArgs, TemplateCommands},
12};
13
14pub const SESSION_ENV: &str = "BW_SESSION";
15
16#[derive(Parser, Clone)]
17#[command(name = "Bitwarden CLI", version, about = "Bitwarden CLI", long_about = None, disable_version_flag = true)]
18pub struct Cli {
19    // Optional as a workaround for https://github.com/clap-rs/clap/issues/3572
20    #[command(subcommand)]
21    pub command: Option<Commands>,
22
23    #[arg(short = 'o', long, global = true, value_enum, default_value_t = Output::JSON)]
24    pub output: Output,
25
26    #[arg(short = 'c', long, global = true, value_enum, default_value_t = Color::Auto)]
27    pub color: Color,
28
29    // TODO(CLI): Pretty/raw/response options
30    #[arg(
31        long,
32        global = true,
33        env = SESSION_ENV,
34        help = "The session key used to decrypt your vault data. Can be obtained with `bw login` or `bw unlock`."
35    )]
36    pub session: Option<String>,
37
38    #[arg(
39        long,
40        global = true,
41        help = "Exit with a success exit code (0) unless an error is thrown."
42    )]
43    pub cleanexit: bool,
44
45    #[arg(
46        short = 'q',
47        long,
48        global = true,
49        help = "Don't return anything to stdout."
50    )]
51    pub quiet: bool,
52
53    #[arg(
54        long,
55        global = true,
56        help = "Do not prompt for interactive user input."
57    )]
58    pub nointeraction: bool,
59
60    // Clap uses uppercase V for the short flag by default, but we want lowercase v
61    // for compatibility with the node CLI:
62    // https://github.com/clap-rs/clap/issues/138
63    #[arg(short = 'v', long, action = clap::builder::ArgAction::Version)]
64    pub version: (),
65}
66
67#[derive(Subcommand, Clone)]
68pub enum Commands {
69    // Auth commands
70    #[command(long_about = "Log into a user account.")]
71    Login(LoginArgs),
72
73    #[command(long_about = "Log out of the current user account.")]
74    Logout,
75
76    #[command(long_about = "Lock the vault and destroy active session keys.")]
77    Lock,
78
79    // KM commands
80    #[command(long_about = "Unlock the vault and return a session key.")]
81    Unlock(UnlockArgs),
82
83    // Platform commands
84    #[command(long_about = "Pull the latest vault data from server.")]
85    Sync(SyncArgs),
86
87    #[command(long_about = "Base 64 encode stdin.")]
88    Encode,
89
90    #[command(long_about = "Configure CLI settings.")]
91    Config {
92        #[command(subcommand)]
93        command: ConfigCommand,
94    },
95
96    #[command(long_about = "Check for updates.")]
97    Update {
98        #[arg(long, help = "Return only the download URL for the update.")]
99        raw: bool,
100    },
101
102    #[command(long_about = "Generate shell completions.")]
103    Completion {
104        #[arg(long, help = "The shell to generate completions for.")]
105        shell: Option<clap_complete::Shell>,
106    },
107
108    #[command(
109        long_about = "Show server, last sync, user information, and vault status.",
110        after_help = r#"Example return value:
111  {
112    "serverUrl": "https://bitwarden.example.com",
113    "lastSync": "2020-06-16T06:33:51.419Z",
114    "userEmail": "[email protected]",
115    "userId": "00000000-0000-0000-0000-000000000000",
116    "status": "locked"
117  }
118
119Notes:
120  `status` is one of:
121    - `unauthenticated` when you are not logged in
122    - `locked` when you are logged in and the vault is locked
123    - `unlocked` when you are logged in and the vault is unlocked
124"#
125    )]
126    Status,
127
128    // These are the old style action-name commands, to be replaced by name-action commands in the
129    // future
130    #[command(long_about = "List an array of objects from the vault.")]
131    List(ListArgs),
132    #[command(long_about = "Get an object from the vault.")]
133    Get {
134        #[command(subcommand)]
135        command: GetCommands,
136    },
137    #[command(long_about = "Create an object in the vault.")]
138    Create {
139        #[command(subcommand)]
140        command: CreateCommands,
141    },
142    #[command(long_about = "Edit an object from the vault.")]
143    Edit(EditArgs),
144    #[command(long_about = "Delete an object from the vault.")]
145    Delete {
146        #[command(subcommand)]
147        command: DeleteCommands,
148    },
149    #[command(long_about = "Restores an object from the trash.")]
150    Restore(RestoreArgs),
151    #[command(long_about = "Move an item to an organization.")]
152    Move(MoveArgs),
153
154    // Admin console commands
155    #[command(long_about = "Confirm an object to the organization.")]
156    Confirm {
157        #[command(subcommand)]
158        command: ConfirmCommand,
159    },
160
161    // Tools commands
162    #[command(long_about = "Generate a password/passphrase.")]
163    #[command(after_help = r#"Notes:
164    Default options are `-uln --length 14`.
165    Minimum `length` is 5.
166    Minimum `words` is 3.
167
168Examples:
169    bw generate
170    bw generate -u -l --length 18
171    bw generate -ulns --length 25
172    bw generate -ul
173    bw generate -p --separator _
174    bw generate -p --words 5 --separator space
175    bw generate -p --words 5 --separator empty
176    "#)]
177    Generate(GenerateArgs),
178    #[command(long_about = "Import vault data from a file.")]
179    Import(ImportArgs),
180    #[command(long_about = "Export vault data to a CSV, JSON or ZIP file.")]
181    Export(ExportArgs),
182    #[command(
183        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."
184    )]
185    Send(SendArgs),
186    #[command(long_about = "Access a Bitwarden Send from a url.")]
187    Receive(ReceiveArgs),
188
189    // Device approval commands
190    #[command(
191        long_about = "Manage device approval requests sent to organizations that use SSO with trusted devices."
192    )]
193    DeviceApproval,
194
195    // Server commands
196    #[command(long_about = "Start a RESTful API webserver.")]
197    Serve(ServeArgs),
198}
199
200#[derive(clap::Args, Clone)]
201pub struct ServeArgs {
202    #[arg(long, help = "Port number to listen on.", default_value = "8087")]
203    pub port: u16,
204
205    #[arg(long, help = "Hostname to bind to.", default_value = "localhost")]
206    pub hostname: String,
207
208    #[arg(
209        long,
210        help = "Disable origin protection (not recommended for production use)."
211    )]
212    pub disable_origin_protection: bool,
213}
214
215#[derive(clap::Args, Clone)]
216pub struct ListArgs {
217    /// The type of object to list
218    pub object: ListObject,
219
220    #[arg(long, help = "Filter items by URL")]
221    pub url: Option<String>,
222
223    #[arg(long, help = "Filter items by folder ID")]
224    pub folderid: Option<String>,
225
226    #[arg(long, help = "Filter items by collection ID")]
227    pub collectionid: Option<String>,
228
229    #[arg(long, help = "Filter items by organization ID")]
230    pub organizationid: Option<String>,
231
232    #[arg(long, help = "Filter items in trash")]
233    pub trash: bool,
234
235    #[arg(long, help = "Search term")]
236    pub search: Option<String>,
237}
238
239#[derive(clap::Args, Clone)]
240pub struct EditArgs {
241    /// The type of object to edit
242    pub object: EditObject,
243    /// Object ID
244    pub id: String,
245    /// Base64-encoded JSON object (optional, can read from stdin)
246    pub encoded_json: Option<String>,
247
248    #[arg(long, help = "Organization ID for an organization object")]
249    pub organizationid: Option<String>,
250}
251
252#[derive(clap::ValueEnum, Clone, Debug)]
253#[value(rename_all = "kebab-case")]
254pub enum ListObject {
255    Items,
256    Folders,
257    Collections,
258    Organizations,
259    OrgCollections,
260    OrgMembers,
261}
262
263#[derive(clap::ValueEnum, Clone, Debug)]
264#[value(rename_all = "kebab-case")]
265pub enum EditObject {
266    Item,
267    ItemCollections,
268    Folder,
269    OrgCollection,
270}
271
272#[derive(Subcommand, Clone, Debug)]
273pub enum GetCommands {
274    #[command(long_about = "Get an item from the vault.")]
275    Item { id: String },
276
277    #[command(long_about = "Get the username for an item.")]
278    Username { id: String },
279
280    #[command(long_about = "Get the password for an item.")]
281    Password { id: String },
282
283    #[command(long_about = "Get the URI for an item.")]
284    Uri { id: String },
285
286    #[command(long_about = "Get the TOTP code for an item.")]
287    Totp { id: String },
288
289    #[command(long_about = "Check if an item password has been exposed in a data breach.")]
290    Exposed { id: String },
291
292    #[command(long_about = "Get the notes for an item.")]
293    Notes { id: String },
294
295    #[command(long_about = "Get a folder from the vault.")]
296    Folder { id: String },
297
298    #[command(long_about = "Get a collection from the vault.")]
299    Collection { id: String },
300
301    #[command(long_about = "Get an organization.")]
302    Organization { id: String },
303
304    #[command(long_about = "Get an organization collection.")]
305    #[command(name = "org-collection")]
306    OrgCollection { id: String },
307
308    #[command(long_about = "Get an attachment from an item.")]
309    Attachment {
310        filename: String,
311        #[arg(long, help = "Item ID that the attachment belongs to.")]
312        itemid: String,
313        #[arg(long, help = "Output file path. If not specified, outputs to stdout.")]
314        output: Option<String>,
315    },
316
317    #[command(long_about = "Get the fingerprint for the current user or a specified user.")]
318    Fingerprint {
319        #[arg(default_value = "me", help = "User ID or 'me' for current user")]
320        user: String,
321    },
322
323    #[command(long_about = "Get a JSON template for creating objects.")]
324    Template {
325        #[command(subcommand)]
326        command: TemplateCommands,
327    },
328
329    #[command(long_about = "Get a Bitwarden Send.")]
330    Send { id: String },
331}
332
333#[derive(Subcommand, Clone, Debug)]
334pub enum CreateCommands {
335    #[command(long_about = "Create an item in the vault.")]
336    Item {
337        #[arg(help = "Base64-encoded JSON item object")]
338        encoded_json: String,
339    },
340
341    #[command(long_about = "Create an attachment for an item.")]
342    Attachment {
343        #[arg(long, help = "Path to the file to attach")]
344        file: String,
345        #[arg(long, help = "Item ID to attach the file to")]
346        itemid: String,
347    },
348
349    #[command(long_about = "Create a folder.")]
350    Folder {
351        #[arg(help = "Base64-encoded JSON folder object")]
352        encoded_json: String,
353    },
354
355    #[command(long_about = "Create an organization collection.")]
356    #[command(name = "org-collection")]
357    OrgCollection {
358        #[arg(help = "Base64-encoded JSON collection object")]
359        encoded_json: String,
360
361        #[arg(long, help = "Organization ID")]
362        organizationid: Option<String>,
363    },
364}
365
366#[derive(Subcommand, Clone, Debug)]
367pub enum DeleteCommands {
368    #[command(long_about = "Delete an item from the vault.")]
369    Item {
370        id: String,
371        #[arg(short = 'p', long, help = "Permanently delete the item (skip trash)")]
372        permanent: bool,
373    },
374
375    #[command(long_about = "Delete an attachment from an item.")]
376    Attachment {
377        id: String,
378        #[arg(long, help = "Item ID that the attachment belongs to")]
379        itemid: String,
380    },
381
382    #[command(long_about = "Delete a folder.")]
383    Folder {
384        id: String,
385        #[arg(short = 'p', long, help = "Permanently delete the folder (skip trash)")]
386        permanent: bool,
387    },
388
389    #[command(long_about = "Delete an organization collection.")]
390    #[command(name = "org-collection")]
391    OrgCollection {
392        id: String,
393        #[arg(long, help = "Organization ID")]
394        organizationid: String,
395    },
396}