use crate::crypto::{ExposeSecret, SecretString};
use crate::error::{CoreError, MetadataError};
use crate::model::{
AuditAction, AuditEntry, AuditId, Group, Profile, Project, ProjectId, ProjectVar, Var,
VarFilter, VarId, VarKind,
};
use crate::traits::{
AuditSink, Clock, IdGenerator, MetadataStore, SecretStore, SystemClock, UuidV4IdGenerator,
};
pub struct RegistryService<M, S, A, C, I>
where
M: MetadataStore,
S: SecretStore,
A: AuditSink,
C: Clock,
I: IdGenerator,
{
metadata: M,
secrets: S,
audit: A,
clock: C,
id_gen: I,
}
impl<M, S, A> RegistryService<M, S, A, SystemClock, UuidV4IdGenerator>
where
M: MetadataStore,
S: SecretStore,
A: AuditSink,
{
pub const fn with_defaults(metadata: M, secrets: S, audit: A) -> Self {
Self::new(metadata, secrets, audit, SystemClock, UuidV4IdGenerator)
}
}
impl<M, S, A, C, I> RegistryService<M, S, A, C, I>
where
M: MetadataStore,
S: SecretStore,
A: AuditSink,
C: Clock,
I: IdGenerator,
{
pub const fn new(metadata: M, secrets: S, audit: A, clock: C, id_gen: I) -> Self {
Self {
metadata,
secrets,
audit,
clock,
id_gen,
}
}
pub fn create_var(
&self,
name: &str,
group: Group,
kind: VarKind,
value: SecretString,
) -> Result<VarId, CoreError> {
Var::validate_name(name)?;
if value.expose_secret().is_empty() {
return Err(MetadataError::Invalid("value is empty".into()).into());
}
if self.metadata.find_var_by_name(name)?.is_some() {
return Err(MetadataError::DuplicateName(name.to_owned()).into());
}
let id = VarId::from_uuid(self.id_gen.next());
let now = self.clock.now();
let length = value.expose_secret().len();
let var = Var::from_trusted_parts(
id,
name.to_owned(),
group,
kind,
Vec::new(),
length,
now,
now,
);
self.metadata.upsert_var(&var)?;
let value_write: Result<(), CoreError> = match kind {
VarKind::Plain => self
.metadata
.set_plain_value(id, value.expose_secret())
.map_err(Into::into),
VarKind::Secret => self.secrets.put(id, value).map_err(Into::into),
};
if let Err(e) = value_write {
let _ = self.metadata.delete_var(id);
return Err(e);
}
self.audit_var(id, AuditAction::Created)?;
Ok(id)
}
pub fn update_value(&self, id: VarId, value: SecretString) -> Result<(), CoreError> {
if value.expose_secret().is_empty() {
return Err(MetadataError::Invalid("value is empty".into()).into());
}
let Some(mut var) = self.metadata.get_var(id)? else {
return Err(MetadataError::VarNotFound(id).into());
};
let length = value.expose_secret().len();
match var.kind() {
VarKind::Plain => self.metadata.set_plain_value(id, value.expose_secret())?,
VarKind::Secret => self.secrets.put(id, value)?,
}
var.set_length(length);
self.metadata.upsert_var(&var)?;
self.audit_var(id, AuditAction::Updated)?;
Ok(())
}
pub fn get_value(&self, id: VarId) -> Result<Option<SecretString>, CoreError> {
let Some(var) = self.metadata.get_var(id)? else {
return Ok(None);
};
let kind = var.kind();
match kind {
VarKind::Plain => {
if let Some(plain) = self.metadata.get_plain_value(id)? {
return Ok(Some(SecretString::from(plain)));
}
if self.secrets.get(id)?.is_some() {
return Err(CoreError::TierMismatch {
id,
expected: VarKind::Plain,
found: VarKind::Secret,
});
}
Ok(None)
}
VarKind::Secret => {
if let Some(value) = self.secrets.get(id)? {
return Ok(Some(value));
}
if self.metadata.get_plain_value(id)?.is_some() {
return Err(CoreError::TierMismatch {
id,
expected: VarKind::Secret,
found: VarKind::Plain,
});
}
Ok(None)
}
}
}
pub fn get_var(&self, id: VarId) -> Result<Option<Var>, CoreError> {
Ok(self.metadata.get_var(id)?)
}
pub fn find_var_by_name(&self, name: &str) -> Result<Option<Var>, CoreError> {
Ok(self.metadata.find_var_by_name(name)?)
}
pub fn list_vars(&self, filter: &VarFilter) -> Result<Vec<Var>, CoreError> {
Ok(self.metadata.list_vars(filter)?)
}
pub fn delete_var(&self, id: VarId) -> Result<(), CoreError> {
if self.metadata.get_var(id)?.is_none() {
return Ok(());
}
self.metadata.delete_var(id)?;
self.secrets.delete(id)?;
self.audit_var(id, AuditAction::Deleted)?;
Ok(())
}
pub fn create_project(
&self,
name: impl Into<String>,
path: std::path::PathBuf,
) -> Result<ProjectId, CoreError> {
let id = ProjectId::from_uuid(self.id_gen.next());
let project = Project::from_parts(id, name.into(), path);
self.metadata.upsert_project(&project)?;
self.audit_project(id, None, AuditAction::Created)?;
Ok(id)
}
pub fn get_project(&self, id: ProjectId) -> Result<Option<Project>, CoreError> {
Ok(self.metadata.get_project(id)?)
}
pub fn list_projects(&self) -> Result<Vec<Project>, CoreError> {
Ok(self.metadata.list_projects()?)
}
pub fn delete_project(&self, id: ProjectId) -> Result<(), CoreError> {
if self.metadata.get_project(id)?.is_none() {
return Ok(());
}
self.metadata.delete_project(id)?;
self.audit_project(id, None, AuditAction::Deleted)?;
Ok(())
}
pub fn link_var(
&self,
project_id: ProjectId,
var_id: VarId,
profile: Profile,
alias: Option<String>,
) -> Result<(), CoreError> {
let link = ProjectVar {
project_id,
var_id,
alias,
profile,
};
self.metadata.upsert_link(&link)?;
self.audit_project(project_id, Some(var_id), AuditAction::Linked)?;
Ok(())
}
pub fn unlink_var(
&self,
project_id: ProjectId,
var_id: VarId,
profile: &Profile,
) -> Result<(), CoreError> {
let removed = self.metadata.delete_link(project_id, var_id, profile)?;
if removed {
self.audit_project(project_id, Some(var_id), AuditAction::Unlinked)?;
}
Ok(())
}
pub fn links_for_project(&self, project_id: ProjectId) -> Result<Vec<ProjectVar>, CoreError> {
Ok(self.metadata.list_links_for_project(project_id)?)
}
pub fn links_for_var(&self, var_id: VarId) -> Result<Vec<ProjectVar>, CoreError> {
Ok(self.metadata.list_links_for_var(var_id)?)
}
pub fn recent_audit(&self, limit: usize) -> Result<Vec<AuditEntry>, CoreError> {
Ok(self.audit.list(limit)?)
}
fn audit_var(&self, var_id: VarId, action: AuditAction) -> Result<(), MetadataError> {
let entry = AuditEntry::from_parts(
AuditId::from_uuid(self.id_gen.next()),
action,
Some(var_id),
None,
None,
self.clock.now(),
);
self.audit.append(&entry)
}
fn audit_project(
&self,
project_id: ProjectId,
var_id: Option<VarId>,
action: AuditAction,
) -> Result<(), MetadataError> {
let entry = AuditEntry::from_parts(
AuditId::from_uuid(self.id_gen.next()),
action,
var_id,
Some(project_id),
None,
self.clock.now(),
);
self.audit.append(&entry)
}
}