main
rs 107 lines 3.46 KB
Raw
1 //! Where `si` keeps its credentials.
2 //!
3 //! One file per environment under `$HOME`: `~/.sigit/token` for production,
4 //! `~/.sigit-dev/token` for a local server. `SIGIT_TOKEN` overrides both, which
5 //! is how CI authenticates without a login step.
6
7 use {
8 anyhow::{anyhow, Context, Result},
9 sigit_si_api::Environment,
10 std::{
11 fs,
12 path::{Path, PathBuf},
13 },
14 };
15
16 /// Environment variable holding a token, checked before the token file.
17 pub const TOKEN_ENV: &str = "SIGIT_TOKEN";
18
19 /// `~/.sigit` (or `~/.sigit-dev`), created if absent.
20 pub fn config_dir(environment: Environment) -> Result<PathBuf> {
21 let home = dirs::home_dir().ok_or_else(|| anyhow!("Could not locate your home directory."))?;
22 let dir = home.join(environment.config_dir_name());
23 fs::create_dir_all(&dir).with_context(|| format!("Could not create {}.", dir.display()))?;
24 Ok(dir)
25 }
26
27 pub fn token_path(environment: Environment) -> Result<PathBuf> {
28 Ok(config_dir(environment)?.join("token"))
29 }
30
31 /// The token for this environment, from `SIGIT_TOKEN` or the token file.
32 ///
33 /// Returns `None` rather than an error when there is simply no token — "not
34 /// signed in" is an expected state that callers phrase themselves.
35 pub fn read_token(environment: Environment) -> Option<String> {
36 if let Ok(token) = std::env::var(TOKEN_ENV) {
37 let token = token.trim().to_string();
38 if !token.is_empty() {
39 return Some(token);
40 }
41 }
42
43 let path = token_path(environment).ok()?;
44 let token = fs::read_to_string(path).ok()?.trim().to_string();
45 (!token.is_empty()).then_some(token)
46 }
47
48 /// Persist a token, readable only by its owner.
49 pub fn write_token(environment: Environment, token: &str) -> Result<PathBuf> {
50 let path = token_path(environment)?;
51 fs::write(&path, token.trim())
52 .with_context(|| format!("Could not write {}.", path.display()))?;
53 restrict_permissions(&path)?;
54 Ok(path)
55 }
56
57 /// Remove the stored token. Succeeds when there was nothing to remove — sign
58 /// out should be idempotent, including after a token has already expired.
59 pub fn clear_token(environment: Environment) -> Result<()> {
60 let path = token_path(environment)?;
61 match fs::remove_file(&path) {
62 Ok(()) => Ok(()),
63 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
64 Err(e) => Err(e).with_context(|| format!("Could not remove {}.", path.display())),
65 }
66 }
67
68 #[cfg(unix)]
69 fn restrict_permissions(path: &Path) -> Result<()> {
70 use std::os::unix::fs::PermissionsExt;
71
72 fs::set_permissions(path, fs::Permissions::from_mode(0o600))
73 .with_context(|| format!("Could not set permissions on {}.", path.display()))
74 }
75
76 #[cfg(not(unix))]
77 fn restrict_permissions(_path: &Path) -> Result<()> {
78 // Windows inherits the user-profile ACL, which is already owner-only.
79 Ok(())
80 }
81
82 #[cfg(test)]
83 mod tests {
84 use super::*;
85
86 #[test]
87 fn each_environment_gets_its_own_directory() {
88 assert_ne!(
89 Environment::Dev.config_dir_name(),
90 Environment::Production.config_dir_name()
91 );
92 }
93
94 #[cfg(unix)]
95 #[test]
96 fn a_written_token_is_owner_only() {
97 use std::os::unix::fs::PermissionsExt;
98
99 let dir = tempfile::tempdir().unwrap();
100 let path = dir.path().join("token");
101 fs::write(&path, "secret").unwrap();
102 restrict_permissions(&path).unwrap();
103
104 let mode = fs::metadata(&path).unwrap().permissions().mode();
105 assert_eq!(mode & 0o777, 0o600);
106 }
107 }