1use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum OperationKind {
16 SecretLifecycle,
17 VaultLifecycle,
18 Execution,
19 Session,
20 Sync,
21 Share,
22 Team,
23 RotationPolicy,
24 CredentialHelper,
25 Other,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum VaultLifecycleState {
31 Created,
32 PasswordRotated,
33 SnapshotRestored,
34 SecretMoved,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "snake_case")]
39pub enum SecretLifecycleState {
40 Written,
41 Accessed,
42 Deleted,
43 Imported,
44 Exported,
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(rename_all = "snake_case")]
49pub enum ShareLifecycleState {
50 Published,
51 Consumed,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(rename_all = "snake_case")]
56pub enum TeamLifecycleState {
57 MemberAdded,
58 MemberRemoved,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
62#[serde(rename_all = "snake_case")]
63pub enum PolicyLifecycleState {
64 Set,
65 Removed,
66 DueChecked,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
70#[serde(rename_all = "snake_case")]
71pub enum SessionLifecycleState {
72 Unlocked,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
76#[serde(rename_all = "snake_case")]
77pub enum SyncLifecycleState {
78 PullCompleted,
79 Merged,
80 TeamKeysSynced,
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
84#[serde(rename_all = "snake_case")]
85pub enum CredentialHelperLifecycleState {
86 Accessed,
87 Stored,
88 Erased,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
92#[serde(tag = "domain", content = "state", rename_all = "snake_case")]
93pub enum LifecycleState {
94 Secret(SecretLifecycleState),
95 Vault(VaultLifecycleState),
96 Share(ShareLifecycleState),
97 Team(TeamLifecycleState),
98 Policy(PolicyLifecycleState),
99 Session(SessionLifecycleState),
100 Sync(SyncLifecycleState),
101 CredentialHelper(CredentialHelperLifecycleState),
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub struct OperationClassification {
106 pub kind: OperationKind,
107 pub lifecycle_state: Option<LifecycleState>,
108 pub event_type: Option<&'static str>,
109}
110
111pub fn classify_operation(operation: &str) -> OperationClassification {
112 match operation {
113 "set" => secret(
114 SecretLifecycleState::Written,
115 Some("com.tsafe.vault.secret.set.v1"),
116 ),
117 "delete" => secret(
118 SecretLifecycleState::Deleted,
119 Some("com.tsafe.vault.secret.deleted.v1"),
120 ),
121 "get" | "browser-get" | "browser-list" => secret(
122 SecretLifecycleState::Accessed,
123 Some("com.tsafe.vault.secret.accessed.v1"),
124 ),
125 "alias" | "gen" | "pin" | "unpin" | "ssh-add" | "ssh-import" | "totp-add"
126 | "browser-save" => secret(SecretLifecycleState::Written, None),
127 "totp-get" | "qr" => secret(SecretLifecycleState::Accessed, None),
128 "import" => secret(
129 SecretLifecycleState::Imported,
130 Some("com.tsafe.vault.imported.v1"),
131 ),
132 "export" => secret(
133 SecretLifecycleState::Exported,
134 Some("com.tsafe.vault.exported.v1"),
135 ),
136
137 "init" | "create" | "team-init" => vault(
138 VaultLifecycleState::Created,
139 Some("com.tsafe.vault.created.v1"),
140 ),
141 "rotate" => vault(
142 VaultLifecycleState::PasswordRotated,
143 Some("com.tsafe.vault.rotated.v1"),
144 ),
145 "snapshot-restore" => vault(VaultLifecycleState::SnapshotRestored, None),
146 "mv" | "ns-move" => vault(VaultLifecycleState::SecretMoved, None),
147 "ns-copy" => secret(SecretLifecycleState::Written, None),
148
149 "exec" | "plugin" => OperationClassification {
150 kind: OperationKind::Execution,
151 lifecycle_state: None,
152 event_type: Some("com.tsafe.vault.exec.v1"),
153 },
154 "unlock" => session(
155 SessionLifecycleState::Unlocked,
156 Some("com.tsafe.session.unlocked.v1"),
157 ),
158 "kv-pull" | "vault-pull" | "op-pull" | "pull" => sync(
159 SyncLifecycleState::PullCompleted,
160 Some("com.tsafe.sync.pull.completed.v1"),
161 ),
162 "sync" => sync(SyncLifecycleState::Merged, None),
163 "team-sync-keys" => sync(SyncLifecycleState::TeamKeysSynced, None),
164 "team-add-member" => team(TeamLifecycleState::MemberAdded),
165 "team-remove-member" => team(TeamLifecycleState::MemberRemoved),
166 "share-once" | "snap" => share(
167 ShareLifecycleState::Published,
168 Some("com.tsafe.share.published.v1"),
169 ),
170 "receive-once" | "snap-receive" => share(
171 ShareLifecycleState::Consumed,
172 Some("com.tsafe.share.consumed.v1"),
173 ),
174 "policy-set" => policy(PolicyLifecycleState::Set, None),
175 "policy-remove" => policy(PolicyLifecycleState::Removed, None),
176 "doctor" => OperationClassification {
177 kind: OperationKind::RotationPolicy,
178 lifecycle_state: None,
179 event_type: None,
180 },
181 "rotate-due" => policy(
182 PolicyLifecycleState::DueChecked,
183 Some("com.tsafe.secret.rotation_due.v1"),
184 ),
185 "credential-helper-get" => helper(CredentialHelperLifecycleState::Accessed),
186 "credential-helper-store" => helper(CredentialHelperLifecycleState::Stored),
187 "credential-helper-erase" => helper(CredentialHelperLifecycleState::Erased),
188 _ => OperationClassification {
189 kind: OperationKind::Other,
190 lifecycle_state: None,
191 event_type: None,
192 },
193 }
194}
195
196fn secret(
197 state: SecretLifecycleState,
198 event_type: Option<&'static str>,
199) -> OperationClassification {
200 OperationClassification {
201 kind: OperationKind::SecretLifecycle,
202 lifecycle_state: Some(LifecycleState::Secret(state)),
203 event_type,
204 }
205}
206
207fn vault(state: VaultLifecycleState, event_type: Option<&'static str>) -> OperationClassification {
208 OperationClassification {
209 kind: OperationKind::VaultLifecycle,
210 lifecycle_state: Some(LifecycleState::Vault(state)),
211 event_type,
212 }
213}
214
215fn share(state: ShareLifecycleState, event_type: Option<&'static str>) -> OperationClassification {
216 OperationClassification {
217 kind: OperationKind::Share,
218 lifecycle_state: Some(LifecycleState::Share(state)),
219 event_type,
220 }
221}
222
223fn policy(
224 state: PolicyLifecycleState,
225 event_type: Option<&'static str>,
226) -> OperationClassification {
227 OperationClassification {
228 kind: OperationKind::RotationPolicy,
229 lifecycle_state: Some(LifecycleState::Policy(state)),
230 event_type,
231 }
232}
233
234fn team(state: TeamLifecycleState) -> OperationClassification {
235 OperationClassification {
236 kind: OperationKind::Team,
237 lifecycle_state: Some(LifecycleState::Team(state)),
238 event_type: None,
239 }
240}
241
242fn session(
243 state: SessionLifecycleState,
244 event_type: Option<&'static str>,
245) -> OperationClassification {
246 OperationClassification {
247 kind: OperationKind::Session,
248 lifecycle_state: Some(LifecycleState::Session(state)),
249 event_type,
250 }
251}
252
253fn sync(state: SyncLifecycleState, event_type: Option<&'static str>) -> OperationClassification {
254 OperationClassification {
255 kind: OperationKind::Sync,
256 lifecycle_state: Some(LifecycleState::Sync(state)),
257 event_type,
258 }
259}
260
261fn helper(state: CredentialHelperLifecycleState) -> OperationClassification {
262 OperationClassification {
263 kind: OperationKind::CredentialHelper,
264 lifecycle_state: Some(LifecycleState::CredentialHelper(state)),
265 event_type: None,
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 #[test]
274 fn init_is_classified_as_created_vault_lifecycle() {
275 let classified = classify_operation("init");
276 assert_eq!(classified.kind, OperationKind::VaultLifecycle);
277 assert_eq!(
278 classified.lifecycle_state,
279 Some(LifecycleState::Vault(VaultLifecycleState::Created))
280 );
281 assert_eq!(classified.event_type, Some("com.tsafe.vault.created.v1"));
282 }
283
284 #[test]
285 fn create_and_team_init_reuse_created_vault_lifecycle() {
286 let created = classify_operation("create");
287 assert_eq!(created.kind, OperationKind::VaultLifecycle);
288 assert_eq!(
289 created.lifecycle_state,
290 Some(LifecycleState::Vault(VaultLifecycleState::Created))
291 );
292 assert_eq!(created.event_type, Some("com.tsafe.vault.created.v1"));
293
294 let team_created = classify_operation("team-init");
295 assert_eq!(team_created.kind, OperationKind::VaultLifecycle);
296 assert_eq!(
297 team_created.lifecycle_state,
298 Some(LifecycleState::Vault(VaultLifecycleState::Created))
299 );
300 assert_eq!(team_created.event_type, Some("com.tsafe.vault.created.v1"));
301 }
302
303 #[test]
304 fn share_once_is_classified_as_published_share() {
305 let classified = classify_operation("share-once");
306 assert_eq!(classified.kind, OperationKind::Share);
307 assert_eq!(
308 classified.lifecycle_state,
309 Some(LifecycleState::Share(ShareLifecycleState::Published))
310 );
311 assert_eq!(classified.event_type, Some("com.tsafe.share.published.v1"));
312 }
313
314 #[test]
315 fn snapshot_restore_keeps_explicit_vault_state_without_forcing_new_event_type() {
316 let classified = classify_operation("snapshot-restore");
317 assert_eq!(classified.kind, OperationKind::VaultLifecycle);
318 assert_eq!(
319 classified.lifecycle_state,
320 Some(LifecycleState::Vault(VaultLifecycleState::SnapshotRestored))
321 );
322 assert_eq!(classified.event_type, None);
323 }
324
325 #[test]
326 fn unlock_is_classified_as_unlocked_session() {
327 let classified = classify_operation("unlock");
328 assert_eq!(classified.kind, OperationKind::Session);
329 assert_eq!(
330 classified.lifecycle_state,
331 Some(LifecycleState::Session(SessionLifecycleState::Unlocked))
332 );
333 assert_eq!(classified.event_type, Some("com.tsafe.session.unlocked.v1"));
334 }
335
336 #[test]
337 fn pull_and_sync_operations_have_explicit_sync_states() {
338 let pull = classify_operation("kv-pull");
339 assert_eq!(pull.kind, OperationKind::Sync);
340 assert_eq!(
341 pull.lifecycle_state,
342 Some(LifecycleState::Sync(SyncLifecycleState::PullCompleted))
343 );
344 assert_eq!(pull.event_type, Some("com.tsafe.sync.pull.completed.v1"));
345
346 let merged = classify_operation("sync");
347 assert_eq!(
348 merged.lifecycle_state,
349 Some(LifecycleState::Sync(SyncLifecycleState::Merged))
350 );
351 assert_eq!(merged.event_type, None);
352
353 let team = classify_operation("team-sync-keys");
354 assert_eq!(
355 team.lifecycle_state,
356 Some(LifecycleState::Sync(SyncLifecycleState::TeamKeysSynced))
357 );
358 assert_eq!(team.event_type, None);
359 }
360
361 #[test]
362 fn secret_operations_have_explicit_secret_states() {
363 let set = classify_operation("set");
364 assert_eq!(set.kind, OperationKind::SecretLifecycle);
365 assert_eq!(
366 set.lifecycle_state,
367 Some(LifecycleState::Secret(SecretLifecycleState::Written))
368 );
369 assert_eq!(set.event_type, Some("com.tsafe.vault.secret.set.v1"));
370
371 let get = classify_operation("get");
372 assert_eq!(
373 get.lifecycle_state,
374 Some(LifecycleState::Secret(SecretLifecycleState::Accessed))
375 );
376 assert_eq!(get.event_type, Some("com.tsafe.vault.secret.accessed.v1"));
377
378 let delete = classify_operation("delete");
379 assert_eq!(
380 delete.lifecycle_state,
381 Some(LifecycleState::Secret(SecretLifecycleState::Deleted))
382 );
383 assert_eq!(delete.event_type, Some("com.tsafe.vault.secret.deleted.v1"));
384
385 let imported = classify_operation("import");
386 assert_eq!(
387 imported.lifecycle_state,
388 Some(LifecycleState::Secret(SecretLifecycleState::Imported))
389 );
390
391 let exported = classify_operation("export");
392 assert_eq!(
393 exported.lifecycle_state,
394 Some(LifecycleState::Secret(SecretLifecycleState::Exported))
395 );
396
397 let generated = classify_operation("gen");
398 assert_eq!(
399 generated.lifecycle_state,
400 Some(LifecycleState::Secret(SecretLifecycleState::Written))
401 );
402 assert_eq!(generated.event_type, None);
403
404 let qr = classify_operation("qr");
405 assert_eq!(
406 qr.lifecycle_state,
407 Some(LifecycleState::Secret(SecretLifecycleState::Accessed))
408 );
409 assert_eq!(qr.event_type, None);
410
411 let namespace_copy = classify_operation("ns-copy");
412 assert_eq!(
413 namespace_copy.lifecycle_state,
414 Some(LifecycleState::Secret(SecretLifecycleState::Written))
415 );
416 assert_eq!(namespace_copy.event_type, None);
417 }
418
419 #[test]
420 fn namespace_move_reuses_secret_moved_vault_state() {
421 let namespace_move = classify_operation("ns-move");
422 assert_eq!(namespace_move.kind, OperationKind::VaultLifecycle);
423 assert_eq!(
424 namespace_move.lifecycle_state,
425 Some(LifecycleState::Vault(VaultLifecycleState::SecretMoved))
426 );
427 assert_eq!(namespace_move.event_type, None);
428 }
429
430 #[test]
431 fn policy_operations_have_explicit_policy_states() {
432 let set = classify_operation("policy-set");
433 assert_eq!(set.kind, OperationKind::RotationPolicy);
434 assert_eq!(
435 set.lifecycle_state,
436 Some(LifecycleState::Policy(PolicyLifecycleState::Set))
437 );
438 assert_eq!(set.event_type, None);
439
440 let removed = classify_operation("policy-remove");
441 assert_eq!(
442 removed.lifecycle_state,
443 Some(LifecycleState::Policy(PolicyLifecycleState::Removed))
444 );
445 assert_eq!(removed.event_type, None);
446
447 let due = classify_operation("rotate-due");
448 assert_eq!(
449 due.lifecycle_state,
450 Some(LifecycleState::Policy(PolicyLifecycleState::DueChecked))
451 );
452 assert_eq!(due.event_type, Some("com.tsafe.secret.rotation_due.v1"));
453 }
454
455 #[test]
456 fn credential_helper_operations_have_explicit_helper_states() {
457 let get = classify_operation("credential-helper-get");
458 assert_eq!(get.kind, OperationKind::CredentialHelper);
459 assert_eq!(
460 get.lifecycle_state,
461 Some(LifecycleState::CredentialHelper(
462 CredentialHelperLifecycleState::Accessed
463 ))
464 );
465 assert_eq!(get.event_type, None);
466
467 let store = classify_operation("credential-helper-store");
468 assert_eq!(
469 store.lifecycle_state,
470 Some(LifecycleState::CredentialHelper(
471 CredentialHelperLifecycleState::Stored
472 ))
473 );
474
475 let erase = classify_operation("credential-helper-erase");
476 assert_eq!(
477 erase.lifecycle_state,
478 Some(LifecycleState::CredentialHelper(
479 CredentialHelperLifecycleState::Erased
480 ))
481 );
482 }
483
484 #[test]
485 fn team_membership_operations_have_explicit_team_states() {
486 let add = classify_operation("team-add-member");
487 assert_eq!(add.kind, OperationKind::Team);
488 assert_eq!(
489 add.lifecycle_state,
490 Some(LifecycleState::Team(TeamLifecycleState::MemberAdded))
491 );
492 assert_eq!(add.event_type, None);
493
494 let remove = classify_operation("team-remove-member");
495 assert_eq!(
496 remove.lifecycle_state,
497 Some(LifecycleState::Team(TeamLifecycleState::MemberRemoved))
498 );
499 assert_eq!(remove.event_type, None);
500 }
501
502 #[test]
503 fn unknown_operation_falls_back_to_other() {
504 let classified = classify_operation("custom-op");
505 assert_eq!(classified.kind, OperationKind::Other);
506 assert_eq!(classified.lifecycle_state, None);
507 assert_eq!(classified.event_type, None);
508 }
509}