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
+97 -61
View File
@@ -9,13 +9,14 @@ use crate::{
request_context::apply_request_context,
routes::{
access::{
create_invitation, create_platform_api_key, delete_invitation, delete_membership,
delete_platform_api_key, delete_workspace, export_workspace, list_invitations,
list_memberships, list_platform_api_keys, revoke_platform_api_key, update_membership,
create_invitation, delete_invitation, delete_membership, delete_workspace,
export_workspace, list_invitations, list_memberships, update_membership,
},
agents::{
archive_agent, create_agent, delete_agent, get_agent, get_agent_version, list_agents,
publish_agent, save_agent_bindings, unpublish_agent, update_agent,
archive_agent, create_agent, create_agent_platform_api_key, delete_agent,
delete_agent_platform_api_key, get_agent, get_agent_version,
list_agent_platform_api_keys, list_agents, publish_agent,
revoke_agent_platform_api_key, save_agent_bindings, unpublish_agent, update_agent,
},
auth::{
change_password, get_profile, get_session, login, logout, update_current_workspace,
@@ -114,6 +115,18 @@ pub fn build_app(state: AppState) -> Router {
.route("/agents/{agent_id}/publish", post(publish_agent))
.route("/agents/{agent_id}/unpublish", post(unpublish_agent))
.route("/agents/{agent_id}/archive", post(archive_agent))
.route(
"/agents/{agent_id}/platform-api-keys",
get(list_agent_platform_api_keys).post(create_agent_platform_api_key),
)
.route(
"/agents/{agent_id}/platform-api-keys/{key_id}/revoke",
post(revoke_agent_platform_api_key),
)
.route(
"/agents/{agent_id}/platform-api-keys/{key_id}",
delete(delete_agent_platform_api_key),
)
.route(
"/auth-profiles",
get(list_auth_profiles).post(create_auth_profile),
@@ -136,18 +149,6 @@ pub fn build_app(state: AppState) -> Router {
)
.route("/invitations/{invitation_id}", delete(delete_invitation))
.route("/export", get(export_workspace))
.route(
"/platform-api-keys",
get(list_platform_api_keys).post(create_platform_api_key),
)
.route(
"/platform-api-keys/{key_id}/revoke",
post(revoke_platform_api_key),
)
.route(
"/platform-api-keys/{key_id}",
delete(delete_platform_api_key),
)
.route("/logs", get(list_logs))
.route("/logs/{log_id}", get(get_log))
.route("/usage", get(get_usage))
@@ -891,48 +892,12 @@ mod tests {
.json::<Value>()
.await
.unwrap();
let created_key = client
.post(format!("{base_url}/platform-api-keys"))
.json(&json!({
"name": "workspace-operator",
"scopes": ["read", "write"]
}))
.send()
.await
.unwrap()
.json::<Value>()
.await
.unwrap();
let key_id = created_key["api_key"]["api_key"]["id"]
.as_str()
.unwrap()
.to_owned();
let listed_keys = client
.get(format!("{base_url}/platform-api-keys"))
.send()
.await
.unwrap()
.json::<Value>()
.await
.unwrap();
let revoke_status = client
.post(format!("{base_url}/platform-api-keys/{key_id}/revoke"))
.send()
.await
.unwrap()
.status();
let delete_invitation_status = client
.delete(format!("{base_url}/invitations/{invitation_id}"))
.send()
.await
.unwrap()
.status();
let delete_key_status = client
.delete(format!("{base_url}/platform-api-keys/{key_id}"))
.send()
.await
.unwrap()
.status();
assert_eq!(members["items"][0]["role"], "owner");
assert_eq!(
@@ -949,14 +914,89 @@ mod tests {
invitations["items"][0]["invitation"]["email"],
"operator@example.com"
);
assert_eq!(delete_invitation_status, reqwest::StatusCode::NO_CONTENT);
}
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn manages_agent_platform_api_keys() {
let registry = test_registry().await;
let storage_root = test_storage_root("agent_platform_keys");
let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await;
let client = authorized_client(&base_url).await;
let created_agent = assert_success_json(
client
.post(format!("{base_url}/agents"))
.json(&json!({
"slug": "sales-routing",
"display_name": "Sales Routing",
"description": "Routing agent",
"instructions": {},
"tool_selection_policy": {}
}))
.send()
.await
.unwrap(),
)
.await;
let agent_id = created_agent["agent_id"].as_str().unwrap().to_owned();
let created_key = assert_success_json(
client
.post(format!("{base_url}/agents/{agent_id}/platform-api-keys"))
.json(&json!({
"name": "sales-routing-primary",
"scopes": ["read", "write"]
}))
.send()
.await
.unwrap(),
)
.await;
let key_id = created_key["api_key"]["api_key"]["id"]
.as_str()
.unwrap()
.to_owned();
let listed_keys = assert_success_json(
client
.get(format!("{base_url}/agents/{agent_id}/platform-api-keys"))
.send()
.await
.unwrap(),
)
.await;
let revoke_status = client
.post(format!(
"{base_url}/agents/{agent_id}/platform-api-keys/{key_id}/revoke"
))
.send()
.await
.unwrap()
.status();
let delete_status = client
.delete(format!(
"{base_url}/agents/{agent_id}/platform-api-keys/{key_id}"
))
.send()
.await
.unwrap()
.status();
assert_eq!(created_key["api_key"]["api_key"]["agent_id"], agent_id);
assert_eq!(
listed_keys["items"][0]["api_key"]["agent_id"],
json!(agent_id)
);
assert_eq!(
listed_keys["items"][0]["api_key"]["name"],
"workspace-operator"
"sales-routing-primary"
);
assert!(created_key["secret"].as_str().unwrap().starts_with("crk_"));
assert_eq!(revoke_status, reqwest::StatusCode::NO_CONTENT);
assert_eq!(delete_invitation_status, reqwest::StatusCode::NO_CONTENT);
assert_eq!(delete_key_status, reqwest::StatusCode::NO_CONTENT);
assert_eq!(delete_status, reqwest::StatusCode::NO_CONTENT);
}
#[tokio::test(flavor = "multi_thread")]
@@ -1087,11 +1127,7 @@ mod tests {
let agents = service.list_agents(&default_workspace_id).await.unwrap();
assert!(agents.len() >= 2);
let api_keys = service
.list_platform_api_keys(&default_workspace_id)
.await
.unwrap();
assert!(api_keys.len() >= 2);
assert!(agents.iter().any(|agent| agent.key_count > 0));
let logs = service
.list_logs(