use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OperationKind {
SecretLifecycle,
VaultLifecycle,
Execution,
Session,
Sync,
Share,
Team,
RotationPolicy,
CredentialHelper,
Other,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VaultLifecycleState {
Created,
PasswordRotated,
SnapshotRestored,
SecretMoved,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SecretLifecycleState {
Written,
Accessed,
Deleted,
Imported,
Exported,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ShareLifecycleState {
Published,
Consumed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TeamLifecycleState {
MemberAdded,
MemberRemoved,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PolicyLifecycleState {
Set,
Removed,
DueChecked,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SessionLifecycleState {
Unlocked,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SyncLifecycleState {
PullCompleted,
Merged,
TeamKeysSynced,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CredentialHelperLifecycleState {
Accessed,
Stored,
Erased,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "domain", content = "state", rename_all = "snake_case")]
pub enum LifecycleState {
Secret(SecretLifecycleState),
Vault(VaultLifecycleState),
Share(ShareLifecycleState),
Team(TeamLifecycleState),
Policy(PolicyLifecycleState),
Session(SessionLifecycleState),
Sync(SyncLifecycleState),
CredentialHelper(CredentialHelperLifecycleState),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OperationClassification {
pub kind: OperationKind,
pub lifecycle_state: Option<LifecycleState>,
pub event_type: Option<&'static str>,
}
pub fn classify_operation(operation: &str) -> OperationClassification {
match operation {
"set" => secret(
SecretLifecycleState::Written,
Some("com.tsafe.vault.secret.set.v1"),
),
"delete" => secret(
SecretLifecycleState::Deleted,
Some("com.tsafe.vault.secret.deleted.v1"),
),
"get" | "browser-get" | "browser-list" => secret(
SecretLifecycleState::Accessed,
Some("com.tsafe.vault.secret.accessed.v1"),
),
"alias" | "gen" | "pin" | "unpin" | "ssh-add" | "ssh-import" | "totp-add"
| "browser-save" => secret(SecretLifecycleState::Written, None),
"totp-get" | "qr" => secret(SecretLifecycleState::Accessed, None),
"import" => secret(
SecretLifecycleState::Imported,
Some("com.tsafe.vault.imported.v1"),
),
"export" => secret(
SecretLifecycleState::Exported,
Some("com.tsafe.vault.exported.v1"),
),
"init" | "create" | "team-init" => vault(
VaultLifecycleState::Created,
Some("com.tsafe.vault.created.v1"),
),
"rotate" => vault(
VaultLifecycleState::PasswordRotated,
Some("com.tsafe.vault.rotated.v1"),
),
"snapshot-restore" => vault(VaultLifecycleState::SnapshotRestored, None),
"mv" | "ns-move" => vault(VaultLifecycleState::SecretMoved, None),
"ns-copy" => secret(SecretLifecycleState::Written, None),
"exec" | "plugin" => OperationClassification {
kind: OperationKind::Execution,
lifecycle_state: None,
event_type: Some("com.tsafe.vault.exec.v1"),
},
"unlock" => session(
SessionLifecycleState::Unlocked,
Some("com.tsafe.session.unlocked.v1"),
),
"kv-pull" | "vault-pull" | "op-pull" | "pull" => sync(
SyncLifecycleState::PullCompleted,
Some("com.tsafe.sync.pull.completed.v1"),
),
"sync" => sync(SyncLifecycleState::Merged, None),
"team-sync-keys" => sync(SyncLifecycleState::TeamKeysSynced, None),
"team-add-member" => team(TeamLifecycleState::MemberAdded),
"team-remove-member" => team(TeamLifecycleState::MemberRemoved),
"share-once" | "snap" => share(
ShareLifecycleState::Published,
Some("com.tsafe.share.published.v1"),
),
"receive-once" | "snap-receive" => share(
ShareLifecycleState::Consumed,
Some("com.tsafe.share.consumed.v1"),
),
"policy-set" => policy(PolicyLifecycleState::Set, None),
"policy-remove" => policy(PolicyLifecycleState::Removed, None),
"doctor" => OperationClassification {
kind: OperationKind::RotationPolicy,
lifecycle_state: None,
event_type: None,
},
"rotate-due" => policy(
PolicyLifecycleState::DueChecked,
Some("com.tsafe.secret.rotation_due.v1"),
),
"credential-helper-get" => helper(CredentialHelperLifecycleState::Accessed),
"credential-helper-store" => helper(CredentialHelperLifecycleState::Stored),
"credential-helper-erase" => helper(CredentialHelperLifecycleState::Erased),
_ => OperationClassification {
kind: OperationKind::Other,
lifecycle_state: None,
event_type: None,
},
}
}
fn secret(
state: SecretLifecycleState,
event_type: Option<&'static str>,
) -> OperationClassification {
OperationClassification {
kind: OperationKind::SecretLifecycle,
lifecycle_state: Some(LifecycleState::Secret(state)),
event_type,
}
}
fn vault(state: VaultLifecycleState, event_type: Option<&'static str>) -> OperationClassification {
OperationClassification {
kind: OperationKind::VaultLifecycle,
lifecycle_state: Some(LifecycleState::Vault(state)),
event_type,
}
}
fn share(state: ShareLifecycleState, event_type: Option<&'static str>) -> OperationClassification {
OperationClassification {
kind: OperationKind::Share,
lifecycle_state: Some(LifecycleState::Share(state)),
event_type,
}
}
fn policy(
state: PolicyLifecycleState,
event_type: Option<&'static str>,
) -> OperationClassification {
OperationClassification {
kind: OperationKind::RotationPolicy,
lifecycle_state: Some(LifecycleState::Policy(state)),
event_type,
}
}
fn team(state: TeamLifecycleState) -> OperationClassification {
OperationClassification {
kind: OperationKind::Team,
lifecycle_state: Some(LifecycleState::Team(state)),
event_type: None,
}
}
fn session(
state: SessionLifecycleState,
event_type: Option<&'static str>,
) -> OperationClassification {
OperationClassification {
kind: OperationKind::Session,
lifecycle_state: Some(LifecycleState::Session(state)),
event_type,
}
}
fn sync(state: SyncLifecycleState, event_type: Option<&'static str>) -> OperationClassification {
OperationClassification {
kind: OperationKind::Sync,
lifecycle_state: Some(LifecycleState::Sync(state)),
event_type,
}
}
fn helper(state: CredentialHelperLifecycleState) -> OperationClassification {
OperationClassification {
kind: OperationKind::CredentialHelper,
lifecycle_state: Some(LifecycleState::CredentialHelper(state)),
event_type: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn init_is_classified_as_created_vault_lifecycle() {
let classified = classify_operation("init");
assert_eq!(classified.kind, OperationKind::VaultLifecycle);
assert_eq!(
classified.lifecycle_state,
Some(LifecycleState::Vault(VaultLifecycleState::Created))
);
assert_eq!(classified.event_type, Some("com.tsafe.vault.created.v1"));
}
#[test]
fn create_and_team_init_reuse_created_vault_lifecycle() {
let created = classify_operation("create");
assert_eq!(created.kind, OperationKind::VaultLifecycle);
assert_eq!(
created.lifecycle_state,
Some(LifecycleState::Vault(VaultLifecycleState::Created))
);
assert_eq!(created.event_type, Some("com.tsafe.vault.created.v1"));
let team_created = classify_operation("team-init");
assert_eq!(team_created.kind, OperationKind::VaultLifecycle);
assert_eq!(
team_created.lifecycle_state,
Some(LifecycleState::Vault(VaultLifecycleState::Created))
);
assert_eq!(team_created.event_type, Some("com.tsafe.vault.created.v1"));
}
#[test]
fn share_once_is_classified_as_published_share() {
let classified = classify_operation("share-once");
assert_eq!(classified.kind, OperationKind::Share);
assert_eq!(
classified.lifecycle_state,
Some(LifecycleState::Share(ShareLifecycleState::Published))
);
assert_eq!(classified.event_type, Some("com.tsafe.share.published.v1"));
}
#[test]
fn snapshot_restore_keeps_explicit_vault_state_without_forcing_new_event_type() {
let classified = classify_operation("snapshot-restore");
assert_eq!(classified.kind, OperationKind::VaultLifecycle);
assert_eq!(
classified.lifecycle_state,
Some(LifecycleState::Vault(VaultLifecycleState::SnapshotRestored))
);
assert_eq!(classified.event_type, None);
}
#[test]
fn unlock_is_classified_as_unlocked_session() {
let classified = classify_operation("unlock");
assert_eq!(classified.kind, OperationKind::Session);
assert_eq!(
classified.lifecycle_state,
Some(LifecycleState::Session(SessionLifecycleState::Unlocked))
);
assert_eq!(classified.event_type, Some("com.tsafe.session.unlocked.v1"));
}
#[test]
fn pull_and_sync_operations_have_explicit_sync_states() {
let pull = classify_operation("kv-pull");
assert_eq!(pull.kind, OperationKind::Sync);
assert_eq!(
pull.lifecycle_state,
Some(LifecycleState::Sync(SyncLifecycleState::PullCompleted))
);
assert_eq!(pull.event_type, Some("com.tsafe.sync.pull.completed.v1"));
let merged = classify_operation("sync");
assert_eq!(
merged.lifecycle_state,
Some(LifecycleState::Sync(SyncLifecycleState::Merged))
);
assert_eq!(merged.event_type, None);
let team = classify_operation("team-sync-keys");
assert_eq!(
team.lifecycle_state,
Some(LifecycleState::Sync(SyncLifecycleState::TeamKeysSynced))
);
assert_eq!(team.event_type, None);
}
#[test]
fn secret_operations_have_explicit_secret_states() {
let set = classify_operation("set");
assert_eq!(set.kind, OperationKind::SecretLifecycle);
assert_eq!(
set.lifecycle_state,
Some(LifecycleState::Secret(SecretLifecycleState::Written))
);
assert_eq!(set.event_type, Some("com.tsafe.vault.secret.set.v1"));
let get = classify_operation("get");
assert_eq!(
get.lifecycle_state,
Some(LifecycleState::Secret(SecretLifecycleState::Accessed))
);
assert_eq!(get.event_type, Some("com.tsafe.vault.secret.accessed.v1"));
let delete = classify_operation("delete");
assert_eq!(
delete.lifecycle_state,
Some(LifecycleState::Secret(SecretLifecycleState::Deleted))
);
assert_eq!(delete.event_type, Some("com.tsafe.vault.secret.deleted.v1"));
let imported = classify_operation("import");
assert_eq!(
imported.lifecycle_state,
Some(LifecycleState::Secret(SecretLifecycleState::Imported))
);
let exported = classify_operation("export");
assert_eq!(
exported.lifecycle_state,
Some(LifecycleState::Secret(SecretLifecycleState::Exported))
);
let generated = classify_operation("gen");
assert_eq!(
generated.lifecycle_state,
Some(LifecycleState::Secret(SecretLifecycleState::Written))
);
assert_eq!(generated.event_type, None);
let qr = classify_operation("qr");
assert_eq!(
qr.lifecycle_state,
Some(LifecycleState::Secret(SecretLifecycleState::Accessed))
);
assert_eq!(qr.event_type, None);
let namespace_copy = classify_operation("ns-copy");
assert_eq!(
namespace_copy.lifecycle_state,
Some(LifecycleState::Secret(SecretLifecycleState::Written))
);
assert_eq!(namespace_copy.event_type, None);
}
#[test]
fn namespace_move_reuses_secret_moved_vault_state() {
let namespace_move = classify_operation("ns-move");
assert_eq!(namespace_move.kind, OperationKind::VaultLifecycle);
assert_eq!(
namespace_move.lifecycle_state,
Some(LifecycleState::Vault(VaultLifecycleState::SecretMoved))
);
assert_eq!(namespace_move.event_type, None);
}
#[test]
fn policy_operations_have_explicit_policy_states() {
let set = classify_operation("policy-set");
assert_eq!(set.kind, OperationKind::RotationPolicy);
assert_eq!(
set.lifecycle_state,
Some(LifecycleState::Policy(PolicyLifecycleState::Set))
);
assert_eq!(set.event_type, None);
let removed = classify_operation("policy-remove");
assert_eq!(
removed.lifecycle_state,
Some(LifecycleState::Policy(PolicyLifecycleState::Removed))
);
assert_eq!(removed.event_type, None);
let due = classify_operation("rotate-due");
assert_eq!(
due.lifecycle_state,
Some(LifecycleState::Policy(PolicyLifecycleState::DueChecked))
);
assert_eq!(due.event_type, Some("com.tsafe.secret.rotation_due.v1"));
}
#[test]
fn credential_helper_operations_have_explicit_helper_states() {
let get = classify_operation("credential-helper-get");
assert_eq!(get.kind, OperationKind::CredentialHelper);
assert_eq!(
get.lifecycle_state,
Some(LifecycleState::CredentialHelper(
CredentialHelperLifecycleState::Accessed
))
);
assert_eq!(get.event_type, None);
let store = classify_operation("credential-helper-store");
assert_eq!(
store.lifecycle_state,
Some(LifecycleState::CredentialHelper(
CredentialHelperLifecycleState::Stored
))
);
let erase = classify_operation("credential-helper-erase");
assert_eq!(
erase.lifecycle_state,
Some(LifecycleState::CredentialHelper(
CredentialHelperLifecycleState::Erased
))
);
}
#[test]
fn team_membership_operations_have_explicit_team_states() {
let add = classify_operation("team-add-member");
assert_eq!(add.kind, OperationKind::Team);
assert_eq!(
add.lifecycle_state,
Some(LifecycleState::Team(TeamLifecycleState::MemberAdded))
);
assert_eq!(add.event_type, None);
let remove = classify_operation("team-remove-member");
assert_eq!(
remove.lifecycle_state,
Some(LifecycleState::Team(TeamLifecycleState::MemberRemoved))
);
assert_eq!(remove.event_type, None);
}
#[test]
fn unknown_operation_falls_back_to_other() {
let classified = classify_operation("custom-op");
assert_eq!(classified.kind, OperationKind::Other);
assert_eq!(classified.lifecycle_state, None);
assert_eq!(classified.event_type, None);
}
}