Split admin secrets service module
Deploy / deploy (push) Successful in 1m37s
CI / Rust Checks (push) Failing after 5m7s
CI / Frontend E2E (push) Has been skipped
CI / Deployment Manifests (push) Has been skipped
CI / UI Checks (push) Has been skipped

This commit is contained in:
github-ops
2026-06-21 01:57:57 +00:00
parent f1fcfdef3d
commit f931d9b51f
2 changed files with 257 additions and 239 deletions
+248
View File
@@ -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(())
}