Skip to main content

dsp_cli/actions/
auth_state.rs

1//! Shared ADR-0007 auth-state disclosure helper, used by all commands that
2//! produce `_meta.auth`.
3
4use crate::config::{AuthCache, ResolvedToken, TokenOrigin};
5
6/// Build the ADR-0007 read-command auth-state disclosure string from token
7/// resolution. Uses ADR-0007 disclosure vocabulary ("anonymous"/"authenticated"),
8/// which is the uniform `_meta.auth` contract across all commands.
9///
10/// Cases:
11///   no token             → "anonymous"
12///   env token            → "authenticated via DSP_TOKEN"
13///   cache token + user   → "authenticated as {user}"
14///   cache token, no user → "authenticated"
15///
16/// **Expiry note (v1):** reflects token *presence/origin*, not validity — does
17/// not check `expires_at`. An expired cached token is sent and reported as
18/// "authenticated as {user}". This is a deliberate v1 simplification (the server
19/// ignores an expired token on a public endpoint anyway).
20///
21/// **Username source:** `ResolvedToken` carries only `token + origin`. The `{user}`
22/// for the cache arm is read from `cache.user(server)` — exactly as `auth status`
23/// does it. The `cache` param is needed solely for this username lookup.
24pub(crate) fn read_auth_state(
25    resolved: Option<&ResolvedToken>,
26    cache: &AuthCache,
27    server: &str,
28) -> String {
29    match resolved {
30        None => "anonymous".to_string(),
31        Some(r) if r.origin == TokenOrigin::Env => "authenticated via DSP_TOKEN".to_string(),
32        Some(_) => {
33            // Cache origin — username comes from the cache entry, not the resolved token.
34            match cache.user(server) {
35                Some(user) => format!("authenticated as {user}"),
36                None => "authenticated".to_string(),
37            }
38        }
39    }
40}
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45    use crate::config::AuthCache;
46    use crate::config::token::{ResolvedToken, TokenOrigin};
47
48    const SERVER: &str = "https://api.test.dasch.swiss";
49
50    fn cache_with_user(server: &str, token: &str, user: &str) -> AuthCache {
51        let mut cache = AuthCache::default();
52        cache.set_entry(
53            server,
54            crate::config::auth_cache::ServerEntry {
55                token: token.to_string(),
56                user: Some(user.to_string()),
57                acquired_at: None,
58                expires_at: None,
59            },
60        );
61        cache
62    }
63
64    fn cache_with_token_no_user(server: &str, token: &str) -> AuthCache {
65        let mut cache = AuthCache::default();
66        cache.set_token(server.to_string(), token.to_string());
67        cache
68    }
69
70    fn env_token() -> ResolvedToken {
71        ResolvedToken {
72            token: "env-tok".to_string(),
73            origin: TokenOrigin::Env,
74        }
75    }
76
77    fn cache_token() -> ResolvedToken {
78        ResolvedToken {
79            token: "cache-tok".to_string(),
80            origin: TokenOrigin::Cache,
81        }
82    }
83
84    /// Case 1: no token → "anonymous"
85    #[test]
86    fn no_token_is_anonymous() {
87        let cache = AuthCache::default();
88        let state = read_auth_state(None, &cache, SERVER);
89        assert_eq!(state, "anonymous");
90    }
91
92    /// Case 2: env token → "authenticated via DSP_TOKEN"
93    #[test]
94    fn env_token_disclosure() {
95        let cache = AuthCache::default();
96        let tok = env_token();
97        let state = read_auth_state(Some(&tok), &cache, SERVER);
98        assert_eq!(state, "authenticated via DSP_TOKEN");
99    }
100
101    /// Case 3: cache token + user → "authenticated as {user}"
102    #[test]
103    fn cache_token_with_user_shows_username() {
104        let cache = cache_with_user(SERVER, "cache-tok", "alice@example.com");
105        let tok = cache_token();
106        let state = read_auth_state(Some(&tok), &cache, SERVER);
107        assert_eq!(state, "authenticated as alice@example.com");
108    }
109
110    /// Case 4: cache token, no user → "authenticated"
111    #[test]
112    fn cache_token_without_user_is_authenticated() {
113        let cache = cache_with_token_no_user(SERVER, "cache-tok");
114        let tok = cache_token();
115        let state = read_auth_state(Some(&tok), &cache, SERVER);
116        assert_eq!(state, "authenticated");
117    }
118}