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 base64::{Engine, engine::general_purpose::STANDARD};
9use bitwarden_cli::install_color_eyre;
10use clap::{CommandFactory, Parser};
11use clap_complete::Shell;
12use color_eyre::eyre::Result;
13use tracing_subscriber::{
14    EnvFilter, prelude::__tracing_subscriber_SubscriberExt as _, util::SubscriberInitExt as _,
15};
16
17use crate::{command::*, render::CommandResult};
18
19mod admin_console;
20mod auth;
21mod command;
22mod key_management;
23mod platform;
24mod render;
25mod tools;
26mod vault;
27
28#[tokio::main(flavor = "current_thread")]
29async fn main() -> Result<()> {
30    // the log level hierarchy is determined by:
31    //    - if RUST_LOG is detected at runtime
32    //    - if RUST_LOG is provided at compile time
33    //    - default to INFO
34    let filter = EnvFilter::builder()
35        .with_default_directive(
36            option_env!("RUST_LOG")
37                .unwrap_or("info")
38                .parse()
39                .expect("should provide valid log level at compile time."),
40        )
41        // parse directives from the RUST_LOG environment variable,
42        // overriding the default directive for matching targets.
43        .from_env_lossy();
44
45    tracing_subscriber::registry()
46        .with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr))
47        .with(filter)
48        .init();
49
50    let cli = Cli::parse();
51    install_color_eyre(cli.color)?;
52    let render_config = render::RenderConfig::new(&cli);
53
54    let Some(command) = cli.command else {
55        let mut cmd = Cli::command();
56        cmd.print_help()?;
57        return Ok(());
58    };
59
60    let result = process_commands(command, cli.session).await;
61
62    // Render the result of the command
63    render_config.render_result(result)
64}
65
66async fn process_commands(command: Commands, _session: Option<String>) -> CommandResult {
67    // Try to initialize the client with the session if provided
68    // Ideally we'd have separate clients and this would be an enum, something like:
69    // enum CliClient {
70    //   Unlocked(_),  // If the user already logged in and the provided session is valid
71    //   Locked(_),    // If the user is logged in, but the session hasn't been provided
72    //   LoggedOut(_), // If the user is not logged in
73    // }
74    // If the session was invalid, we'd just return an error immediately
75    // This would allow each command to match on the client type that they need, and we don't need
76    // to do two matches over the whole command tree
77    let client = bitwarden_pm::PasswordManagerClient::new(None);
78
79    // Temporary until rehydration
80    if let (Ok(email), Ok(password)) = (std::env::var("BW_EMAIL"), std::env::var("BW_PASSWORD")) {
81        temp_login(&client.0, email, password).await?;
82    }
83
84    match command {
85        // Auth commands
86        Commands::Login(args) => args.run().await,
87        Commands::Logout => todo!(),
88
89        // KM commands
90        Commands::Lock => todo!(),
91        Commands::Unlock(_args) => todo!(),
92
93        // Platform commands
94        Commands::Sync(args) => args.execute_sync(client).await,
95
96        Commands::Encode => {
97            let input = std::io::read_to_string(std::io::stdin())?;
98            let encoded = STANDARD.encode(input);
99            Ok(encoded.into())
100        }
101
102        Commands::Config { command } => command.run().await,
103
104        Commands::Update { .. } => todo!(),
105
106        Commands::Completion { shell } => {
107            let Some(shell) = shell.or_else(Shell::from_env) else {
108                return Ok(
109                    "Couldn't autodetect a valid shell. Run `bw completion --help` for more info."
110                        .into(),
111                );
112            };
113
114            let mut cmd = Cli::command();
115            let name = cmd.get_name().to_string();
116            clap_complete::generate(shell, &mut cmd, name, &mut std::io::stdout());
117            Ok(().into())
118        }
119
120        Commands::Status => todo!(),
121
122        // Vault commands
123        Commands::List(_args) => todo!(),
124        Commands::Get { command } => match command {
125            GetCommands::Template { command } => command.run(),
126            _ => todo!("Get command implementation with {:?}", command),
127        },
128        Commands::Create { .. } => todo!(),
129        Commands::Edit(_args) => todo!(),
130        Commands::Delete { .. } => todo!(),
131        Commands::Restore(_args) => todo!(),
132
133        // Admin console commands
134        Commands::Confirm { .. } => todo!(),
135        Commands::DeviceApproval => todo!(),
136        Commands::Move(_args) => todo!(),
137
138        // Tools commands
139        Commands::Generate(arg) => arg.run(&client),
140        Commands::Import(_args) => todo!(),
141        Commands::Export(_args) => todo!(),
142        Commands::Send(_args) => todo!(),
143        Commands::Receive(_args) => todo!(),
144
145        // Server commands
146        Commands::Serve(_args) => todo!(),
147    }
148}
149
150// Stop-gap solution for login until we have a proper session management solution in place. This
151// allows us to test the commands that require authentication without having to implement
152// rehydration.
153async fn temp_login(
154    client: &bitwarden_core::Client,
155    email: String,
156    password: String,
157) -> color_eyre::eyre::Result<()> {
158    use bitwarden_core::auth::login::PasswordLoginRequest;
159
160    let result = client
161        .auth()
162        .login_password(&PasswordLoginRequest {
163            email,
164            password,
165            two_factor: None,
166        })
167        .await?;
168
169    tracing::info!("Login result: {:?}", result);
170
171    Ok(())
172}