Skip to main content

winterbaume_secretsmanager/
views.rs

1//! Serde-compatible view types for Secrets Manager state snapshots.
2
3use std::collections::HashMap;
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7use winterbaume_core::{StateChangeNotifier, StateViewError, StatefulService};
8
9/// Serialise `Option<Vec<u8>>` as a lowercase hex string, deserialise back.
10mod opt_hex {
11    use super::*;
12
13    pub fn serialize<S>(value: &Option<Vec<u8>>, serializer: S) -> Result<S::Ok, S::Error>
14    where
15        S: Serializer,
16    {
17        match value {
18            Some(bytes) => {
19                let hex: String = bytes.iter().map(|b| format!("{b:02x}")).collect();
20                serializer.serialize_some(&hex)
21            }
22            None => serializer.serialize_none(),
23        }
24    }
25
26    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error>
27    where
28        D: Deserializer<'de>,
29    {
30        let opt: Option<String> = Option::deserialize(deserializer)?;
31        match opt {
32            None => Ok(None),
33            Some(hex) => {
34                if hex.len() % 2 != 0 {
35                    return Err(serde::de::Error::custom("odd-length hex string"));
36                }
37                let bytes = (0..hex.len())
38                    .step_by(2)
39                    .map(|i| u8::from_str_radix(&hex[i..i + 2], 16))
40                    .collect::<Result<Vec<u8>, _>>()
41                    .map_err(|e| serde::de::Error::custom(format!("invalid hex: {e}")))?;
42                Ok(Some(bytes))
43            }
44        }
45    }
46}
47
48use crate::handlers::SecretsManagerService;
49use crate::state::SecretsManagerState;
50use crate::types::{ReplicationStatus, RotationRules, Secret, SecretVersion};
51
52/// Serializable view of the entire Secrets Manager state for one account/region.
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct SecretsmanagerStateView {
55    /// Secrets keyed by secret name.
56    #[serde(default)]
57    pub secrets: HashMap<String, SecretView>,
58}
59
60/// Serializable view of a single secret.
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct SecretView {
63    pub name: String,
64    pub arn: String,
65    pub description: String,
66    pub created_date: String,
67    pub last_changed_date: String,
68    #[serde(default)]
69    pub versions: HashMap<String, SecretVersionView>,
70    pub current_version_id: Option<String>,
71    pub deleted_date: Option<String>,
72    #[serde(default)]
73    pub tags: HashMap<String, String>,
74    pub resource_policy: Option<String>,
75    pub rotation_enabled: Option<bool>,
76    pub rotation_lambda_arn: Option<String>,
77    pub rotation_rules: Option<RotationRulesView>,
78    pub last_rotated_date: Option<String>,
79    #[serde(default)]
80    pub replication_status: Vec<ReplicationStatusView>,
81    pub primary_region: Option<String>,
82}
83
84/// Serializable view of a single secret version.
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct SecretVersionView {
87    pub version_id: String,
88    pub secret_string: Option<String>,
89    #[serde(default, with = "opt_hex")]
90    pub secret_binary: Option<Vec<u8>>,
91    pub created_date: String,
92    #[serde(default)]
93    pub version_stages: Vec<String>,
94}
95
96/// Serializable view of rotation rules.
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct RotationRulesView {
99    pub automatically_after_days: Option<i64>,
100    pub duration: Option<String>,
101    pub schedule_expression: Option<String>,
102}
103
104/// Serializable view of replication status.
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct ReplicationStatusView {
107    pub region: String,
108    pub status: String,
109    pub status_message: Option<String>,
110    pub kms_key_id: Option<String>,
111    pub last_accessed_date: Option<String>,
112}
113
114// --- From internal types to view types ---
115
116impl From<&SecretsManagerState> for SecretsmanagerStateView {
117    fn from(state: &SecretsManagerState) -> Self {
118        SecretsmanagerStateView {
119            secrets: state
120                .secrets
121                .iter()
122                .map(|(k, v)| (k.clone(), SecretView::from(v)))
123                .collect(),
124        }
125    }
126}
127
128impl From<&Secret> for SecretView {
129    fn from(s: &Secret) -> Self {
130        SecretView {
131            name: s.name.clone(),
132            arn: s.arn.clone(),
133            description: s.description.clone(),
134            created_date: s.created_date.to_rfc3339(),
135            last_changed_date: s.last_changed_date.to_rfc3339(),
136            versions: s
137                .versions
138                .iter()
139                .map(|(k, v)| (k.clone(), SecretVersionView::from(v)))
140                .collect(),
141            current_version_id: s.current_version_id.clone(),
142            deleted_date: s.deleted_date.map(|d| d.to_rfc3339()),
143            tags: s.tags.clone(),
144            resource_policy: s.resource_policy.clone(),
145            rotation_enabled: s.rotation_enabled,
146            rotation_lambda_arn: s.rotation_lambda_arn.clone(),
147            rotation_rules: s.rotation_rules.as_ref().map(RotationRulesView::from),
148            last_rotated_date: s.last_rotated_date.map(|d| d.to_rfc3339()),
149            replication_status: s
150                .replication_status
151                .iter()
152                .map(ReplicationStatusView::from)
153                .collect(),
154            primary_region: s.primary_region.clone(),
155        }
156    }
157}
158
159impl From<&SecretVersion> for SecretVersionView {
160    fn from(v: &SecretVersion) -> Self {
161        SecretVersionView {
162            version_id: v.version_id.clone(),
163            secret_string: v.secret_string.clone(),
164            secret_binary: v.secret_binary.clone(),
165            created_date: v.created_date.to_rfc3339(),
166            version_stages: v.version_stages.clone(),
167        }
168    }
169}
170
171impl From<&RotationRules> for RotationRulesView {
172    fn from(r: &RotationRules) -> Self {
173        RotationRulesView {
174            automatically_after_days: r.automatically_after_days,
175            duration: r.duration.clone(),
176            schedule_expression: r.schedule_expression.clone(),
177        }
178    }
179}
180
181impl From<&ReplicationStatus> for ReplicationStatusView {
182    fn from(r: &ReplicationStatus) -> Self {
183        ReplicationStatusView {
184            region: r.region.clone(),
185            status: r.status.clone(),
186            status_message: r.status_message.clone(),
187            kms_key_id: r.kms_key_id.clone(),
188            last_accessed_date: r.last_accessed_date.map(|d| d.to_rfc3339()),
189        }
190    }
191}
192
193// --- From view types to internal types ---
194
195impl From<SecretsmanagerStateView> for SecretsManagerState {
196    fn from(view: SecretsmanagerStateView) -> Self {
197        SecretsManagerState {
198            secrets: view
199                .secrets
200                .into_iter()
201                .map(|(k, v)| (k, Secret::from(v)))
202                .collect(),
203        }
204    }
205}
206
207impl From<SecretView> for Secret {
208    fn from(v: SecretView) -> Self {
209        let parse_dt = |s: &str| -> DateTime<Utc> {
210            DateTime::parse_from_rfc3339(s)
211                .map(|dt| dt.with_timezone(&Utc))
212                .unwrap_or_else(|_| Utc::now())
213        };
214        Secret {
215            name: v.name,
216            arn: v.arn,
217            description: v.description,
218            created_date: parse_dt(&v.created_date),
219            last_changed_date: parse_dt(&v.last_changed_date),
220            versions: v
221                .versions
222                .into_iter()
223                .map(|(k, sv)| (k, SecretVersion::from(sv)))
224                .collect(),
225            current_version_id: v.current_version_id,
226            deleted_date: v.deleted_date.as_deref().map(parse_dt),
227            tags: v.tags,
228            resource_policy: v.resource_policy,
229            rotation_enabled: v.rotation_enabled,
230            rotation_lambda_arn: v.rotation_lambda_arn,
231            rotation_rules: v.rotation_rules.map(RotationRules::from),
232            last_rotated_date: v.last_rotated_date.as_deref().map(parse_dt),
233            replication_status: v
234                .replication_status
235                .into_iter()
236                .map(ReplicationStatus::from)
237                .collect(),
238            primary_region: v.primary_region,
239        }
240    }
241}
242
243impl From<SecretVersionView> for SecretVersion {
244    fn from(v: SecretVersionView) -> Self {
245        let created_date = DateTime::parse_from_rfc3339(&v.created_date)
246            .map(|dt| dt.with_timezone(&Utc))
247            .unwrap_or_else(|_| Utc::now());
248        SecretVersion {
249            version_id: v.version_id,
250            secret_string: v.secret_string,
251            secret_binary: v.secret_binary,
252            created_date,
253            version_stages: v.version_stages,
254        }
255    }
256}
257
258impl From<RotationRulesView> for RotationRules {
259    fn from(v: RotationRulesView) -> Self {
260        RotationRules {
261            automatically_after_days: v.automatically_after_days,
262            duration: v.duration,
263            schedule_expression: v.schedule_expression,
264        }
265    }
266}
267
268impl From<ReplicationStatusView> for ReplicationStatus {
269    fn from(v: ReplicationStatusView) -> Self {
270        let last_accessed_date = v.last_accessed_date.as_deref().and_then(|s| {
271            DateTime::parse_from_rfc3339(s)
272                .map(|dt| dt.with_timezone(&Utc))
273                .ok()
274        });
275        ReplicationStatus {
276            region: v.region,
277            status: v.status,
278            status_message: v.status_message,
279            kms_key_id: v.kms_key_id,
280            last_accessed_date,
281        }
282    }
283}
284
285// --- StatefulService implementation ---
286
287impl StatefulService for SecretsManagerService {
288    type StateView = SecretsmanagerStateView;
289
290    async fn snapshot(&self, account_id: &str, region: &str) -> Self::StateView {
291        let state = self.state.get(account_id, region);
292        let guard = state.read().await;
293        SecretsmanagerStateView::from(&*guard)
294    }
295
296    async fn restore(
297        &self,
298        account_id: &str,
299        region: &str,
300        view: Self::StateView,
301    ) -> Result<(), StateViewError> {
302        let state = self.state.get(account_id, region);
303        {
304            let mut guard = state.write().await;
305            *guard = SecretsManagerState::from(view);
306        }
307        self.notify_state_changed(account_id, region).await;
308        Ok(())
309    }
310
311    async fn merge(
312        &self,
313        account_id: &str,
314        region: &str,
315        view: Self::StateView,
316    ) -> Result<(), StateViewError> {
317        let state = self.state.get(account_id, region);
318        {
319            let mut guard = state.write().await;
320            for (name, secret_view) in view.secrets {
321                guard.secrets.insert(name, Secret::from(secret_view));
322            }
323        }
324        self.notify_state_changed(account_id, region).await;
325        Ok(())
326    }
327
328    fn notifier(&self) -> &StateChangeNotifier<Self::StateView> {
329        &self.notifier
330    }
331}