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, 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, 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 { 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 { 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!( name: "admin.secret.created", 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 { 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!( name: "admin.secret.rotated", 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!( name: "admin.secret.deleted", 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 { 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 { 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!( name: "admin.auth_profile.created", 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(()) }