Skip to main content

raqeem_core/
credentials.rs

1//! Which key a backend is allowed to be given.
2//!
3//! This is one rule, and it is a security rule, so it lives in one place. The CLI and
4//! the Python binding both route through [`resolve_api_key`] rather than each deciding
5//! for itself — they used to decide separately, and a comment in the binding admitted
6//! it was "mirroring the CLI's credential rules exactly", which is the kind of promise
7//! that holds right up until it doesn't.
8//!
9//! Reading the environment stays with the caller. That keeps this function pure and its
10//! tests free of `set_var`, which is racy under a thread-parallel test runner and
11//! `unsafe` as of Rust 2024.
12
13use crate::provider::Provider;
14
15/// Pick the API key to send, or `None` to send no `Authorization` header at all.
16///
17/// - `explicit` — what the user handed us for *this* call: `--api-key`, the `api_key=`
18///   argument, or our own `$RAQEEM_API_KEY`. Applies to any backend, because it is
19///   scoped to raqeem rather than to a vendor.
20/// - `cohere_env` — `$COHERE_API_KEY`. Scoped to Cohere by its name, so it is a fallback
21///   for [`Provider::Cohere`] and nothing else.
22///
23/// The asymmetry is the whole point: a self-hosted or third-party endpoint reached via
24/// `--endpoint` must never be handed a Cohere key just because one happens to be
25/// exported. That would ship the user's credential to a server Cohere doesn't control.
26pub fn resolve_api_key(
27    provider: Provider,
28    explicit: Option<String>,
29    cohere_env: Option<String>,
30) -> Option<String> {
31    let (explicit, cohere_env) = (present(explicit), present(cohere_env));
32    match provider {
33        Provider::Cohere => explicit.or(cohere_env),
34        Provider::OpenAiCompatible => explicit,
35    }
36}
37
38/// A blank value is not a key.
39///
40/// `std::env::var` hands back `Ok("")` for a variable that is set but empty, which is an
41/// everyday state: a CI secret that didn't resolve, `export COHERE_API_KEY=` in a shell
42/// script, a `.env` line with nothing after the `=`. Treating that as a credential meant
43/// sending `Authorization: Bearer ` and getting an opaque 401 back from the server, in
44/// place of the clear local error one line further on.
45fn present(key: Option<String>) -> Option<String> {
46    key.filter(|k| !k.trim().is_empty())
47}
48
49#[cfg(test)]
50mod tests {
51    use super::resolve_api_key;
52    use crate::provider::Provider;
53
54    #[test]
55    fn cohere_falls_back_to_the_cohere_scoped_env() {
56        assert_eq!(
57            resolve_api_key(Provider::Cohere, None, Some("cohere-key".into())),
58            Some("cohere-key".into())
59        );
60    }
61
62    #[test]
63    fn an_explicit_key_wins_over_the_fallback() {
64        assert_eq!(
65            resolve_api_key(
66                Provider::Cohere,
67                Some("explicit".into()),
68                Some("cohere-key".into())
69            ),
70            Some("explicit".into())
71        );
72    }
73
74    /// The one that matters: a Cohere-scoped key must not leak to someone else's server.
75    #[test]
76    fn a_self_hosted_endpoint_never_receives_the_cohere_key() {
77        assert_eq!(
78            resolve_api_key(Provider::OpenAiCompatible, None, Some("cohere-key".into())),
79            None
80        );
81    }
82
83    #[test]
84    fn a_self_hosted_endpoint_still_honors_its_own_key() {
85        assert_eq!(
86            resolve_api_key(
87                Provider::OpenAiCompatible,
88                Some("mykey".into()),
89                Some("cohere-key".into())
90            ),
91            Some("mykey".into())
92        );
93    }
94
95    #[test]
96    fn a_blank_key_is_not_a_key() {
97        for blank in ["", "   ", "\t", "\n"] {
98            assert_eq!(
99                resolve_api_key(Provider::Cohere, Some(blank.into()), None),
100                None,
101                "blank explicit key {blank:?} should not count"
102            );
103            assert_eq!(
104                resolve_api_key(Provider::Cohere, None, Some(blank.into())),
105                None,
106                "blank $COHERE_API_KEY {blank:?} should not count"
107            );
108        }
109    }
110
111    /// The case that actually bites: an empty `--api-key` or `$RAQEEM_API_KEY` must fall
112    /// through to a real `$COHERE_API_KEY` rather than shadowing it with nothing.
113    #[test]
114    fn a_blank_explicit_key_falls_through_to_the_env() {
115        assert_eq!(
116            resolve_api_key(Provider::Cohere, Some("".into()), Some("real-key".into())),
117            Some("real-key".into())
118        );
119    }
120
121    /// ...but it must not resurrect the Cohere key for a self-hosted endpoint.
122    #[test]
123    fn a_blank_explicit_key_does_not_leak_the_cohere_key_to_openai() {
124        assert_eq!(
125            resolve_api_key(
126                Provider::OpenAiCompatible,
127                Some("".into()),
128                Some("cohere-key".into())
129            ),
130            None
131        );
132    }
133
134    #[test]
135    fn no_key_anywhere_is_none_not_empty_string() {
136        assert_eq!(resolve_api_key(Provider::Cohere, None, None), None);
137        assert_eq!(
138            resolve_api_key(Provider::OpenAiCompatible, None, None),
139            None
140        );
141    }
142}