Skip to main content

agent_first_http/cli/
token_source.rs

1//! What `--token-secret` accepts, and the one source afhttp reads itself.
2//!
3//! The grammar and every general source — `env:NAME`, `file:PATH#DOT_PATH`,
4//! `literal:` — come from AFDATA, which also renders the syntax into help and
5//! rejects an unacceptable one with the rest of the argv errors. What is left
6//! here is the part only afhttp can do: `container:NAME`, which reads a host
7//! token out of a container this binary manages. That token lives in a named
8//! volume, so no file path on this machine addresses it and no general source
9//! could reach it.
10
11use agent_first_data::value_source::SecretString;
12use agent_first_data::{SourceSet, ValueSource};
13
14use crate::shared::error::{Error, ErrorCode};
15
16/// The scheme afhttp parses for itself. Declared to AFDATA so help documents it
17/// and argv naming it is accepted; read below.
18const CONTAINER: &str = "container";
19
20/// The sources `--token-secret` accepts, in both the places that must agree:
21/// the registry that validates argv, and the read that follows.
22///
23/// No stream sources. A token is one short string a caller already keeps
24/// somewhere, and `stdin` in particular would collide with `--params @-`.
25pub fn set() -> SourceSet {
26    SourceSet::config().host_scheme(CONTAINER, "container:NAME")
27}
28
29/// Classify one `--token-secret` value. Pure — argv has already been validated
30/// against the same set, so this only fails if the two ever disagree.
31pub fn parse(raw: &str) -> Result<ValueSource, Error> {
32    set().parse(raw).map_err(|error| {
33        Error::new(
34            ErrorCode::InvalidArgument,
35            format!("--token-secret {error}"),
36        )
37    })
38}
39
40/// Read the token. A `SecretString`, so it reaches the `Authorization` header
41/// and nothing else — including a `{:?}` on whatever ends up holding it.
42pub fn read(source: &ValueSource) -> Result<SecretString, Error> {
43    match source {
44        ValueSource::Host { scheme, value } if scheme == CONTAINER => {
45            crate::cli::cmd::container::read_host_token(value)
46        }
47        other => other.read_secret().map_err(|error| {
48            Error::new(
49                ErrorCode::InvalidArgument,
50                format!("--token-secret {error}"),
51            )
52        }),
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn the_container_scheme_is_afhttps_and_the_rest_is_afdatas() {
62        assert_eq!(
63            parse("container:afhttp-host").expect("container"),
64            ValueSource::Host {
65                scheme: CONTAINER.to_string(),
66                value: "afhttp-host".to_string(),
67            }
68        );
69        assert_eq!(
70            parse("env:AFHTTP_TOKEN_SECRET").expect("env"),
71            ValueSource::Env("AFHTTP_TOKEN_SECRET".to_string())
72        );
73        // A bare token is the token, and `literal:` rescues one that starts
74        // with a scheme prefix.
75        assert_eq!(
76            parse("t0ken").expect("bare"),
77            ValueSource::Literal("t0ken".to_string())
78        );
79        assert_eq!(
80            parse("literal:container:x").expect("escape hatch"),
81            ValueSource::Literal("container:x".to_string())
82        );
83    }
84
85    /// A stream source is not in the set, so it is refused rather than read.
86    #[test]
87    fn a_token_does_not_come_from_a_stream() {
88        for raw in ["stdin", "fd:3", "prompt"] {
89            let error = parse(raw).expect_err(raw);
90            assert_eq!(error.error_code, ErrorCode::InvalidArgument, "{raw}");
91            assert!(error.detail.starts_with("--token-secret"), "{raw}");
92        }
93    }
94
95    #[test]
96    fn an_unreadable_source_names_the_flag_and_not_the_value() {
97        let error = read(&ValueSource::Env("AFHTTP_TEST_ABSENT_TOKEN".to_string()))
98            .expect_err("unset env source");
99        assert!(
100            error.detail.starts_with("--token-secret"),
101            "{}",
102            error.detail
103        );
104        assert!(
105            error.detail.contains("AFHTTP_TEST_ABSENT_TOKEN"),
106            "{}",
107            error.detail
108        );
109    }
110}