Split admin secrets service module
This commit is contained in:
@@ -11,20 +11,18 @@ use crank_core::{
|
|||||||
InvocationLogId, InvocationSource, InvocationStatus, LoginOutcome, MembershipRole,
|
InvocationLogId, InvocationSource, InvocationStatus, LoginOutcome, MembershipRole,
|
||||||
NoopAuditSink, OperationId, OperationSecurityLevel, OperationStatus, OwnerOnlyPolicyEngine,
|
NoopAuditSink, OperationId, OperationSecurityLevel, OperationStatus, OwnerOnlyPolicyEngine,
|
||||||
PlatformApiKey, PlatformApiKeyId, PlatformApiKeyScope, PlatformApiKeyStatus, PolicyEngine,
|
PlatformApiKey, PlatformApiKeyId, PlatformApiKeyScope, PlatformApiKeyStatus, PolicyEngine,
|
||||||
ProductEdition, Protocol, ResponseCachePolicy, SampleId, Samples, Secret, SecretId, SecretKind,
|
ProductEdition, Protocol, ResponseCachePolicy, SampleId, Samples, SecretKind, Target,
|
||||||
SecretStatus, Target, ToolQualityMappingRule, ToolQualityMappingSet, ToolQualitySchemaKind,
|
ToolQualityMappingRule, ToolQualityMappingSet, ToolQualitySchemaKind, ToolQualitySchemaNode,
|
||||||
ToolQualitySchemaNode, UsagePeriod, UserId, UserSessionId, WizardState, Workspace, WorkspaceId,
|
UsagePeriod, UserSessionId, WizardState, Workspace, WorkspaceId, WorkspaceStatus,
|
||||||
WorkspaceStatus,
|
|
||||||
};
|
};
|
||||||
use crank_mapping::{JsonPathRoot, MappingRule, MappingSet, infer_mapping_from_samples};
|
use crank_mapping::{JsonPathRoot, MappingRule, MappingSet, infer_mapping_from_samples};
|
||||||
use crank_registry::{
|
use crank_registry::{
|
||||||
AgentSummary, AgentVersionRecord, CreateAgentDraftVersionRequest, CreateAgentRequest,
|
AgentSummary, AgentVersionRecord, CreateAgentDraftVersionRequest, CreateAgentRequest,
|
||||||
CreateInvocationLogRequest, CreatePlatformApiKeyRequest, CreateSecretRequest,
|
CreateInvocationLogRequest, CreatePlatformApiKeyRequest, CreateVersionRequest,
|
||||||
CreateVersionRequest, CreateWorkspaceRequest, ListInvocationLogsQuery, OperationAgentRef,
|
CreateWorkspaceRequest, ListInvocationLogsQuery, OperationAgentRef, OperationSampleMetadata,
|
||||||
OperationSampleMetadata, OperationSummary, OperationUsageSummary, OperationVersionRecord,
|
OperationSummary, OperationUsageSummary, OperationVersionRecord, PlatformApiKeyRecord,
|
||||||
PlatformApiKeyRecord, PostgresRegistry, PublishAgentRequest, PublishRequest, RegistryError,
|
PostgresRegistry, PublishAgentRequest, PublishRequest, RegistryOperation, SampleKind,
|
||||||
RegistryOperation, RotateSecretRequest, SampleKind, SaveAgentBindingsRequest,
|
SaveAgentBindingsRequest, SaveSampleMetadataRequest, SaveWorkspaceUpstreamRequest,
|
||||||
SaveAuthProfileRequest, SaveSampleMetadataRequest, SaveWorkspaceUpstreamRequest,
|
|
||||||
UpdateWorkspaceRequest, UsageAgentBreakdown, UsageBucket, UsageOperationBreakdown, UsageQuery,
|
UpdateWorkspaceRequest, UsageAgentBreakdown, UsageBucket, UsageOperationBreakdown, UsageQuery,
|
||||||
UsageSummary, UsageTimelinePoint, WorkspaceMembershipRecord, WorkspaceRecord,
|
UsageSummary, UsageTimelinePoint, WorkspaceMembershipRecord, WorkspaceRecord,
|
||||||
WorkspaceUpstream, WorkspaceUpstreamId,
|
WorkspaceUpstream, WorkspaceUpstreamId,
|
||||||
@@ -43,6 +41,7 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
mod import_export;
|
mod import_export;
|
||||||
mod observability;
|
mod observability;
|
||||||
|
mod secrets;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::{
|
auth::{
|
||||||
@@ -1716,15 +1715,6 @@ impl AdminService {
|
|||||||
ResolvedAuth::from_profile(auth_profile, &secrets)
|
ResolvedAuth::from_profile(auth_profile, &secrets)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
|
||||||
pub async fn list_auth_profiles(
|
|
||||||
&self,
|
|
||||||
workspace_id: &WorkspaceId,
|
|
||||||
) -> Result<Vec<AuthProfile>, ApiError> {
|
|
||||||
self.ensure_workspace_exists(workspace_id).await?;
|
|
||||||
Ok(self.registry.list_auth_profiles(workspace_id).await?)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
pub async fn list_workspace_upstreams(
|
pub async fn list_workspace_upstreams(
|
||||||
&self,
|
&self,
|
||||||
@@ -1823,136 +1813,6 @@ impl AdminService {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
|
||||||
pub async fn list_secrets(&self, workspace_id: &WorkspaceId) -> Result<Vec<Secret>, ApiError> {
|
|
||||||
self.ensure_workspace_exists(workspace_id).await?;
|
|
||||||
Ok(self
|
|
||||||
.registry
|
|
||||||
.list_secrets(workspace_id)
|
|
||||||
.await?
|
|
||||||
.into_iter()
|
|
||||||
.map(|record| record.secret)
|
|
||||||
.collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
|
||||||
pub async fn get_secret(
|
|
||||||
&self,
|
|
||||||
workspace_id: &WorkspaceId,
|
|
||||||
secret_id: &SecretId,
|
|
||||||
) -> Result<Secret, ApiError> {
|
|
||||||
self.ensure_workspace_exists(workspace_id).await?;
|
|
||||||
self.registry
|
|
||||||
.get_secret(workspace_id, secret_id)
|
|
||||||
.await?
|
|
||||||
.map(|record| record.secret)
|
|
||||||
.ok_or_else(|| {
|
|
||||||
ApiError::not_found_with_context(
|
|
||||||
format!("secret {} was not found", secret_id.as_str()),
|
|
||||||
json!({ "secret_id": secret_id.as_str() }),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument(skip(self, payload), fields(workspace_id = %workspace_id.as_str(), secret_name = %payload.name))]
|
|
||||||
pub async fn create_secret(
|
|
||||||
&self,
|
|
||||||
workspace_id: &WorkspaceId,
|
|
||||||
created_by: Option<&UserId>,
|
|
||||||
payload: SecretPayload,
|
|
||||||
) -> Result<Secret, ApiError> {
|
|
||||||
self.ensure_workspace_exists(workspace_id).await?;
|
|
||||||
validate_secret_payload(&payload)?;
|
|
||||||
|
|
||||||
let now = OffsetDateTime::now_utc();
|
|
||||||
let secret = Secret {
|
|
||||||
id: SecretId::new(new_prefixed_id("secret")),
|
|
||||||
workspace_id: workspace_id.clone(),
|
|
||||||
name: payload.name.trim().to_owned(),
|
|
||||||
kind: payload.kind,
|
|
||||||
status: SecretStatus::Active,
|
|
||||||
current_version: 1,
|
|
||||||
created_at: now,
|
|
||||||
updated_at: now,
|
|
||||||
last_used_at: None,
|
|
||||||
};
|
|
||||||
let ciphertext = self
|
|
||||||
.secret_crypto
|
|
||||||
.encrypt(&payload.value)
|
|
||||||
.map_err(|error| ApiError::internal(error.to_string()))?;
|
|
||||||
|
|
||||||
self.registry
|
|
||||||
.create_secret(CreateSecretRequest {
|
|
||||||
secret: &secret,
|
|
||||||
ciphertext: &ciphertext,
|
|
||||||
key_version: self.secret_crypto.key_version(),
|
|
||||||
created_by,
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
info!(secret_id = %secret.id.as_str(), "secret created");
|
|
||||||
|
|
||||||
Ok(secret)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument(skip(self, payload), fields(workspace_id = %workspace_id.as_str(), secret_id = %secret_id.as_str()))]
|
|
||||||
pub async fn rotate_secret(
|
|
||||||
&self,
|
|
||||||
workspace_id: &WorkspaceId,
|
|
||||||
secret_id: &SecretId,
|
|
||||||
created_by: Option<&UserId>,
|
|
||||||
payload: RotateSecretPayload,
|
|
||||||
) -> Result<Secret, ApiError> {
|
|
||||||
self.ensure_workspace_exists(workspace_id).await?;
|
|
||||||
if payload.value.is_null() {
|
|
||||||
return Err(ApiError::validation("secret value must not be null"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let now = OffsetDateTime::now_utc();
|
|
||||||
let ciphertext = self
|
|
||||||
.secret_crypto
|
|
||||||
.encrypt(&payload.value)
|
|
||||||
.map_err(|error| ApiError::internal(error.to_string()))?;
|
|
||||||
self.registry
|
|
||||||
.rotate_secret(RotateSecretRequest {
|
|
||||||
workspace_id,
|
|
||||||
secret_id,
|
|
||||||
ciphertext: &ciphertext,
|
|
||||||
key_version: self.secret_crypto.key_version(),
|
|
||||||
created_at: &now,
|
|
||||||
updated_at: &now,
|
|
||||||
created_by,
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
info!(secret_id = %secret_id.as_str(), "secret rotated");
|
|
||||||
|
|
||||||
self.get_secret(workspace_id, secret_id).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument(skip(self), fields(workspace_id = %workspace_id.as_str(), secret_id = %secret_id.as_str()))]
|
|
||||||
pub async fn delete_secret(
|
|
||||||
&self,
|
|
||||||
workspace_id: &WorkspaceId,
|
|
||||||
secret_id: &SecretId,
|
|
||||||
) -> Result<(), ApiError> {
|
|
||||||
self.ensure_workspace_exists(workspace_id).await?;
|
|
||||||
if let Some(profile) = self
|
|
||||||
.registry
|
|
||||||
.list_auth_profiles_referencing_secret(workspace_id, secret_id)
|
|
||||||
.await?
|
|
||||||
.into_iter()
|
|
||||||
.next()
|
|
||||||
{
|
|
||||||
return Err(RegistryError::SecretReferencedByAuthProfile {
|
|
||||||
secret_id: secret_id.as_str().to_owned(),
|
|
||||||
auth_profile_id: profile.id.as_str().to_owned(),
|
|
||||||
}
|
|
||||||
.into());
|
|
||||||
}
|
|
||||||
self.registry.delete_secret(workspace_id, secret_id).await?;
|
|
||||||
info!(secret_id = %secret_id.as_str(), "secret deleted");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
pub async fn list_agents(
|
pub async fn list_agents(
|
||||||
&self,
|
&self,
|
||||||
@@ -2421,68 +2281,6 @@ impl AdminService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
|
||||||
pub async fn get_auth_profile(
|
|
||||||
&self,
|
|
||||||
workspace_id: &WorkspaceId,
|
|
||||||
auth_profile_id: &AuthProfileId,
|
|
||||||
) -> Result<AuthProfile, ApiError> {
|
|
||||||
self.registry
|
|
||||||
.get_auth_profile(workspace_id, auth_profile_id)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| {
|
|
||||||
ApiError::not_found_with_context(
|
|
||||||
format!("auth profile {} was not found", auth_profile_id.as_str()),
|
|
||||||
json!({ "auth_profile_id": auth_profile_id.as_str() }),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument(skip(self, payload), fields(auth_profile_name = %payload.name, auth_kind = ?payload.kind))]
|
|
||||||
pub async fn create_auth_profile(
|
|
||||||
&self,
|
|
||||||
workspace_id: &WorkspaceId,
|
|
||||||
payload: AuthProfilePayload,
|
|
||||||
) -> Result<AuthProfile, ApiError> {
|
|
||||||
validate_auth_profile_kind(payload.kind, &payload.config)?;
|
|
||||||
self.ensure_workspace_exists(workspace_id).await?;
|
|
||||||
self.validate_auth_profile_secret_ids(workspace_id, &payload.config)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let now = OffsetDateTime::now_utc();
|
|
||||||
let profile = AuthProfile {
|
|
||||||
id: AuthProfileId::new(new_prefixed_id("auth")),
|
|
||||||
workspace_id: workspace_id.clone(),
|
|
||||||
name: payload.name,
|
|
||||||
kind: payload.kind,
|
|
||||||
config: payload.config,
|
|
||||||
created_at: now,
|
|
||||||
updated_at: now,
|
|
||||||
};
|
|
||||||
|
|
||||||
self.registry
|
|
||||||
.save_auth_profile(SaveAuthProfileRequest {
|
|
||||||
workspace_id,
|
|
||||||
profile: &profile,
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
info!(auth_profile_id = %profile.id.as_str(), "auth profile created");
|
|
||||||
|
|
||||||
Ok(profile)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn validate_auth_profile_secret_ids(
|
|
||||||
&self,
|
|
||||||
workspace_id: &WorkspaceId,
|
|
||||||
config: &AuthConfig,
|
|
||||||
) -> Result<(), ApiError> {
|
|
||||||
for secret_id in config.secret_ids() {
|
|
||||||
self.get_secret(workspace_id, secret_id).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument(skip(self, payload), fields(operation_id = %operation_id.as_str(), sample_kind = ?sample_kind))]
|
#[instrument(skip(self, payload), fields(operation_id = %operation_id.as_str(), sample_kind = ?sample_kind))]
|
||||||
pub async fn save_json_sample(
|
pub async fn save_json_sample(
|
||||||
&self,
|
&self,
|
||||||
@@ -3022,22 +2820,6 @@ fn validate_protocol_target(protocol: Protocol, target: &Target) -> Result<(), A
|
|||||||
Err(ApiError::validation("protocol and target kind must match"))
|
Err(ApiError::validation("protocol and target kind must match"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_auth_profile_kind(kind: AuthKind, config: &AuthConfig) -> Result<(), ApiError> {
|
|
||||||
let is_match = matches!(
|
|
||||||
(kind, config),
|
|
||||||
(AuthKind::Bearer, AuthConfig::Bearer(_))
|
|
||||||
| (AuthKind::Basic, AuthConfig::Basic(_))
|
|
||||||
| (AuthKind::ApiKeyHeader, AuthConfig::ApiKeyHeader(_))
|
|
||||||
| (AuthKind::ApiKeyQuery, AuthConfig::ApiKeyQuery(_))
|
|
||||||
);
|
|
||||||
|
|
||||||
if is_match {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(ApiError::validation("auth kind and config must match"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_upstream_payload(payload: &UpstreamPayload) -> Result<(), ApiError> {
|
fn validate_upstream_payload(payload: &UpstreamPayload) -> Result<(), ApiError> {
|
||||||
if payload.name.trim().is_empty() {
|
if payload.name.trim().is_empty() {
|
||||||
return Err(ApiError::validation("upstream name is required"));
|
return Err(ApiError::validation("upstream name is required"));
|
||||||
@@ -3104,18 +2886,6 @@ fn validate_profile_email(value: &str) -> Result<String, ApiError> {
|
|||||||
Ok(email)
|
Ok(email)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_secret_payload(payload: &SecretPayload) -> Result<(), ApiError> {
|
|
||||||
if payload.name.trim().is_empty() {
|
|
||||||
return Err(ApiError::validation("secret name must not be empty"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if payload.value.is_null() {
|
|
||||||
return Err(ApiError::validation("secret value must not be null"));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn latest_sample_ref(
|
fn latest_sample_ref(
|
||||||
samples: &[OperationSampleMetadata],
|
samples: &[OperationSampleMetadata],
|
||||||
sample_kind: SampleKind,
|
sample_kind: SampleKind,
|
||||||
|
|||||||
@@ -0,0 +1,248 @@
|
|||||||
|
use crank_core::{
|
||||||
|
AuthConfig, AuthKind, AuthProfile, AuthProfileId, Secret, SecretId, SecretStatus, UserId,
|
||||||
|
WorkspaceId,
|
||||||
|
};
|
||||||
|
use crank_registry::{
|
||||||
|
CreateSecretRequest, RegistryError, RotateSecretRequest, SaveAuthProfileRequest,
|
||||||
|
};
|
||||||
|
use serde_json::json;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
use tracing::{info, instrument};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
error::ApiError,
|
||||||
|
service::{
|
||||||
|
AdminService, AuthProfilePayload, RotateSecretPayload, SecretPayload, new_prefixed_id,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
impl AdminService {
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub async fn list_auth_profiles(
|
||||||
|
&self,
|
||||||
|
workspace_id: &WorkspaceId,
|
||||||
|
) -> Result<Vec<AuthProfile>, ApiError> {
|
||||||
|
self.ensure_workspace_exists(workspace_id).await?;
|
||||||
|
Ok(self.registry.list_auth_profiles(workspace_id).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub async fn list_secrets(&self, workspace_id: &WorkspaceId) -> Result<Vec<Secret>, ApiError> {
|
||||||
|
self.ensure_workspace_exists(workspace_id).await?;
|
||||||
|
Ok(self
|
||||||
|
.registry
|
||||||
|
.list_secrets(workspace_id)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|record| record.secret)
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub async fn get_secret(
|
||||||
|
&self,
|
||||||
|
workspace_id: &WorkspaceId,
|
||||||
|
secret_id: &SecretId,
|
||||||
|
) -> Result<Secret, ApiError> {
|
||||||
|
self.ensure_workspace_exists(workspace_id).await?;
|
||||||
|
self.registry
|
||||||
|
.get_secret(workspace_id, secret_id)
|
||||||
|
.await?
|
||||||
|
.map(|record| record.secret)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ApiError::not_found_with_context(
|
||||||
|
format!("secret {} was not found", secret_id.as_str()),
|
||||||
|
json!({ "secret_id": secret_id.as_str() }),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self, payload), fields(workspace_id = %workspace_id.as_str(), secret_name = %payload.name))]
|
||||||
|
pub async fn create_secret(
|
||||||
|
&self,
|
||||||
|
workspace_id: &WorkspaceId,
|
||||||
|
created_by: Option<&UserId>,
|
||||||
|
payload: SecretPayload,
|
||||||
|
) -> Result<Secret, ApiError> {
|
||||||
|
self.ensure_workspace_exists(workspace_id).await?;
|
||||||
|
validate_secret_payload(&payload)?;
|
||||||
|
|
||||||
|
let now = OffsetDateTime::now_utc();
|
||||||
|
let secret = Secret {
|
||||||
|
id: SecretId::new(new_prefixed_id("secret")),
|
||||||
|
workspace_id: workspace_id.clone(),
|
||||||
|
name: payload.name.trim().to_owned(),
|
||||||
|
kind: payload.kind,
|
||||||
|
status: SecretStatus::Active,
|
||||||
|
current_version: 1,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
last_used_at: None,
|
||||||
|
};
|
||||||
|
let ciphertext = self
|
||||||
|
.secret_crypto
|
||||||
|
.encrypt(&payload.value)
|
||||||
|
.map_err(|error| ApiError::internal(error.to_string()))?;
|
||||||
|
|
||||||
|
self.registry
|
||||||
|
.create_secret(CreateSecretRequest {
|
||||||
|
secret: &secret,
|
||||||
|
ciphertext: &ciphertext,
|
||||||
|
key_version: self.secret_crypto.key_version(),
|
||||||
|
created_by,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
info!(secret_id = %secret.id.as_str(), "secret created");
|
||||||
|
|
||||||
|
Ok(secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self, payload), fields(workspace_id = %workspace_id.as_str(), secret_id = %secret_id.as_str()))]
|
||||||
|
pub async fn rotate_secret(
|
||||||
|
&self,
|
||||||
|
workspace_id: &WorkspaceId,
|
||||||
|
secret_id: &SecretId,
|
||||||
|
created_by: Option<&UserId>,
|
||||||
|
payload: RotateSecretPayload,
|
||||||
|
) -> Result<Secret, ApiError> {
|
||||||
|
self.ensure_workspace_exists(workspace_id).await?;
|
||||||
|
if payload.value.is_null() {
|
||||||
|
return Err(ApiError::validation("secret value must not be null"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let now = OffsetDateTime::now_utc();
|
||||||
|
let ciphertext = self
|
||||||
|
.secret_crypto
|
||||||
|
.encrypt(&payload.value)
|
||||||
|
.map_err(|error| ApiError::internal(error.to_string()))?;
|
||||||
|
self.registry
|
||||||
|
.rotate_secret(RotateSecretRequest {
|
||||||
|
workspace_id,
|
||||||
|
secret_id,
|
||||||
|
ciphertext: &ciphertext,
|
||||||
|
key_version: self.secret_crypto.key_version(),
|
||||||
|
created_at: &now,
|
||||||
|
updated_at: &now,
|
||||||
|
created_by,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
info!(secret_id = %secret_id.as_str(), "secret rotated");
|
||||||
|
|
||||||
|
self.get_secret(workspace_id, secret_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self), fields(workspace_id = %workspace_id.as_str(), secret_id = %secret_id.as_str()))]
|
||||||
|
pub async fn delete_secret(
|
||||||
|
&self,
|
||||||
|
workspace_id: &WorkspaceId,
|
||||||
|
secret_id: &SecretId,
|
||||||
|
) -> Result<(), ApiError> {
|
||||||
|
self.ensure_workspace_exists(workspace_id).await?;
|
||||||
|
if let Some(profile) = self
|
||||||
|
.registry
|
||||||
|
.list_auth_profiles_referencing_secret(workspace_id, secret_id)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
{
|
||||||
|
return Err(RegistryError::SecretReferencedByAuthProfile {
|
||||||
|
secret_id: secret_id.as_str().to_owned(),
|
||||||
|
auth_profile_id: profile.id.as_str().to_owned(),
|
||||||
|
}
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
self.registry.delete_secret(workspace_id, secret_id).await?;
|
||||||
|
info!(secret_id = %secret_id.as_str(), "secret deleted");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub async fn get_auth_profile(
|
||||||
|
&self,
|
||||||
|
workspace_id: &WorkspaceId,
|
||||||
|
auth_profile_id: &AuthProfileId,
|
||||||
|
) -> Result<AuthProfile, ApiError> {
|
||||||
|
self.registry
|
||||||
|
.get_auth_profile(workspace_id, auth_profile_id)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ApiError::not_found_with_context(
|
||||||
|
format!("auth profile {} was not found", auth_profile_id.as_str()),
|
||||||
|
json!({ "auth_profile_id": auth_profile_id.as_str() }),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self, payload), fields(auth_profile_name = %payload.name, auth_kind = ?payload.kind))]
|
||||||
|
pub async fn create_auth_profile(
|
||||||
|
&self,
|
||||||
|
workspace_id: &WorkspaceId,
|
||||||
|
payload: AuthProfilePayload,
|
||||||
|
) -> Result<AuthProfile, ApiError> {
|
||||||
|
validate_auth_profile_kind(payload.kind, &payload.config)?;
|
||||||
|
self.ensure_workspace_exists(workspace_id).await?;
|
||||||
|
self.validate_auth_profile_secret_ids(workspace_id, &payload.config)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let now = OffsetDateTime::now_utc();
|
||||||
|
let profile = AuthProfile {
|
||||||
|
id: AuthProfileId::new(new_prefixed_id("auth")),
|
||||||
|
workspace_id: workspace_id.clone(),
|
||||||
|
name: payload.name,
|
||||||
|
kind: payload.kind,
|
||||||
|
config: payload.config,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.registry
|
||||||
|
.save_auth_profile(SaveAuthProfileRequest {
|
||||||
|
workspace_id,
|
||||||
|
profile: &profile,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
info!(auth_profile_id = %profile.id.as_str(), "auth profile created");
|
||||||
|
|
||||||
|
Ok(profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn validate_auth_profile_secret_ids(
|
||||||
|
&self,
|
||||||
|
workspace_id: &WorkspaceId,
|
||||||
|
config: &AuthConfig,
|
||||||
|
) -> Result<(), ApiError> {
|
||||||
|
for secret_id in config.secret_ids() {
|
||||||
|
self.get_secret(workspace_id, secret_id).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_auth_profile_kind(kind: AuthKind, config: &AuthConfig) -> Result<(), ApiError> {
|
||||||
|
let is_match = matches!(
|
||||||
|
(kind, config),
|
||||||
|
(AuthKind::Bearer, AuthConfig::Bearer(_))
|
||||||
|
| (AuthKind::Basic, AuthConfig::Basic(_))
|
||||||
|
| (AuthKind::ApiKeyHeader, AuthConfig::ApiKeyHeader(_))
|
||||||
|
| (AuthKind::ApiKeyQuery, AuthConfig::ApiKeyQuery(_))
|
||||||
|
);
|
||||||
|
|
||||||
|
if is_match {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(ApiError::validation("auth kind and config must match"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_secret_payload(payload: &SecretPayload) -> Result<(), ApiError> {
|
||||||
|
if payload.name.trim().is_empty() {
|
||||||
|
return Err(ApiError::validation("secret name must not be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.value.is_null() {
|
||||||
|
return Err(ApiError::validation("secret value must not be null"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user