hackamore_control/
credentials.rs1use parking_lot::RwLock;
6use std::collections::HashMap;
7
8#[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 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
32pub trait CredentialStore: Send + Sync {
36 fn resolve(&self, id: &str) -> Option<Secret>;
38}
39
40#[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 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}