use super::*; impl PostgresRegistry { pub async fn list_secrets( &self, workspace_id: &WorkspaceId, ) -> Result, 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, 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, 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 { 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(()) } }