Menu
Public repository / fae297b794c347636c31aa9fabc2003287f83915
AkurAI-PassVault / crates/passvault-core/src/entities/mod.rs
Latest change fae297b794c347636c31aa9fabc2003287f83915 - AKURAI-PASSVAULT-6 add tenant-scoped managed-workspace secret lifecycle by Ólafur Búi Ólafsson
use crate::error::Error;
use crate::types::Timestamp;
use akurai_json::Value;
pub fn normalize_entry_name(name: &str) -> Result<String, Error> {
let trimmed = name.trim();
if trimmed.is_empty() {
return Err(Error::Validation("name is required".into()));
}
Ok(trimmed.to_string())
}
#[derive(Debug, Clone)]
pub struct VaultEntry {
pub id: i64,
pub name: String,
pub username: String,
pub password: String,
pub uri: String,
pub notes: String,
pub folder_id: Option<i64>,
pub created_at: Timestamp,
pub updated_at: Timestamp,
}
impl VaultEntry {
pub fn from_value(v: &Value) -> Result<Self, Error> {
Ok(VaultEntry {
id: v
.get("id")
.and_then(|v| v.as_i64())
.ok_or(Error::Validation("missing id".into()))?,
name: v
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
username: v
.get("username")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
password: v
.get("password")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
uri: v
.get("uri")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
notes: v
.get("notes")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
folder_id: v.get("folderId").and_then(|v| v.as_i64()),
created_at: v.get("createdAt").and_then(|v| v.as_i64()).unwrap_or(0),
updated_at: v.get("updatedAt").and_then(|v| v.as_i64()).unwrap_or(0),
})
}
pub fn to_value(&self) -> Value {
Value::Object(vec![
("id".into(), Value::Int(self.id)),
("name".into(), Value::Str(self.name.clone())),
("username".into(), Value::Str(self.username.clone())),
("password".into(), Value::Str(self.password.clone())),
("uri".into(), Value::Str(self.uri.clone())),
("notes".into(), Value::Str(self.notes.clone())),
(
"folderId".into(),
self.folder_id.map_or(Value::Null, Value::Int),
),
("createdAt".into(), Value::Int(self.created_at)),
("updatedAt".into(), Value::Int(self.updated_at)),
])
}
pub fn summary_value(&self) -> Value {
Value::Object(vec![
("id".into(), Value::Int(self.id)),
("name".into(), Value::Str(self.name.clone())),
("username".into(), Value::Str(self.username.clone())),
("uri".into(), Value::Str(self.uri.clone())),
(
"folderId".into(),
self.folder_id.map_or(Value::Null, Value::Int),
),
])
}
}
#[derive(Debug, Clone)]
pub struct Folder {
pub id: i64,
pub name: String,
pub created_at: Timestamp,
}
impl Folder {
pub fn from_value(v: &Value) -> Result<Self, Error> {
Ok(Folder {
id: v
.get("id")
.and_then(|v| v.as_i64())
.ok_or(Error::Validation("missing id".into()))?,
name: v
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
created_at: v.get("createdAt").and_then(|v| v.as_i64()).unwrap_or(0),
})
}
pub fn to_value(&self) -> Value {
Value::Object(vec![
("id".into(), Value::Int(self.id)),
("name".into(), Value::Str(self.name.clone())),
("createdAt".into(), Value::Int(self.created_at)),
])
}
}
/// A tenant-scoped managed secret for a workspace.
#[derive(Debug, Clone)]
pub struct WorkspaceSecret {
pub id: i64,
pub organization_id: String,
pub workspace_id: String,
pub name: String,
pub value: String,
pub notes: String,
pub active: bool,
pub rotation_id: Option<i64>,
pub created_at: Timestamp,
pub updated_at: Timestamp,
}
impl WorkspaceSecret {
pub fn from_value(v: &Value) -> Result<Self, Error> {
Ok(WorkspaceSecret {
id: v.get("id").and_then(|v| v.as_i64()).ok_or(Error::Validation("missing id".into()))?,
organization_id: v.get("organizationId").and_then(|v| v.as_str()).unwrap_or("").to_string(),
workspace_id: v.get("workspaceId").and_then(|v| v.as_str()).unwrap_or("").to_string(),
name: v.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string(),
value: v.get("value").and_then(|v| v.as_str()).unwrap_or("").to_string(),
notes: v.get("notes").and_then(|v| v.as_str()).unwrap_or("").to_string(),
active: v.get("active").and_then(|v| v.as_bool()).unwrap_or(true),
rotation_id: v.get("rotationId").and_then(|v| v.as_i64()),
created_at: v.get("createdAt").and_then(|v| v.as_i64()).unwrap_or(0),
updated_at: v.get("updatedAt").and_then(|v| v.as_i64()).unwrap_or(0),
})
}
pub fn to_value(&self) -> Value {
Value::Object(vec![
("id".into(), Value::Int(self.id)),
("organizationId".into(), Value::Str(self.organization_id.clone())),
("workspaceId".into(), Value::Str(self.workspace_id.clone())),
("name".into(), Value::Str(self.name.clone())),
("value".into(), Value::Str(self.value.clone())),
("notes".into(), Value::Str(self.notes.clone())),
("active".into(), Value::Bool(self.active)),
("rotationId".into(), self.rotation_id.map_or(Value::Null, Value::Int)),
("createdAt".into(), Value::Int(self.created_at)),
("updatedAt".into(), Value::Int(self.updated_at)),
])
}
/// Metadata-only view — never includes the secret value.
pub fn metadata_value(&self) -> Value {
Value::Object(vec![
("id".into(), Value::Int(self.id)),
("organizationId".into(), Value::Str(self.organization_id.clone())),
("workspaceId".into(), Value::Str(self.workspace_id.clone())),
("name".into(), Value::Str(self.name.clone())),
("notes".into(), Value::Str(self.notes.clone())),
("active".into(), Value::Bool(self.active)),
("rotationId".into(), self.rotation_id.map_or(Value::Null, Value::Int)),
("createdAt".into(), Value::Int(self.created_at)),
("updatedAt".into(), Value::Int(self.updated_at)),
])
}
}
/// A service identity within a workspace, used for scoped secret access.
#[derive(Debug, Clone)]
pub struct ServiceIdentity {
pub id: i64,
pub organization_id: String,
pub workspace_id: String,
pub name: String,
pub created_at: Timestamp,
}
impl ServiceIdentity {
pub fn from_value(v: &Value) -> Result<Self, Error> {
Ok(ServiceIdentity {
id: v.get("id").and_then(|v| v.as_i64()).ok_or(Error::Validation("missing id".into()))?,
organization_id: v.get("organizationId").and_then(|v| v.as_str()).unwrap_or("").to_string(),
workspace_id: v.get("workspaceId").and_then(|v| v.as_str()).unwrap_or("").to_string(),
name: v.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string(),
created_at: v.get("createdAt").and_then(|v| v.as_i64()).unwrap_or(0),
})
}
pub fn to_value(&self) -> Value {
Value::Object(vec![
("id".into(), Value::Int(self.id)),
("organizationId".into(), Value::Str(self.organization_id.clone())),
("workspaceId".into(), Value::Str(self.workspace_id.clone())),
("name".into(), Value::Str(self.name.clone())),
("createdAt".into(), Value::Int(self.created_at)),
])
}
}
/// A grant linking a service identity to a secret with a visibility level.
#[derive(Debug, Clone)]
pub struct SecretGrant {
pub id: i64,
pub organization_id: String,
pub workspace_id: String,
pub identity_id: i64,
pub secret_id: i64,
pub visibility: String,
pub created_at: Timestamp,
}
impl SecretGrant {
pub fn from_value(v: &Value) -> Result<Self, Error> {
Ok(SecretGrant {
id: v.get("id").and_then(|v| v.as_i64()).ok_or(Error::Validation("missing id".into()))?,
organization_id: v.get("organizationId").and_then(|v| v.as_str()).unwrap_or("").to_string(),
workspace_id: v.get("workspaceId").and_then(|v| v.as_str()).unwrap_or("").to_string(),
identity_id: v.get("identityId").and_then(|v| v.as_i64()).ok_or(Error::Validation("missing identityId".into()))?,
secret_id: v.get("secretId").and_then(|v| v.as_i64()).ok_or(Error::Validation("missing secretId".into()))?,
visibility: v.get("visibility").and_then(|v| v.as_str()).unwrap_or("metadata").to_string(),
created_at: v.get("createdAt").and_then(|v| v.as_i64()).unwrap_or(0),
})
}
pub fn to_value(&self) -> Value {
Value::Object(vec![
("id".into(), Value::Int(self.id)),
("organizationId".into(), Value::Str(self.organization_id.clone())),
("workspaceId".into(), Value::Str(self.workspace_id.clone())),
("identityId".into(), Value::Int(self.identity_id)),
("secretId".into(), Value::Int(self.secret_id)),
("visibility".into(), Value::Str(self.visibility.clone())),
("createdAt".into(), Value::Int(self.created_at)),
])
}
}
/// Rotation state for a secret — supports overlap and rollback.
#[derive(Debug, Clone)]
pub struct RotationState {
pub id: i64,
pub secret_id: i64,
pub previous_value_hash: Option<String>,
pub phase: String,
pub activated_at: Timestamp,
pub overlap_until: Option<Timestamp>,
pub created_at: Timestamp,
}
impl RotationState {
pub fn from_value(v: &Value) -> Result<Self, Error> {
Ok(RotationState {
id: v.get("id").and_then(|v| v.as_i64()).ok_or(Error::Validation("missing id".into()))?,
secret_id: v.get("secretId").and_then(|v| v.as_i64()).ok_or(Error::Validation("missing secretId".into()))?,
previous_value_hash: v.get("previousValueHash").and_then(|v| v.as_str()).map(|s| s.to_string()),
phase: v.get("phase").and_then(|v| v.as_str()).unwrap_or("active").to_string(),
activated_at: v.get("activatedAt").and_then(|v| v.as_i64()).unwrap_or(0),
overlap_until: v.get("overlapUntil").and_then(|v| v.as_i64()),
created_at: v.get("createdAt").and_then(|v| v.as_i64()).unwrap_or(0),
})
}
pub fn to_value(&self) -> Value {
Value::Object(vec![
("id".into(), Value::Int(self.id)),
("secretId".into(), Value::Int(self.secret_id)),
("previousValueHash".into(), self.previous_value_hash.as_ref().map_or(Value::Null, |s| Value::Str(s.clone()))),
("phase".into(), Value::Str(self.phase.clone())),
("activatedAt".into(), Value::Int(self.activated_at)),
("overlapUntil".into(), self.overlap_until.map_or(Value::Null, Value::Int)),
("createdAt".into(), Value::Int(self.created_at)),
])
}
}
/// Immutable audit entry — tracks actor/purpose/result without secret material.
#[derive(Debug, Clone)]
pub struct AuditEntry {
pub id: i64,
pub organization_id: String,
pub workspace_id: String,
pub actor: String,
pub action: String,
pub target: String,
pub purpose: String,
pub result: String,
pub timestamp: Timestamp,
}
impl AuditEntry {
pub fn from_value(v: &Value) -> Result<Self, Error> {
Ok(AuditEntry {
id: v.get("id").and_then(|v| v.as_i64()).ok_or(Error::Validation("missing id".into()))?,
organization_id: v.get("organizationId").and_then(|v| v.as_str()).unwrap_or("").to_string(),
workspace_id: v.get("workspaceId").and_then(|v| v.as_str()).unwrap_or("").to_string(),
actor: v.get("actor").and_then(|v| v.as_str()).unwrap_or("").to_string(),
action: v.get("action").and_then(|v| v.as_str()).unwrap_or("").to_string(),
target: v.get("target").and_then(|v| v.as_str()).unwrap_or("").to_string(),
purpose: v.get("purpose").and_then(|v| v.as_str()).unwrap_or("").to_string(),
result: v.get("result").and_then(|v| v.as_str()).unwrap_or("").to_string(),
timestamp: v.get("timestamp").and_then(|v| v.as_i64()).unwrap_or(0),
})
}
pub fn to_value(&self) -> Value {
Value::Object(vec![
("id".into(), Value::Int(self.id)),
("organizationId".into(), Value::Str(self.organization_id.clone())),
("workspaceId".into(), Value::Str(self.workspace_id.clone())),
("actor".into(), Value::Str(self.actor.clone())),
("action".into(), Value::Str(self.action.clone())),
("target".into(), Value::Str(self.target.clone())),
("purpose".into(), Value::Str(self.purpose.clone())),
("result".into(), Value::Str(self.result.clone())),
("timestamp".into(), Value::Int(self.timestamp)),
])
}
}
#[cfg(test)]
mod tests {
use super::normalize_entry_name;
#[test]
fn normalize_entry_name_trims_whitespace() {
assert_eq!(normalize_entry_name(" Example ").unwrap(), "Example");
}
#[test]
fn normalize_entry_name_rejects_blank_values() {
assert!(normalize_entry_name(" \t\n ").is_err());
}
}