| 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 | } |