auth: cut over community machine access to agent keys

This commit is contained in:
a.tolmachev
2026-05-03 15:42:07 +00:00
parent c7b33930db
commit 6fd62df2a8
18 changed files with 768 additions and 302 deletions
+5 -1
View File
@@ -1813,7 +1813,11 @@ async fn require_platform_api_key(
let secret_hash = hash_access_secret(secret);
let Some(api_key) = state
.registry
.get_platform_api_key_by_secret_for_workspace_slug(&path.workspace_slug, &secret_hash)
.get_platform_api_key_by_secret_for_agent_slug(
&path.workspace_slug,
&path.agent_slug,
&secret_hash,
)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
else {
+160 -25
View File
@@ -208,6 +208,10 @@ mod tests {
"default"
}
fn test_agent_id(agent_slug: &str) -> AgentId {
AgentId::new(format!("agent_{agent_slug}"))
}
fn build_test_app(
registry: PostgresRegistry,
refresh_interval: Duration,
@@ -277,6 +281,7 @@ mod tests {
publish_agent_for_operation(&registry, &operation, "sales-rest").await;
let api_key = create_platform_api_key(
&registry,
"sales-rest",
"mcp-rest",
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
)
@@ -383,6 +388,7 @@ mod tests {
publish_agent_for_operation(&registry, &operation, "sales-request-id").await;
let api_key = create_platform_api_key(
&registry,
"sales-request-id",
"mcp-request-id",
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
)
@@ -466,6 +472,7 @@ mod tests {
publish_agent_for_operation(&registry, &operation, "sales-generated-request-id").await;
let api_key = create_platform_api_key(
&registry,
"sales-generated-request-id",
"mcp-generated-request-id",
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
)
@@ -553,6 +560,7 @@ mod tests {
publish_agent_for_operation(&registry, &operation, "sales-request-trace").await;
let api_key = create_platform_api_key(
&registry,
"sales-request-trace",
"mcp-request-trace",
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
)
@@ -633,6 +641,7 @@ mod tests {
publish_agent_for_operation(&registry, &operation, "sales-graphql").await;
let api_key = create_platform_api_key(
&registry,
"sales-graphql",
"mcp-graphql",
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
)
@@ -697,6 +706,7 @@ mod tests {
publish_agent_for_operation(&registry, &operation, "sales-grpc").await;
let api_key = create_platform_api_key(
&registry,
"sales-grpc",
"mcp-grpc",
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
)
@@ -742,8 +752,13 @@ mod tests {
async fn requires_initialized_notification_before_tool_methods() {
let registry = test_registry().await;
publish_agent_with_bindings(&registry, "sales-init", vec![]).await;
let api_key =
create_platform_api_key(&registry, "mcp-init", &[PlatformApiKeyScope::Read]).await;
let api_key = create_platform_api_key(
&registry,
"sales-init",
"mcp-init",
&[PlatformApiKeyScope::Read],
)
.await;
let base_url = spawn_mcp_server(build_test_app(
registry,
Duration::from_millis(0),
@@ -802,6 +817,7 @@ mod tests {
publish_agent_with_bindings(&registry, "sales-sse-init", vec![]).await;
let api_key = create_platform_api_key(
&registry,
"sales-sse-init",
"mcp-sse-init",
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
)
@@ -852,6 +868,7 @@ mod tests {
publish_agent_with_bindings(&registry, "sales-get-sse", vec![]).await;
let api_key = create_platform_api_key(
&registry,
"sales-get-sse",
"mcp-get-sse",
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
)
@@ -903,6 +920,7 @@ mod tests {
publish_agent_with_bindings(&registry, "sales-expired-session", vec![]).await;
let api_key = create_platform_api_key(
&registry,
"sales-expired-session",
"mcp-expired-session",
&[PlatformApiKeyScope::Read],
)
@@ -954,6 +972,7 @@ mod tests {
publish_agent_with_bindings(&registry, "sales-get-rate-limit", vec![]).await;
let api_key = create_platform_api_key(
&registry,
"sales-get-rate-limit",
"mcp-get-rate-limit",
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
)
@@ -1003,6 +1022,7 @@ mod tests {
publish_agent_with_bindings(&registry, "sales-get-sse-missing", vec![]).await;
let api_key = create_platform_api_key(
&registry,
"sales-get-sse-missing",
"mcp-get-sse-missing",
&[PlatformApiKeyScope::Read],
)
@@ -1032,6 +1052,7 @@ mod tests {
publish_agent_with_bindings(&registry, "sales-delete-session", vec![]).await;
let api_key = create_platform_api_key(
&registry,
"sales-delete-session",
"mcp-delete-session",
&[PlatformApiKeyScope::Read],
)
@@ -1074,9 +1095,13 @@ mod tests {
async fn initialize_accepts_json_only_response_negotiation() {
let registry = test_registry().await;
publish_agent_with_bindings(&registry, "sales-json-accept", vec![]).await;
let api_key =
create_platform_api_key(&registry, "mcp-json-accept", &[PlatformApiKeyScope::Read])
.await;
let api_key = create_platform_api_key(
&registry,
"sales-json-accept",
"mcp-json-accept",
&[PlatformApiKeyScope::Read],
)
.await;
let base_url = spawn_mcp_server(build_test_app(
registry,
Duration::from_millis(0),
@@ -1117,6 +1142,7 @@ mod tests {
publish_agent_with_bindings(&registry, "sales-get-bad-version", vec![]).await;
let api_key = create_platform_api_key(
&registry,
"sales-get-bad-version",
"mcp-get-bad-version",
&[PlatformApiKeyScope::Read],
)
@@ -1158,6 +1184,7 @@ mod tests {
let mcp_url = agent_mcp_url(&base_url, "sales-refresh");
let api_key = create_platform_api_key(
&registry,
"sales-refresh",
"mcp-refresh",
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
)
@@ -1252,9 +1279,17 @@ mod tests {
vec![binding_for_operation(&operation_b)],
)
.await;
let api_key = create_platform_api_key(
let api_key_a = create_platform_api_key(
&registry,
"mcp-agents",
"sales-a",
"mcp-agent-a",
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
)
.await;
let api_key_b = create_platform_api_key(
&registry,
"sales-b",
"mcp-agent-b",
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
)
.await;
@@ -1268,13 +1303,13 @@ mod tests {
let client = reqwest::Client::new();
let agent_a_url = agent_mcp_url(&base_url, "sales-a");
let agent_b_url = agent_mcp_url(&base_url, "sales-b");
let session_a = initialize_session(&client, &agent_a_url, &api_key).await;
let session_b = initialize_session(&client, &agent_b_url, &api_key).await;
let session_a = initialize_session(&client, &agent_a_url, &api_key_a).await;
let session_b = initialize_session(&client, &agent_b_url, &api_key_b).await;
let tools_a = post_jsonrpc(
&client,
&agent_a_url,
&api_key,
&api_key_a,
Some(&session_a),
json!({
"jsonrpc": "2.0",
@@ -1287,7 +1322,7 @@ mod tests {
let tools_b = post_jsonrpc(
&client,
&agent_b_url,
&api_key,
&api_key_b,
Some(&session_b),
json!({
"jsonrpc": "2.0",
@@ -1302,6 +1337,76 @@ mod tests {
assert_eq!(tools_b["result"]["tools"][0]["name"], "crm_update_lead");
}
#[tokio::test]
async fn rejects_initialize_with_key_from_different_agent() {
let registry = test_registry().await;
let upstream_base_url = spawn_upstream_server().await;
let operation_a = test_operation(&upstream_base_url, "crm_create_lead");
let operation_b = test_operation(&upstream_base_url, "crm_update_lead");
for operation in [&operation_a, &operation_b] {
registry
.create_operation(&test_workspace_id(), operation, Some("alice"))
.await
.unwrap();
registry
.publish_operation(PublishRequest {
workspace_id: &test_workspace_id(),
operation_id: &operation.id,
version: 1,
published_at: &OffsetDateTime::parse("2026-03-26T10:00:00Z", &Rfc3339).unwrap(),
published_by: Some("alice"),
})
.await
.unwrap();
}
publish_agent_with_bindings(
&registry,
"sales-a",
vec![binding_for_operation(&operation_a)],
)
.await;
publish_agent_with_bindings(
&registry,
"sales-b",
vec![binding_for_operation(&operation_b)],
)
.await;
let api_key_a = create_platform_api_key(
&registry,
"sales-a",
"mcp-agent-a-only",
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
)
.await;
let base_url = spawn_mcp_server(build_test_app(
registry,
Duration::from_millis(0),
Some("https://crank.example.com".to_owned()),
))
.await;
let client = reqwest::Client::new();
let response = client
.post(agent_mcp_url(&base_url, "sales-b"))
.header(header::ACCEPT, "application/json, text/event-stream")
.header(header::AUTHORIZATION, format!("Bearer {api_key_a}"))
.json(&json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-11-25"
}
}))
.send()
.await
.unwrap();
assert_eq!(response.status(), reqwest::StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn rejects_initialize_without_platform_api_key() {
let registry = test_registry().await;
@@ -1352,8 +1457,13 @@ mod tests {
.await
.unwrap();
publish_agent_for_operation(&registry, &operation, "sales-read-only").await;
let api_key =
create_platform_api_key(&registry, "mcp-read", &[PlatformApiKeyScope::Read]).await;
let api_key = create_platform_api_key(
&registry,
"sales-read-only",
"mcp-read",
&[PlatformApiKeyScope::Read],
)
.await;
let base_url = spawn_mcp_server(build_test_app(
registry,
@@ -1411,6 +1521,7 @@ mod tests {
publish_agent_for_operation(&registry, &operation, "sales-session").await;
let api_key = create_platform_api_key(
&registry,
"sales-session",
"mcp-session",
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
)
@@ -1558,9 +1669,17 @@ mod tests {
publish_agent_for_operation(&registry, &operation_a, "sales-session-a").await;
publish_agent_for_operation(&registry, &operation_b, "sales-session-b").await;
let api_key = create_platform_api_key(
let api_key_a = create_platform_api_key(
&registry,
"mcp-cross-session",
"sales-session-a",
"mcp-cross-session-a",
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
)
.await;
let api_key_b = create_platform_api_key(
&registry,
"sales-session-b",
"mcp-cross-session-b",
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
)
.await;
@@ -1574,13 +1693,13 @@ mod tests {
let client = reqwest::Client::new();
let agent_a_url = agent_mcp_url(&base_url, "sales-session-a");
let agent_b_url = agent_mcp_url(&base_url, "sales-session-b");
let session_a = initialize_session(&client, &agent_a_url, &api_key).await;
let session_b = initialize_session(&client, &agent_b_url, &api_key).await;
let session_a = initialize_session(&client, &agent_a_url, &api_key_a).await;
let session_b = initialize_session(&client, &agent_b_url, &api_key_b).await;
let start_response = post_jsonrpc(
&client,
&agent_a_url,
&api_key,
&api_key_a,
Some(&session_a),
json!({
"jsonrpc": "2.0",
@@ -1601,7 +1720,7 @@ mod tests {
let poll_response = post_jsonrpc(
&client,
&agent_b_url,
&api_key,
&api_key_b,
Some(&session_b),
json!({
"jsonrpc": "2.0",
@@ -1650,6 +1769,7 @@ mod tests {
publish_agent_for_operation(&registry, &operation, "sales-session-rate").await;
let api_key = create_platform_api_key(
&registry,
"sales-session-rate",
"mcp-session-rate",
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
)
@@ -1755,6 +1875,7 @@ mod tests {
publish_agent_for_operation(&registry, &operation, "sales-async-rate").await;
let api_key = create_platform_api_key(
&registry,
"sales-async-rate",
"mcp-async-rate",
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
)
@@ -1866,6 +1987,7 @@ mod tests {
publish_agent_for_operation(&registry, &operation, "sales-async-completed").await;
let api_key = create_platform_api_key(
&registry,
"sales-async-completed",
"mcp-async-completed",
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
)
@@ -1951,6 +2073,7 @@ mod tests {
publish_agent_for_operation(&registry, &operation, "sales-rate-limited").await;
let api_key = create_platform_api_key(
&registry,
"sales-rate-limited",
"mcp-rate-limit",
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
)
@@ -2050,9 +2173,17 @@ mod tests {
publish_agent_for_operation(&registry, &operation_a, "sales-async-a").await;
publish_agent_for_operation(&registry, &operation_b, "sales-async-b").await;
let api_key = create_platform_api_key(
let api_key_a = create_platform_api_key(
&registry,
"mcp-cross-async",
"sales-async-a",
"mcp-cross-async-a",
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
)
.await;
let api_key_b = create_platform_api_key(
&registry,
"sales-async-b",
"mcp-cross-async-b",
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
)
.await;
@@ -2066,13 +2197,13 @@ mod tests {
let client = reqwest::Client::new();
let agent_a_url = agent_mcp_url(&base_url, "sales-async-a");
let agent_b_url = agent_mcp_url(&base_url, "sales-async-b");
let session_a = initialize_session(&client, &agent_a_url, &api_key).await;
let session_b = initialize_session(&client, &agent_b_url, &api_key).await;
let session_a = initialize_session(&client, &agent_a_url, &api_key_a).await;
let session_b = initialize_session(&client, &agent_b_url, &api_key_b).await;
let start_response = post_jsonrpc(
&client,
&agent_a_url,
&api_key,
&api_key_a,
Some(&session_a),
json!({
"jsonrpc": "2.0",
@@ -2093,7 +2224,7 @@ mod tests {
let status_response = post_jsonrpc(
&client,
&agent_b_url,
&api_key,
&api_key_b,
Some(&session_b),
json!({
"jsonrpc": "2.0",
@@ -2141,6 +2272,7 @@ mod tests {
publish_agent_for_operation(&registry, &operation, "sales-async").await;
let api_key = create_platform_api_key(
&registry,
"sales-async",
"mcp-async",
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
)
@@ -2290,6 +2422,7 @@ mod tests {
publish_agent_for_operation(&registry, &operation, "sales-window").await;
let api_key = create_platform_api_key(
&registry,
"sales-window",
"mcp-window",
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
)
@@ -2418,6 +2551,7 @@ mod tests {
async fn create_platform_api_key(
registry: &PostgresRegistry,
agent_slug: &str,
name: &str,
scopes: &[PlatformApiKeyScope],
) -> String {
@@ -2425,6 +2559,7 @@ mod tests {
let api_key = PlatformApiKey {
id: PlatformApiKeyId::new(format!("pk_{name}")),
workspace_id: test_workspace_id(),
agent_id: Some(test_agent_id(agent_slug)),
name: name.to_owned(),
prefix: secret.chars().take(16).collect(),
scopes: scopes.to_vec(),