Split MCP and approval agent keys
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
use crank_core::{AgentId, PlatformApiKey, PlatformApiKeyId, PlatformApiKeyStatus, WorkspaceId};
|
||||
use crank_core::{
|
||||
AgentId, PlatformApiKey, PlatformApiKeyId, PlatformApiKeyKind, PlatformApiKeyScope,
|
||||
PlatformApiKeyStatus, WorkspaceId,
|
||||
};
|
||||
use crank_registry::{CreatePlatformApiKeyRequest, PlatformApiKeyRecord};
|
||||
use serde_json::json;
|
||||
use time::OffsetDateTime;
|
||||
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::{
|
||||
@@ -53,7 +56,19 @@ impl AdminService {
|
||||
)
|
||||
})?;
|
||||
|
||||
let secret = generate_access_secret("crk");
|
||||
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(match payload.key_kind {
|
||||
PlatformApiKeyKind::McpClient => "crk",
|
||||
PlatformApiKeyKind::Approval => "crk_appr",
|
||||
});
|
||||
let api_key = PlatformApiKeyRecord {
|
||||
api_key: PlatformApiKey {
|
||||
id: PlatformApiKeyId::new(new_prefixed_id("pk")),
|
||||
@@ -61,10 +76,13 @@ impl AdminService {
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -109,3 +127,38 @@ impl AdminService {
|
||||
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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user