static_credstore_plugin/
config.rs1use serde::Deserialize;
2use uuid::Uuid;
3
4use credstore_sdk::SharingMode;
5
6#[derive(Debug, Clone, Deserialize, modkit_macros::ExpandVars)]
8#[serde(default, deny_unknown_fields)]
9pub struct StaticCredStorePluginConfig {
10 pub vendor: String,
12
13 pub priority: i16,
15
16 #[expand_vars]
18 pub secrets: Vec<SecretConfig>,
19}
20
21impl Default for StaticCredStorePluginConfig {
22 fn default() -> Self {
23 Self {
24 vendor: "hyperspot".to_owned(),
25 priority: 100,
26 secrets: Vec::new(),
27 }
28 }
29}
30
31#[derive(Clone, Deserialize, modkit_macros::ExpandVars)]
33#[serde(deny_unknown_fields)]
34pub struct SecretConfig {
35 pub tenant_id: Option<Uuid>,
47
48 pub owner_id: Option<Uuid>,
61
62 pub key: String,
64
65 #[expand_vars]
67 pub value: String,
68
69 pub sharing: Option<SharingMode>,
75}
76
77impl SecretConfig {
78 #[must_use]
81 pub fn resolve_sharing(&self) -> SharingMode {
82 self.sharing
83 .unwrap_or(match (self.tenant_id, self.owner_id) {
84 (None, _) => SharingMode::Shared,
85 (Some(_), None) => SharingMode::Tenant,
86 (Some(_), Some(_)) => SharingMode::Private,
87 })
88 }
89}
90
91impl core::fmt::Debug for SecretConfig {
92 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
93 f.debug_struct("SecretConfig")
94 .field("tenant_id", &self.tenant_id)
95 .field("owner_id", &self.owner_id)
96 .field("key", &self.key)
97 .field("value", &"<redacted>")
98 .field("sharing", &self.resolve_sharing())
99 .finish()
100 }
101}
102
103#[cfg(test)]
104#[cfg_attr(coverage_nightly, coverage(off))]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn config_defaults_are_applied() {
110 let yaml = r#"
111secrets:
112 - tenant_id: "00000000-0000-0000-0000-000000000001"
113 owner_id: "00000000-0000-0000-0000-000000000002"
114 key: "openai_api_key"
115 value: "sk-test-123"
116"#;
117
118 let cfg: StaticCredStorePluginConfig = serde_saphyr::from_str(yaml).unwrap();
119
120 assert_eq!(cfg.vendor, "hyperspot");
121 assert_eq!(cfg.priority, 100);
122 assert_eq!(cfg.secrets.len(), 1);
123 assert!(cfg.secrets[0].sharing.is_none());
124 assert_eq!(cfg.secrets[0].resolve_sharing(), SharingMode::Private);
125 }
126
127 #[test]
128 fn config_allows_omitted_tenant_and_owner() {
129 let yaml = r#"
130secrets:
131 - key: "global_api_key"
132 value: "sk-global"
133"#;
134
135 let cfg: StaticCredStorePluginConfig = serde_saphyr::from_str(yaml).unwrap();
136 assert_eq!(cfg.secrets.len(), 1);
137 assert!(cfg.secrets[0].tenant_id.is_none());
138 assert!(cfg.secrets[0].owner_id.is_none());
139 assert!(cfg.secrets[0].sharing.is_none());
140 assert_eq!(cfg.secrets[0].resolve_sharing(), SharingMode::Shared);
141 }
142
143 #[test]
144 fn config_allows_partial_tenant_only() {
145 let yaml = r#"
146secrets:
147 - tenant_id: "00000000-0000-0000-0000-000000000001"
148 key: "scoped_key"
149 value: "val"
150"#;
151
152 let cfg: StaticCredStorePluginConfig = serde_saphyr::from_str(yaml).unwrap();
153 assert!(cfg.secrets[0].tenant_id.is_some());
154 assert!(cfg.secrets[0].owner_id.is_none());
155 assert!(cfg.secrets[0].sharing.is_none());
156 assert_eq!(cfg.secrets[0].resolve_sharing(), SharingMode::Tenant);
157 }
158
159 #[test]
160 fn config_explicit_sharing_overrides_default() {
161 let yaml = r#"
163secrets:
164 - tenant_id: "00000000-0000-0000-0000-000000000001"
165 key: "key"
166 value: "val"
167 sharing: "shared"
168"#;
169
170 let cfg: StaticCredStorePluginConfig = serde_saphyr::from_str(yaml).unwrap();
171 assert_eq!(cfg.secrets[0].sharing, Some(SharingMode::Shared));
172 assert_eq!(cfg.secrets[0].resolve_sharing(), SharingMode::Shared);
173 }
174
175 #[test]
176 fn config_rejects_unknown_fields() {
177 let yaml = r#"
178vendor: "hyperspot"
179priority: 100
180unexpected: true
181"#;
182
183 let parsed: Result<StaticCredStorePluginConfig, _> = serde_saphyr::from_str(yaml);
184 assert!(parsed.is_err());
185 }
186
187 #[test]
188 fn config_allows_empty_secrets() {
189 let parsed: Result<StaticCredStorePluginConfig, _> = serde_saphyr::from_str("{}");
190 assert!(parsed.is_ok());
191
192 let cfg = match parsed {
193 Ok(cfg) => cfg,
194 Err(e) => panic!("failed to parse config: {e}"),
195 };
196 assert!(cfg.secrets.is_empty());
197 assert_eq!(cfg.vendor, "hyperspot");
198 assert_eq!(cfg.priority, 100);
199 }
200}