use crank_core::{ AgentId, PlatformApiKey, PlatformApiKeyId, PlatformApiKeyKind, PlatformApiKeyScope, PlatformApiKeyStatus, WorkspaceId, }; use crank_registry::{CreatePlatformApiKeyRequest, PlatformApiKeyRecord}; use serde_json::json; use time::{OffsetDateTime, format_description::well_known::Rfc3339}; use tracing::instrument; use crate::{ error::ApiError, service::{ AdminService, CreatedPlatformApiKeyResponse, PlatformApiKeyPayload, generate_access_secret, hash_access_secret, new_prefixed_id, }, }; impl AdminService { #[instrument(skip(self))] pub async fn list_agent_platform_api_keys( &self, workspace_id: &WorkspaceId, agent_id: &AgentId, ) -> Result, ApiError> { self.ensure_workspace_exists(workspace_id).await?; self.registry .get_agent_summary(workspace_id, agent_id) .await? .ok_or_else(|| { ApiError::not_found_with_context( format!("agent {} was not found", agent_id.as_str()), json!({ "agent_id": agent_id.as_str() }), ) })?; Ok(self .registry .list_platform_api_keys_for_agent(workspace_id, agent_id) .await?) } #[instrument(skip(self, payload), fields(workspace_id = %workspace_id.as_str(), agent_id = %agent_id.as_str(), key_name = %payload.name))] pub async fn create_agent_platform_api_key( &self, workspace_id: &WorkspaceId, agent_id: &AgentId, payload: PlatformApiKeyPayload, ) -> Result { self.ensure_workspace_exists(workspace_id).await?; self.registry .get_agent_summary(workspace_id, agent_id) .await? .ok_or_else(|| { ApiError::not_found_with_context( format!("agent {} was not found", agent_id.as_str()), json!({ "agent_id": agent_id.as_str() }), ) })?; validate_platform_api_key_payload(&payload)?; let expires_at = match payload.expires_at.as_deref() { Some(value) => Some( OffsetDateTime::parse(value, &Rfc3339) .map_err(|_| ApiError::validation("expires_at must be RFC3339 timestamp"))?, ), None => None, }; let secret = generate_access_secret(payload.key_kind.secret_marker()); let api_key = PlatformApiKeyRecord { api_key: PlatformApiKey { id: PlatformApiKeyId::new(new_prefixed_id("pk")), workspace_id: workspace_id.clone(), agent_id: Some(agent_id.clone()), name: payload.name, prefix: secret.chars().take(16).collect(), key_kind: payload.key_kind, scopes: payload.scopes, status: PlatformApiKeyStatus::Active, created_at: OffsetDateTime::now_utc(), last_used_at: None, expires_at, allowed_origins: payload.allowed_origins, }, }; self.registry .create_platform_api_key(CreatePlatformApiKeyRequest { api_key: &api_key.api_key, secret_hash: &hash_access_secret(&secret), }) .await?; Ok(CreatedPlatformApiKeyResponse { api_key, secret }) } #[instrument(skip(self), fields(workspace_id = %workspace_id.as_str(), agent_id = %agent_id.as_str(), key_id = %key_id.as_str()))] pub async fn revoke_agent_platform_api_key( &self, workspace_id: &WorkspaceId, agent_id: &AgentId, key_id: &PlatformApiKeyId, ) -> Result<(), ApiError> { self.registry .revoke_platform_api_key_for_agent( workspace_id, agent_id, key_id, &OffsetDateTime::now_utc(), ) .await?; Ok(()) } #[instrument(skip(self), fields(workspace_id = %workspace_id.as_str(), agent_id = %agent_id.as_str(), key_id = %key_id.as_str()))] pub async fn delete_agent_platform_api_key( &self, workspace_id: &WorkspaceId, agent_id: &AgentId, key_id: &PlatformApiKeyId, ) -> Result<(), ApiError> { self.registry .delete_platform_api_key_for_agent(workspace_id, agent_id, key_id) .await?; Ok(()) } } fn validate_platform_api_key_payload(payload: &PlatformApiKeyPayload) -> Result<(), ApiError> { if payload.name.trim().is_empty() { return Err(ApiError::validation("key name is required")); } if payload.scopes.is_empty() { return Err(ApiError::validation("at least one key scope is required")); } let valid = payload.scopes.iter().all(|scope| match payload.key_kind { PlatformApiKeyKind::McpClient => matches!( scope, PlatformApiKeyScope::Read | PlatformApiKeyScope::Write | PlatformApiKeyScope::Deploy ), PlatformApiKeyKind::Approval => matches!( scope, PlatformApiKeyScope::Approve | PlatformApiKeyScope::Deny | PlatformApiKeyScope::ReadPending ), }); if !valid { return Err(ApiError::validation( "key scopes do not match selected key kind", )); } if payload.key_kind == PlatformApiKeyKind::Approval && payload.allowed_origins.len() > 20 { return Err(ApiError::validation( "approval key can contain at most 20 allowed origins", )); } Ok(()) }