Skip to main content

dsp_cli/actions/
auth_state.rs

1//! Shared dsp-cli/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 dsp-cli/ADR-0007 read-command auth-state disclosure string from token
7/// resolution. Uses dsp-cli/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(resolved: Option<&ResolvedToken>, cache: &AuthCache, server: &str) -> String {
25    match resolved {
26        None => "anonymous".to_string(),
27        Some(r) if r.origin == TokenOrigin::Env => "authenticated via DSP_TOKEN".to_string(),
28        Some(_) => {
29            // Cache origin — username comes from the cache entry, not the resolved token.
30            match cache.user(server) {
31                Some(user) => format!("authenticated as {user}"),
32                None => "authenticated".to_string(),
33            }
34        }
35    }
36}
37
38#[cfg(test)]
39mod tests {
40    use super::*;
41    use crate::config::AuthCache;
42    use crate::config::token::{ResolvedToken, TokenOrigin};
43
44    const SERVER: &str = "https://api.test.dasch.swiss";
45
46    fn cache_with_user(server: &str, token: &str, user: &str) -> AuthCache {
47        let mut cache = AuthCache::default();
48        cache.set_entry(
49            server,
50            crate::config::auth_cache::ServerEntry {
51                token: token.to_string(),
52                user: Some(user.to_string()),
53                acquired_at: None,
54                expires_at: None,
55            },
56        );
57        cache
58    }
59
60    fn cache_with_token_no_user(server: &str, token: &str) -> AuthCache {
61        let mut cache = AuthCache::default();
62        cache.set_token(server.to_string(), token.to_string());
63        cache
64    }
65
66    fn env_token() -> ResolvedToken {
67        ResolvedToken { token: "env-tok".to_string(), origin: TokenOrigin::Env }
68    }
69
70    fn cache_token() -> ResolvedToken {
71        ResolvedToken { token: "cache-tok".to_string(), origin: TokenOrigin::Cache }
72    }
73
74    /// Case 1: no token → "anonymous"
75    #[test]
76    fn no_token_is_anonymous() {
77        let cache = AuthCache::default();
78        let state = read_auth_state(None, &cache, SERVER);
79        assert_eq!(state, "anonymous");
80    }
81
82    /// Case 2: env token → "authenticated via DSP_TOKEN"
83    #[test]
84    fn env_token_disclosure() {
85        let cache = AuthCache::default();
86        let tok = env_token();
87        let state = read_auth_state(Some(&tok), &cache, SERVER);
88        assert_eq!(state, "authenticated via DSP_TOKEN");
89    }
90
91    /// Case 3: cache token + user → "authenticated as {user}"
92    #[test]
93    fn cache_token_with_user_shows_username() {
94        let cache = cache_with_user(SERVER, "cache-tok", "alice@example.com");
95        let tok = cache_token();
96        let state = read_auth_state(Some(&tok), &cache, SERVER);
97        assert_eq!(state, "authenticated as alice@example.com");
98    }
99
100    /// Case 4: cache token, no user → "authenticated"
101    #[test]
102    fn cache_token_without_user_is_authenticated() {
103        let cache = cache_with_token_no_user(SERVER, "cache-tok");
104        let tok = cache_token();
105        let state = read_auth_state(Some(&tok), &cache, SERVER);
106        assert_eq!(state, "authenticated");
107    }
108}