auth: cut over community machine access to agent keys
This commit is contained in:
@@ -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
@@ -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(®istry, &operation, "sales-rest").await;
|
||||
let api_key = create_platform_api_key(
|
||||
®istry,
|
||||
"sales-rest",
|
||||
"mcp-rest",
|
||||
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
|
||||
)
|
||||
@@ -383,6 +388,7 @@ mod tests {
|
||||
publish_agent_for_operation(®istry, &operation, "sales-request-id").await;
|
||||
let api_key = create_platform_api_key(
|
||||
®istry,
|
||||
"sales-request-id",
|
||||
"mcp-request-id",
|
||||
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
|
||||
)
|
||||
@@ -466,6 +472,7 @@ mod tests {
|
||||
publish_agent_for_operation(®istry, &operation, "sales-generated-request-id").await;
|
||||
let api_key = create_platform_api_key(
|
||||
®istry,
|
||||
"sales-generated-request-id",
|
||||
"mcp-generated-request-id",
|
||||
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
|
||||
)
|
||||
@@ -553,6 +560,7 @@ mod tests {
|
||||
publish_agent_for_operation(®istry, &operation, "sales-request-trace").await;
|
||||
let api_key = create_platform_api_key(
|
||||
®istry,
|
||||
"sales-request-trace",
|
||||
"mcp-request-trace",
|
||||
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
|
||||
)
|
||||
@@ -633,6 +641,7 @@ mod tests {
|
||||
publish_agent_for_operation(®istry, &operation, "sales-graphql").await;
|
||||
let api_key = create_platform_api_key(
|
||||
®istry,
|
||||
"sales-graphql",
|
||||
"mcp-graphql",
|
||||
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
|
||||
)
|
||||
@@ -697,6 +706,7 @@ mod tests {
|
||||
publish_agent_for_operation(®istry, &operation, "sales-grpc").await;
|
||||
let api_key = create_platform_api_key(
|
||||
®istry,
|
||||
"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(®istry, "sales-init", vec![]).await;
|
||||
let api_key =
|
||||
create_platform_api_key(®istry, "mcp-init", &[PlatformApiKeyScope::Read]).await;
|
||||
let api_key = create_platform_api_key(
|
||||
®istry,
|
||||
"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(®istry, "sales-sse-init", vec![]).await;
|
||||
let api_key = create_platform_api_key(
|
||||
®istry,
|
||||
"sales-sse-init",
|
||||
"mcp-sse-init",
|
||||
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
|
||||
)
|
||||
@@ -852,6 +868,7 @@ mod tests {
|
||||
publish_agent_with_bindings(®istry, "sales-get-sse", vec![]).await;
|
||||
let api_key = create_platform_api_key(
|
||||
®istry,
|
||||
"sales-get-sse",
|
||||
"mcp-get-sse",
|
||||
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
|
||||
)
|
||||
@@ -903,6 +920,7 @@ mod tests {
|
||||
publish_agent_with_bindings(®istry, "sales-expired-session", vec![]).await;
|
||||
let api_key = create_platform_api_key(
|
||||
®istry,
|
||||
"sales-expired-session",
|
||||
"mcp-expired-session",
|
||||
&[PlatformApiKeyScope::Read],
|
||||
)
|
||||
@@ -954,6 +972,7 @@ mod tests {
|
||||
publish_agent_with_bindings(®istry, "sales-get-rate-limit", vec![]).await;
|
||||
let api_key = create_platform_api_key(
|
||||
®istry,
|
||||
"sales-get-rate-limit",
|
||||
"mcp-get-rate-limit",
|
||||
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
|
||||
)
|
||||
@@ -1003,6 +1022,7 @@ mod tests {
|
||||
publish_agent_with_bindings(®istry, "sales-get-sse-missing", vec![]).await;
|
||||
let api_key = create_platform_api_key(
|
||||
®istry,
|
||||
"sales-get-sse-missing",
|
||||
"mcp-get-sse-missing",
|
||||
&[PlatformApiKeyScope::Read],
|
||||
)
|
||||
@@ -1032,6 +1052,7 @@ mod tests {
|
||||
publish_agent_with_bindings(®istry, "sales-delete-session", vec![]).await;
|
||||
let api_key = create_platform_api_key(
|
||||
®istry,
|
||||
"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(®istry, "sales-json-accept", vec![]).await;
|
||||
let api_key =
|
||||
create_platform_api_key(®istry, "mcp-json-accept", &[PlatformApiKeyScope::Read])
|
||||
.await;
|
||||
let api_key = create_platform_api_key(
|
||||
®istry,
|
||||
"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(®istry, "sales-get-bad-version", vec![]).await;
|
||||
let api_key = create_platform_api_key(
|
||||
®istry,
|
||||
"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(
|
||||
®istry,
|
||||
"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(
|
||||
®istry,
|
||||
"mcp-agents",
|
||||
"sales-a",
|
||||
"mcp-agent-a",
|
||||
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
|
||||
)
|
||||
.await;
|
||||
let api_key_b = create_platform_api_key(
|
||||
®istry,
|
||||
"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(
|
||||
®istry,
|
||||
"sales-a",
|
||||
vec![binding_for_operation(&operation_a)],
|
||||
)
|
||||
.await;
|
||||
publish_agent_with_bindings(
|
||||
®istry,
|
||||
"sales-b",
|
||||
vec![binding_for_operation(&operation_b)],
|
||||
)
|
||||
.await;
|
||||
let api_key_a = create_platform_api_key(
|
||||
®istry,
|
||||
"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(®istry, &operation, "sales-read-only").await;
|
||||
let api_key =
|
||||
create_platform_api_key(®istry, "mcp-read", &[PlatformApiKeyScope::Read]).await;
|
||||
let api_key = create_platform_api_key(
|
||||
®istry,
|
||||
"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(®istry, &operation, "sales-session").await;
|
||||
let api_key = create_platform_api_key(
|
||||
®istry,
|
||||
"sales-session",
|
||||
"mcp-session",
|
||||
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
|
||||
)
|
||||
@@ -1558,9 +1669,17 @@ mod tests {
|
||||
|
||||
publish_agent_for_operation(®istry, &operation_a, "sales-session-a").await;
|
||||
publish_agent_for_operation(®istry, &operation_b, "sales-session-b").await;
|
||||
let api_key = create_platform_api_key(
|
||||
let api_key_a = create_platform_api_key(
|
||||
®istry,
|
||||
"mcp-cross-session",
|
||||
"sales-session-a",
|
||||
"mcp-cross-session-a",
|
||||
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
|
||||
)
|
||||
.await;
|
||||
let api_key_b = create_platform_api_key(
|
||||
®istry,
|
||||
"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(®istry, &operation, "sales-session-rate").await;
|
||||
let api_key = create_platform_api_key(
|
||||
®istry,
|
||||
"sales-session-rate",
|
||||
"mcp-session-rate",
|
||||
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
|
||||
)
|
||||
@@ -1755,6 +1875,7 @@ mod tests {
|
||||
publish_agent_for_operation(®istry, &operation, "sales-async-rate").await;
|
||||
let api_key = create_platform_api_key(
|
||||
®istry,
|
||||
"sales-async-rate",
|
||||
"mcp-async-rate",
|
||||
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
|
||||
)
|
||||
@@ -1866,6 +1987,7 @@ mod tests {
|
||||
publish_agent_for_operation(®istry, &operation, "sales-async-completed").await;
|
||||
let api_key = create_platform_api_key(
|
||||
®istry,
|
||||
"sales-async-completed",
|
||||
"mcp-async-completed",
|
||||
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
|
||||
)
|
||||
@@ -1951,6 +2073,7 @@ mod tests {
|
||||
publish_agent_for_operation(®istry, &operation, "sales-rate-limited").await;
|
||||
let api_key = create_platform_api_key(
|
||||
®istry,
|
||||
"sales-rate-limited",
|
||||
"mcp-rate-limit",
|
||||
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
|
||||
)
|
||||
@@ -2050,9 +2173,17 @@ mod tests {
|
||||
|
||||
publish_agent_for_operation(®istry, &operation_a, "sales-async-a").await;
|
||||
publish_agent_for_operation(®istry, &operation_b, "sales-async-b").await;
|
||||
let api_key = create_platform_api_key(
|
||||
let api_key_a = create_platform_api_key(
|
||||
®istry,
|
||||
"mcp-cross-async",
|
||||
"sales-async-a",
|
||||
"mcp-cross-async-a",
|
||||
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
|
||||
)
|
||||
.await;
|
||||
let api_key_b = create_platform_api_key(
|
||||
®istry,
|
||||
"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(®istry, &operation, "sales-async").await;
|
||||
let api_key = create_platform_api_key(
|
||||
®istry,
|
||||
"sales-async",
|
||||
"mcp-async",
|
||||
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
|
||||
)
|
||||
@@ -2290,6 +2422,7 @@ mod tests {
|
||||
publish_agent_for_operation(®istry, &operation, "sales-window").await;
|
||||
let api_key = create_platform_api_key(
|
||||
®istry,
|
||||
"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(),
|
||||
|
||||
Reference in New Issue
Block a user