Skip to main content

bw/
main.rs

1#![doc = include_str!("../README.md")]
2#![allow(
3    clippy::print_stdout,
4    clippy::print_stderr,
5    reason = "The CLI uses stdout/stderr for user interaction"
6)]
7
8use bitwarden_cli::install_color_eyre;
9use bitwarden_core::{DeviceType, GlobalClient, HostPlatformInfo, init_host_platform_info};
10use clap::{CommandFactory, Parser};
11use color_eyre::eyre::Result;
12use tracing_subscriber::{
13    EnvFilter, prelude::__tracing_subscriber_SubscriberExt as _, util::SubscriberInitExt as _,
14};
15
16use crate::{
17    client_state::{BwCommandExt, ClientContext},
18    command::*,
19    render::CommandResult,
20};
21
22mod admin_console;
23mod auth;
24mod client_state;
25mod command;
26mod dirt;
27mod key_management;
28mod platform;
29mod render;
30mod tools;
31mod vault;
32
33#[tokio::main(flavor = "current_thread")]
34async fn main() -> Result<()> {
35    // the log level hierarchy is determined by:
36    //    - if RUST_LOG is detected at runtime
37    //    - if RUST_LOG is provided at compile time
38    //    - default to INFO
39    let filter = EnvFilter::builder()
40        .with_default_directive(
41            option_env!("RUST_LOG")
42                .unwrap_or("info")
43                .parse()
44                .expect("should provide valid log level at compile time."),
45        )
46        // parse directives from the RUST_LOG environment variable,
47        // overriding the default directive for matching targets.
48        .from_env_lossy();
49
50    tracing_subscriber::registry()
51        .with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr))
52        .with(filter)
53        .init();
54
55    init_cli_platform_info();
56
57    let cli = Cli::parse();
58    install_color_eyre(cli.color)?;
59    let render_config = render::RenderConfig::new(&cli);
60
61    let Some(command) = cli.command else {
62        let mut cmd = Cli::command();
63        cmd.print_help()?;
64        return Ok(());
65    };
66
67    let result = process_commands(command, cli.session).await;
68
69    // Render the result of the command
70    render_config.render_result(result)
71}
72
73async fn process_commands(command: Commands, _session: Option<String>) -> CommandResult {
74    let global = GlobalClient::new();
75
76    // Temporary until session persistence (PM-35206): if the legacy env vars are present, eagerly
77    // construct + log in a `PasswordManagerClient` so commands that need a logged-in user can run.
78    let user = if let (Ok(email), Ok(password)) =
79        (std::env::var("BW_EMAIL"), std::env::var("BW_PASSWORD"))
80    {
81        let client = bitwarden_pm::PasswordManagerClient::new(None);
82        temp_login(&client.0, email, password).await?;
83        Some(client)
84    } else {
85        None
86    };
87
88    let ctx = ClientContext { global, user };
89
90    match command {
91        // Auth commands
92        Commands::Login(args) => args.run().await,
93        Commands::Logout => todo!(),
94
95        // KM commands
96        Commands::Lock(args) => args.dispatch(ctx).await,
97        Commands::Unlock(_args) => todo!(),
98
99        // Platform commands
100        Commands::Sync(args) => args.dispatch(ctx).await,
101        Commands::Encode(args) => args.dispatch(ctx).await,
102        Commands::Config { command } => command.dispatch(ctx).await,
103        Commands::Completion(args) => args.dispatch(ctx).await,
104
105        Commands::Update { .. } => todo!(),
106
107        Commands::Status(_) => todo!(),
108
109        // Vault commands
110        Commands::List { .. } => todo!(),
111        Commands::Get { command } => command.run(),
112        Commands::Create { command } => command.run(),
113        Commands::Edit { .. } => todo!(),
114        Commands::Delete { .. } => todo!(),
115        Commands::Restore(_args) => todo!(),
116
117        // Admin console commands
118        Commands::Confirm { .. } => todo!(),
119        Commands::DeviceApproval => todo!(),
120        Commands::Move(_args) => todo!(),
121
122        // Tools commands
123        Commands::Generate(args) => {
124            let client = ctx
125                .user
126                .unwrap_or_else(|| bitwarden_pm::PasswordManagerClient::new(None));
127            args.run(&client)
128        }
129        Commands::Import(_args) => todo!(),
130        Commands::Export(_args) => todo!(),
131        Commands::Send(_args) => todo!(),
132        Commands::Receive(_args) => todo!(),
133
134        // Server commands
135        Commands::Serve(_args) => todo!(),
136    }
137}
138
139// Stop-gap solution for login until we have a proper session management solution in place. This
140// allows us to test the commands that require authentication without having to implement
141// rehydration.
142async fn temp_login(
143    client: &bitwarden_core::Client,
144    email: String,
145    password: String,
146) -> color_eyre::eyre::Result<()> {
147    use bitwarden_core::auth::login::PasswordLoginRequest;
148
149    let result = client
150        .auth()
151        .login_password(&PasswordLoginRequest {
152            email,
153            password,
154            two_factor: None,
155        })
156        .await?;
157
158    tracing::info!("Login result: {:?}", result);
159
160    Ok(())
161}
162
163fn init_cli_platform_info() {
164    let device_type = if cfg!(target_os = "windows") {
165        DeviceType::WindowsCLI
166    } else if cfg!(target_os = "macos") {
167        DeviceType::MacOsCLI
168    } else {
169        DeviceType::LinuxCLI
170    };
171
172    init_host_platform_info(HostPlatformInfo {
173        user_agent: format!("Bitwarden_CLI/{}", env!("CARGO_PKG_VERSION")),
174        device_type,
175        // Stable identifier comes from session persistence (PM-35206).
176        device_identifier: None,
177        bitwarden_client_version: Some(env!("CARGO_PKG_VERSION").to_string()),
178        bitwarden_package_type: Some("cli".to_string()),
179    });
180}