Split MCP and approval agent keys

This commit is contained in:
github-ops
2026-06-24 10:31:52 +00:00
parent d739f17393
commit 5922aea68f
16 changed files with 471 additions and 56 deletions
+12 -2
View File
@@ -1,8 +1,8 @@
use crank_core::{
AgentId, AgentStatus, AuthConfig, AuthKind, ExecutionMode, ExportMode, GeneratedDraft,
InvocationLevel, InvocationSource, InvocationStatus, OperationSecurityLevel, OperationStatus,
PlatformApiKeyScope, Protocol, SecretKind, Target, UsagePeriod, WizardState, WorkspaceId,
WorkspaceStatus,
PlatformApiKeyKind, PlatformApiKeyScope, Protocol, SecretKind, Target, UsagePeriod,
WizardState, WorkspaceId, WorkspaceStatus,
};
use crank_mapping::MappingSet;
use crank_registry::{
@@ -211,7 +211,17 @@ pub struct AgentMutationResult {
#[derive(Clone, Debug, Deserialize)]
pub struct PlatformApiKeyPayload {
pub name: String,
#[serde(default = "default_platform_api_key_kind")]
pub key_kind: PlatformApiKeyKind,
pub scopes: Vec<PlatformApiKeyScope>,
#[serde(default)]
pub expires_at: Option<String>,
#[serde(default)]
pub allowed_origins: Vec<String>,
}
fn default_platform_api_key_kind() -> PlatformApiKeyKind {
PlatformApiKeyKind::McpClient
}
#[derive(Clone, Debug, Serialize)]
+56 -3
View File
@@ -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(())
}
+5 -2
View File
@@ -2,8 +2,8 @@ use std::collections::BTreeMap;
use crank_core::{
AgentId, InvocationLevel, InvocationSource, InvocationStatus, MembershipRole, OperationId,
OperationSecurityLevel, PlatformApiKeyScope, PlatformApiKeyStatus, Protocol, Target,
WizardState, WorkspaceId,
OperationSecurityLevel, PlatformApiKeyKind, PlatformApiKeyScope, PlatformApiKeyStatus,
Protocol, Target, WizardState, WorkspaceId,
};
use crank_mapping::{JsonPathRoot, infer_mapping_from_samples};
use crank_mapping::{MappingRule, MappingSet};
@@ -162,7 +162,10 @@ impl AdminService {
agent_id,
PlatformApiKeyPayload {
name: name.to_owned(),
key_kind: PlatformApiKeyKind::McpClient,
scopes,
expires_at: None,
allowed_origins: Vec::new(),
},
)
.await?