Skip to main content

bwac/
main.rs

1//! Entry point for the `bwac` binary.
2//!
3//! Exit codes: 0 clean shutdown, 1 invalid configuration, token or arguments, 2 credential refused,
4//! 3 not eligible, 4 startup authentication failed transiently, 5 another process runs this token.
5
6use bitwarden_access_connector::{
7    cli::{Cli, Command},
8    config::Config,
9    executor::RunExit,
10    instance_lock::{self, InstanceLockError},
11};
12use bitwarden_threading::cancellation_token::CancellationToken;
13use clap::Parser;
14use tracing_subscriber::{
15    EnvFilter, prelude::__tracing_subscriber_SubscriberExt as _, util::SubscriberInitExt as _,
16};
17
18#[tokio::main(flavor = "current_thread")]
19async fn main() {
20    // Unset or invalid `RUST_LOG` falls back to INFO.
21    let filter = EnvFilter::builder()
22        .with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into())
23        .from_env_lossy();
24
25    tracing_subscriber::registry()
26        .with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr))
27        .with(filter)
28        .init();
29
30    // Clap exits 2 on a usage error, which would read as credential refused.
31    let cli = match Cli::try_parse() {
32        Ok(cli) => cli,
33        Err(e) => {
34            // Help and version go to stdout, errors to stderr.
35            let _ = e.print();
36            std::process::exit(if e.use_stderr() { 1 } else { 0 });
37        }
38    };
39    let Command::Run(run_args) = cli.command;
40
41    let connector_cfg = match Config::from_cli(run_args) {
42        Ok(cfg) => cfg.into_access_connector_config(),
43        Err(e) => {
44            tracing::error!("startup error: {e}");
45            std::process::exit(1);
46        }
47    };
48
49    // Two processes on one token would race each other for every job. Not a security boundary, so
50    // a failed check only warns.
51    let _instance_lock = match instance_lock::acquire(connector_cfg.api_key_id()) {
52        Ok(lock) => Some(lock),
53        Err(InstanceLockError::AlreadyRunning) => {
54            tracing::error!(
55                "Another bwac process is already running with this access connector token. Stop \
56                 it before starting this one."
57            );
58            std::process::exit(5);
59        }
60        Err(e) => {
61            tracing::warn!("{e}; continuing without the single-instance check");
62            None
63        }
64    };
65
66    let cancel = CancellationToken::new();
67
68    let watcher_cancel = cancel.clone();
69    tokio::spawn(async move {
70        wait_for_shutdown_signal().await;
71        tracing::info!("shutdown signal received; cancelling");
72        watcher_cancel.cancel();
73    });
74
75    let exit = bitwarden_access_connector::run(connector_cfg, cancel).await;
76
77    match exit {
78        RunExit::Shutdown => {
79            tracing::info!("access connector shut down cleanly");
80            std::process::exit(0);
81        }
82        RunExit::CredentialRefused => {
83            tracing::error!(
84                "The server rejected the access connector token. Check BWAC_TOKEN is current; \
85                 if so, ask an admin for a new token and restart."
86            );
87            std::process::exit(2);
88        }
89        RunExit::StartupFailed => {
90            tracing::error!(
91                "Access connector could not authenticate at startup. Check network connectivity \
92                 and the identity URL; restarting may succeed."
93            );
94            std::process::exit(4);
95        }
96        RunExit::NotEligible => {
97            tracing::error!(
98                "Access connector not eligible for rotation endpoints. Check: connector record not \
99                 revoked or disabled, organisation license active, PAM included in the \
100                 organisation plan."
101            );
102            std::process::exit(3);
103        }
104    }
105}
106
107/// Wait for Ctrl-C or, on Unix, SIGTERM.
108async fn wait_for_shutdown_signal() {
109    #[cfg(unix)]
110    {
111        use tokio::signal::unix::{SignalKind, signal};
112
113        let mut sigterm = match signal(SignalKind::terminate()) {
114            Ok(s) => s,
115            Err(e) => {
116                tracing::warn!("failed to install SIGTERM handler: {e}");
117                // Fall back to Ctrl-C only.
118                tokio::signal::ctrl_c()
119                    .await
120                    .unwrap_or_else(|e| tracing::warn!("ctrl_c error: {e}"));
121                return;
122            }
123        };
124
125        tokio::select! {
126            _ = sigterm.recv() => {
127                tracing::info!("received SIGTERM");
128            }
129            result = tokio::signal::ctrl_c() => {
130                if let Err(e) = result {
131                    tracing::warn!("ctrl_c error: {e}");
132                } else {
133                    tracing::info!("received Ctrl-C");
134                }
135            }
136        }
137    }
138
139    #[cfg(not(unix))]
140    {
141        if let Err(e) = tokio::signal::ctrl_c().await {
142            tracing::warn!("ctrl_c error: {e}");
143        } else {
144            tracing::info!("received Ctrl-C");
145        }
146    }
147}