Files
crank/crates/crank-registry/src/postgres/secret.rs
T
github-ops 338bb4d74a
Deploy / deploy (push) Successful in 37s
CI / Rust Checks (push) Successful in 5m33s
CI / UI Checks (push) Successful in 5s
CI / Deployment Manifests (push) Successful in 3s
CI / Frontend E2E (push) Successful in 4m24s
chore: publish clean community baseline
2026-06-17 07:29:50 +00:00

305 lines
9.9 KiB
Rust

use super::*;
impl PostgresRegistry {
pub async fn list_secrets(
&self,
workspace_id: &WorkspaceId,
) -> Result<Vec<SecretRecord>, RegistryError> {
let rows = sqlx::query!(
"select
id,
workspace_id,
name,
kind,
status,
current_version,
created_at as \"created_at!: time::OffsetDateTime\",
updated_at as \"updated_at!: time::OffsetDateTime\",
last_used_at as \"last_used_at: time::OffsetDateTime\"
from secrets
where workspace_id = $1
order by name asc",
workspace_id.as_str(),
)
.fetch_all(&self.pool)
.await?;
rows.into_iter()
.map(|row| {
Ok(SecretRecord {
secret: Secret {
id: SecretId::new(row.id),
workspace_id: WorkspaceId::new(row.workspace_id),
name: row.name,
kind: deserialize_enum_text(&row.kind, "kind")?,
status: deserialize_enum_text(&row.status, "status")?,
current_version: from_db_version(row.current_version, "current_version")?,
created_at: row.created_at,
updated_at: row.updated_at,
last_used_at: row.last_used_at,
},
})
})
.collect()
}
pub async fn get_secret(
&self,
workspace_id: &WorkspaceId,
secret_id: &SecretId,
) -> Result<Option<SecretRecord>, RegistryError> {
let row = sqlx::query!(
"select
id,
workspace_id,
name,
kind,
status,
current_version,
created_at as \"created_at!: time::OffsetDateTime\",
updated_at as \"updated_at!: time::OffsetDateTime\",
last_used_at as \"last_used_at: time::OffsetDateTime\"
from secrets
where workspace_id = $1 and id = $2",
workspace_id.as_str(),
secret_id.as_str(),
)
.fetch_optional(&self.pool)
.await?;
row.map(|row| {
Ok(SecretRecord {
secret: Secret {
id: SecretId::new(row.id),
workspace_id: WorkspaceId::new(row.workspace_id),
name: row.name,
kind: deserialize_enum_text(&row.kind, "kind")?,
status: deserialize_enum_text(&row.status, "status")?,
current_version: from_db_version(row.current_version, "current_version")?,
created_at: row.created_at,
updated_at: row.updated_at,
last_used_at: row.last_used_at,
},
})
})
.transpose()
}
pub async fn get_current_secret_version(
&self,
workspace_id: &WorkspaceId,
secret_id: &SecretId,
) -> Result<Option<SecretVersionRecord>, RegistryError> {
let row = sqlx::query!(
"select
sv.secret_id,
sv.version,
sv.ciphertext,
sv.key_version,
sv.created_at as \"created_at!: time::OffsetDateTime\",
sv.created_by
from secrets s
join secret_versions sv
on sv.secret_id = s.id and sv.version = s.current_version
where s.workspace_id = $1 and s.id = $2",
workspace_id.as_str(),
secret_id.as_str(),
)
.fetch_optional(&self.pool)
.await?;
row.map(|row| {
Ok(SecretVersionRecord {
secret_version: SecretVersion {
secret_id: SecretId::new(row.secret_id),
version: from_db_version(row.version, "version")?,
ciphertext: row.ciphertext,
key_version: row.key_version,
created_at: row.created_at,
created_by: row.created_by.map(UserId::new),
},
})
})
.transpose()
}
pub async fn create_secret(
&self,
request: CreateSecretRequest<'_>,
) -> Result<(), RegistryError> {
let mut tx = self.pool.begin().await?;
let result = sqlx::query(
"insert into secrets (
id,
workspace_id,
name,
kind,
status,
current_version,
last_used_at,
created_at,
updated_at
) values (
$1, $2, $3, $4, $5, $6, $7::timestamptz, $8::timestamptz, $9::timestamptz
)",
)
.bind(request.secret.id.as_str())
.bind(request.secret.workspace_id.as_str())
.bind(&request.secret.name)
.bind(serialize_enum_text(&request.secret.kind, "kind")?)
.bind(serialize_enum_text(&request.secret.status, "status")?)
.bind(to_db_version(request.secret.current_version))
.bind(request.secret.last_used_at)
.bind(request.secret.created_at)
.bind(request.secret.updated_at)
.execute(&mut *tx)
.await;
match result {
Ok(_) => {
sqlx::query(
"insert into secret_versions (
secret_id,
version,
ciphertext,
key_version,
created_at,
created_by
) values (
$1, $2, $3, $4, $5::timestamptz, $6
)",
)
.bind(request.secret.id.as_str())
.bind(to_db_version(request.secret.current_version))
.bind(request.ciphertext)
.bind(request.key_version)
.bind(request.secret.created_at)
.bind(request.created_by.map(|value| value.as_str()))
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(())
}
Err(sqlx::Error::Database(error))
if error.constraint() == Some("secrets_workspace_name_idx") =>
{
Err(RegistryError::SecretNameAlreadyExists {
workspace_id: request.secret.workspace_id.as_str().to_owned(),
name: request.secret.name.clone(),
})
}
Err(error) => Err(RegistryError::Storage(error)),
}
}
pub async fn rotate_secret(
&self,
request: RotateSecretRequest<'_>,
) -> Result<SecretVersionRecord, RegistryError> {
let existing = self
.get_secret(request.workspace_id, request.secret_id)
.await?
.ok_or_else(|| RegistryError::SecretNotFound {
secret_id: request.secret_id.as_str().to_owned(),
})?;
let next_version = existing.secret.current_version + 1;
let mut tx = self.pool.begin().await?;
sqlx::query(
"insert into secret_versions (
secret_id,
version,
ciphertext,
key_version,
created_at,
created_by
) values (
$1, $2, $3, $4, $5::timestamptz, $6
)",
)
.bind(request.secret_id.as_str())
.bind(to_db_version(next_version))
.bind(request.ciphertext)
.bind(request.key_version)
.bind(request.created_at)
.bind(request.created_by.map(|value| value.as_str()))
.execute(&mut *tx)
.await?;
sqlx::query(
"update secrets
set current_version = $3,
updated_at = $4::timestamptz
where workspace_id = $1 and id = $2",
)
.bind(request.workspace_id.as_str())
.bind(request.secret_id.as_str())
.bind(to_db_version(next_version))
.bind(request.updated_at)
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(SecretVersionRecord {
secret_version: SecretVersion {
secret_id: request.secret_id.clone(),
version: next_version,
ciphertext: request.ciphertext.to_owned(),
key_version: request.key_version.to_owned(),
created_at: *request.created_at,
created_by: request.created_by.cloned(),
},
})
}
pub async fn delete_secret(
&self,
workspace_id: &WorkspaceId,
secret_id: &SecretId,
) -> Result<(), RegistryError> {
let result = sqlx::query(
"delete from secrets
where workspace_id = $1 and id = $2",
)
.bind(workspace_id.as_str())
.bind(secret_id.as_str())
.execute(&self.pool)
.await?;
if result.rows_affected() == 0 {
return Err(RegistryError::SecretNotFound {
secret_id: secret_id.as_str().to_owned(),
});
}
Ok(())
}
pub async fn touch_secret(
&self,
workspace_id: &WorkspaceId,
secret_id: &SecretId,
used_at: &OffsetDateTime,
) -> Result<(), RegistryError> {
let result = sqlx::query(
"update secrets
set last_used_at = $3::timestamptz
where workspace_id = $1 and id = $2",
)
.bind(workspace_id.as_str())
.bind(secret_id.as_str())
.bind(used_at)
.execute(&self.pool)
.await?;
if result.rows_affected() == 0 {
return Err(RegistryError::SecretNotFound {
secret_id: secret_id.as_str().to_owned(),
});
}
Ok(())
}
}