Skip to main content

hackamore_control/
credentials.rs

1//! The credential vault: resolves a logical credential id (named by the policy engine
2//! via a `CredentialRef`) into a real upstream secret. Secrets live only here and in
3//! the data plane's outbound request; the agent never sees them.
4
5use parking_lot::RwLock;
6use std::collections::HashMap;
7
8/// A resolved credential value. A semantic type, deliberately not a `String`: its
9/// `Debug` is redacted so a secret can never leak into a log line, and the inner value
10/// is reachable only through the explicit [`Secret::expose`] call.
11#[derive(Clone)]
12pub struct Secret(String);
13
14impl Secret {
15    pub fn new(value: impl Into<String>) -> Self {
16        Self(value.into())
17    }
18
19    /// Reveal the raw secret. Call sites are the audited boundary where a secret enters
20    /// an outbound request; keep them few and obvious.
21    pub fn expose(&self) -> &str {
22        &self.0
23    }
24}
25
26impl std::fmt::Debug for Secret {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        f.write_str("Secret(***)")
29    }
30}
31
32/// Resolves credential ids to secrets. A trait so the in-memory store here can later be
33/// swapped for a GitHub App token minter, a KMS-backed vault, etc., with no change to
34/// the data plane.
35pub trait CredentialStore: Send + Sync {
36    /// The real secret for `id`, or `None` if no such credential is configured.
37    fn resolve(&self, id: &str) -> Option<Secret>;
38}
39
40/// A static, in-memory credential store seeded at startup. Adequate for v1, where the
41/// real upstream credential (e.g. a GitHub App installation token) is provisioned out
42/// of band and handed to hackamore.
43#[derive(Default)]
44pub struct InMemoryCredentials {
45    secrets: RwLock<HashMap<String, Secret>>,
46}
47
48impl InMemoryCredentials {
49    pub fn new() -> Self {
50        Self::default()
51    }
52
53    /// Register or replace the secret for a logical credential id.
54    pub fn insert(&self, id: impl Into<String>, secret: Secret) {
55        self.secrets.write().insert(id.into(), secret);
56    }
57}
58
59impl CredentialStore for InMemoryCredentials {
60    fn resolve(&self, id: &str) -> Option<Secret> {
61        self.secrets.read().get(id).cloned()
62    }
63}
64
65#[cfg(test)]
66#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn secret_debug_is_redacted() {
72        let s = Secret::new("ghp_supersecret");
73        assert_eq!(format!("{s:?}"), "Secret(***)");
74        assert_eq!(s.expose(), "ghp_supersecret");
75    }
76
77    #[test]
78    fn store_resolves_known_and_misses_unknown() {
79        let store = InMemoryCredentials::new();
80        store.insert("github-app", Secret::new("token-123"));
81        assert_eq!(store.resolve("github-app").unwrap().expose(), "token-123");
82        assert!(store.resolve("nope").is_none());
83    }
84}