162 lines
5.6 KiB
Rust
162 lines
5.6 KiB
Rust
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<Vec<PlatformApiKeyRecord>, 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<CreatedPlatformApiKeyResponse, 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() }),
|
|
)
|
|
})?;
|
|
|
|
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(())
|
|
}
|