feat: add secret store foundation
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
use crank_core::{
|
||||
AgentId, AgentOperationBinding, AgentStatus, AgentVersion, AuthProfile, GraphqlOperationType,
|
||||
HttpMethod, InvitationId, InvitationToken, InvocationLog, InvocationLogId, MembershipRole,
|
||||
OperationId, OperationStatus, PlatformApiKey, PlatformApiKeyId, PlatformApiKeyStatus, Target,
|
||||
UsageRollup, User, UserId, UserSessionId, Workspace, WorkspaceId,
|
||||
OperationId, OperationStatus, PlatformApiKey, PlatformApiKeyId, PlatformApiKeyStatus, Secret,
|
||||
SecretId, SecretVersion, Target, UsageRollup, User, UserId, UserSessionId, Workspace,
|
||||
WorkspaceId,
|
||||
};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use serde_json::Value;
|
||||
@@ -18,16 +19,17 @@ use crate::{
|
||||
model::{
|
||||
AgentSummary, AgentVersionRecord, AuthUserRecord, CreateAgentRequest,
|
||||
CreateInvitationRequest, CreateInvocationLogRequest, CreatePlatformApiKeyRequest,
|
||||
CreateVersionRequest, CreateWorkspaceRequest, CreateYamlImportJobRequest,
|
||||
DescriptorMetadata, InvitationRecord, InvocationLogRecord, ListInvocationLogsQuery,
|
||||
MembershipRecord, OperationAgentRef, OperationSampleMetadata, OperationSummary,
|
||||
OperationUsageSummary, OperationVersionRecord, PlatformApiKeyRecord, PublishAgentRequest,
|
||||
PublishRequest, PublishedAgentTool, RegistryOperation, SaveAgentBindingsRequest,
|
||||
SaveAuthProfileRequest, SaveDescriptorMetadataRequest, SaveSampleMetadataRequest,
|
||||
SessionRecord, UpdateWorkspaceRequest, UsageAgentBreakdown, UsageOperationBreakdown,
|
||||
UsageQuery, UsageRollupRecord, UsageSummary, UsageTimelinePoint, WorkspaceMembershipRecord,
|
||||
WorkspaceRecord, YamlImportJob, YamlImportJobCompletion, YamlImportJobId,
|
||||
YamlImportJobStatus,
|
||||
CreateSecretRequest, CreateVersionRequest, CreateWorkspaceRequest,
|
||||
CreateYamlImportJobRequest, DescriptorMetadata, InvitationRecord, InvocationLogRecord,
|
||||
ListInvocationLogsQuery, MembershipRecord, OperationAgentRef, OperationSampleMetadata,
|
||||
OperationSummary, OperationUsageSummary, OperationVersionRecord, PlatformApiKeyRecord,
|
||||
PublishAgentRequest, PublishRequest, PublishedAgentTool, RegistryOperation,
|
||||
RotateSecretRequest, SaveAgentBindingsRequest, SaveAuthProfileRequest,
|
||||
SaveDescriptorMetadataRequest, SaveSampleMetadataRequest, SecretRecord,
|
||||
SecretVersionRecord, SessionRecord, UpdateWorkspaceRequest, UsageAgentBreakdown,
|
||||
UsageOperationBreakdown, UsageQuery, UsageRollupRecord, UsageSummary, UsageTimelinePoint,
|
||||
WorkspaceMembershipRecord, WorkspaceRecord, YamlImportJob, YamlImportJobCompletion,
|
||||
YamlImportJobId, YamlImportJobStatus,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -652,6 +654,212 @@ impl PostgresRegistry {
|
||||
row.as_ref().map(map_platform_api_key_record).transpose()
|
||||
}
|
||||
|
||||
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,
|
||||
to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
|
||||
to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at,
|
||||
to_char(last_used_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as last_used_at
|
||||
from secrets
|
||||
where workspace_id = $1
|
||||
order by name asc",
|
||||
)
|
||||
.bind(workspace_id.as_str())
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
|
||||
rows.iter().map(map_secret_record).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,
|
||||
to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
|
||||
to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at,
|
||||
to_char(last_used_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as last_used_at
|
||||
from secrets
|
||||
where workspace_id = $1 and id = $2",
|
||||
)
|
||||
.bind(workspace_id.as_str())
|
||||
.bind(secret_id.as_str())
|
||||
.fetch_optional(&self.pool)
|
||||
.await?;
|
||||
|
||||
row.as_ref().map(map_secret_record).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.as_deref())
|
||||
.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.to_owned(),
|
||||
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 create_platform_api_key(
|
||||
&self,
|
||||
request: CreatePlatformApiKeyRequest<'_>,
|
||||
@@ -3088,6 +3296,25 @@ fn map_platform_api_key_record(row: &PgRow) -> Result<PlatformApiKeyRecord, Regi
|
||||
})
|
||||
}
|
||||
|
||||
fn map_secret_record(row: &PgRow) -> Result<SecretRecord, RegistryError> {
|
||||
Ok(SecretRecord {
|
||||
secret: Secret {
|
||||
id: SecretId::new(row.try_get::<String, _>("id")?),
|
||||
workspace_id: WorkspaceId::new(row.try_get::<String, _>("workspace_id")?),
|
||||
name: row.try_get("name")?,
|
||||
kind: deserialize_enum_text(&row.try_get::<String, _>("kind")?, "kind")?,
|
||||
status: deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?,
|
||||
current_version: from_db_version(
|
||||
row.try_get::<i32, _>("current_version")?,
|
||||
"current_version",
|
||||
)?,
|
||||
created_at: row.try_get("created_at")?,
|
||||
updated_at: row.try_get("updated_at")?,
|
||||
last_used_at: row.try_get("last_used_at")?,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn map_invocation_log_record(row: &PgRow) -> Result<InvocationLogRecord, RegistryError> {
|
||||
Ok(InvocationLogRecord {
|
||||
log: InvocationLog {
|
||||
|
||||
Reference in New Issue
Block a user