Skip to main content

via/
login.rs

1use std::path::Path;
2use std::process::{Command, Stdio};
3
4use crate::config::{Config, ProviderConfig};
5use crate::error::ViaError;
6
7pub fn run(config_path: Option<&Path>, provider_name: Option<&str>) -> Result<(), ViaError> {
8    let providers = login_targets(config_path, provider_name)?;
9
10    if providers.is_empty() {
11        return Err(ViaError::InvalidConfig(
12            "no login-capable providers are configured".to_owned(),
13        ));
14    }
15
16    for provider in providers {
17        login_onepassword(&provider.name, provider.account.as_deref())?;
18    }
19
20    Ok(())
21}
22
23fn login_targets(
24    config_path: Option<&Path>,
25    provider_name: Option<&str>,
26) -> Result<Vec<OnePasswordLoginTarget>, ViaError> {
27    match Config::load(config_path) {
28        Ok(config) => onepassword_login_targets(&config, provider_name),
29        Err(ViaError::ConfigNotFound(_)) if config_path.is_none() => {
30            default_onepassword_login_targets(provider_name)
31        }
32        Err(error) => Err(error),
33    }
34}
35
36#[derive(Debug)]
37struct OnePasswordLoginTarget {
38    name: String,
39    account: Option<String>,
40}
41
42fn onepassword_login_targets(
43    config: &Config,
44    provider_name: Option<&str>,
45) -> Result<Vec<OnePasswordLoginTarget>, ViaError> {
46    if let Some(provider_name) = provider_name {
47        let provider = config.providers.get(provider_name).ok_or_else(|| {
48            ViaError::InvalidConfig(format!("provider `{provider_name}` is not configured"))
49        })?;
50        return Ok(onepassword_target(provider_name, provider)
51            .into_iter()
52            .collect());
53    }
54
55    Ok(config
56        .providers
57        .iter()
58        .filter_map(|(name, provider)| onepassword_target(name, provider))
59        .collect())
60}
61
62fn default_onepassword_login_targets(
63    provider_name: Option<&str>,
64) -> Result<Vec<OnePasswordLoginTarget>, ViaError> {
65    match provider_name {
66        Some("onepassword") | None => Ok(vec![OnePasswordLoginTarget {
67            name: "onepassword".to_owned(),
68            account: None,
69        }]),
70        Some(provider_name) => Err(ViaError::InvalidConfig(format!(
71            "provider `{provider_name}` is not configured"
72        ))),
73    }
74}
75
76fn onepassword_target(name: &str, provider: &ProviderConfig) -> Option<OnePasswordLoginTarget> {
77    match provider {
78        ProviderConfig::OnePassword { account, .. } => Some(OnePasswordLoginTarget {
79            name: name.to_owned(),
80            account: account.clone(),
81        }),
82    }
83}
84
85fn login_onepassword(provider_name: &str, account: Option<&str>) -> Result<(), ViaError> {
86    println!("provider {provider_name} (1Password): signing in");
87
88    let args = onepassword_signin_args(account);
89    let status = Command::new("op")
90        .args(&args)
91        .stdin(Stdio::inherit())
92        .stdout(Stdio::inherit())
93        .stderr(Stdio::inherit())
94        .status()
95        .map_err(|source| ViaError::MissingProgram {
96            program: "op".to_owned(),
97            source,
98        })?;
99
100    if !status.success() {
101        return Err(ViaError::ExternalCommandFailed {
102            program: "op".to_owned(),
103            status: status.code(),
104            stderr: "op signin did not complete successfully".to_owned(),
105        });
106    }
107
108    println!("provider {provider_name} (1Password): authenticated");
109    Ok(())
110}
111
112fn onepassword_signin_args(account: Option<&str>) -> Vec<String> {
113    let mut args = vec!["signin".to_owned()];
114    if let Some(account) = account {
115        args.push("--account".to_owned());
116        args.push(account.to_owned());
117    }
118    args
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    const CONFIG: &str = r#"
126version = 1
127
128[providers.onepassword]
129type = "1password"
130account = "example.1password.com"
131"#;
132
133    #[test]
134    fn builds_plain_onepassword_signin_args() {
135        assert_eq!(onepassword_signin_args(None), ["signin"]);
136    }
137
138    #[test]
139    fn builds_account_scoped_onepassword_signin_args() {
140        assert_eq!(
141            onepassword_signin_args(Some("example.1password.com")),
142            ["signin", "--account", "example.1password.com"]
143        );
144    }
145
146    #[test]
147    fn selects_configured_onepassword_provider() {
148        let config = Config::from_toml_str(CONFIG).unwrap();
149        let targets = onepassword_login_targets(&config, None).unwrap();
150
151        assert_eq!(targets.len(), 1);
152        assert_eq!(targets[0].name, "onepassword");
153        assert_eq!(targets[0].account.as_deref(), Some("example.1password.com"));
154    }
155
156    #[test]
157    fn rejects_unknown_provider() {
158        let config = Config::from_toml_str(CONFIG).unwrap();
159        let error = onepassword_login_targets(&config, Some("missing")).unwrap_err();
160
161        assert!(
162            matches!(error, ViaError::InvalidConfig(message) if message.contains("provider `missing`"))
163        );
164    }
165
166    #[test]
167    fn defaults_to_onepassword_when_config_is_missing() {
168        let targets = default_onepassword_login_targets(None).unwrap();
169
170        assert_eq!(targets.len(), 1);
171        assert_eq!(targets[0].name, "onepassword");
172        assert_eq!(targets[0].account, None);
173    }
174
175    #[test]
176    fn rejects_unknown_provider_when_config_is_missing() {
177        let error = default_onepassword_login_targets(Some("missing")).unwrap_err();
178
179        assert!(
180            matches!(error, ViaError::InvalidConfig(message) if message.contains("provider `missing`"))
181        );
182    }
183}