auth: add community token service seam
This commit is contained in:
@@ -24,6 +24,7 @@ use crate::{
|
||||
},
|
||||
auth_profiles::{create_auth_profile, get_auth_profile, list_auth_profiles},
|
||||
capabilities::get_capabilities,
|
||||
machine_auth::{issue_agent_token, issue_one_time_agent_token},
|
||||
observability::{get_agent_usage, get_log, get_operation_usage, get_usage, list_logs},
|
||||
operations::{
|
||||
archive_operation, create_operation, create_version, delete_operation,
|
||||
@@ -209,6 +210,12 @@ pub fn build_app(state: AppState) -> Router {
|
||||
.route("/login", post(login))
|
||||
.merge(protected_auth_router),
|
||||
)
|
||||
.nest(
|
||||
"/mcp-auth/v1",
|
||||
Router::new()
|
||||
.route("/token", post(issue_agent_token))
|
||||
.route("/token/one-time", post(issue_one_time_agent_token)),
|
||||
)
|
||||
.nest("/api/admin", admin_router)
|
||||
.layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
@@ -1038,6 +1045,87 @@ mod tests {
|
||||
assert_eq!(response["limits"]["max_agents_per_workspace"], 1);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[serial]
|
||||
async fn rejects_short_lived_machine_token_issue_in_community() {
|
||||
let registry = test_registry().await;
|
||||
let storage_root = test_storage_root("community_short_lived_token_contract");
|
||||
let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await;
|
||||
let root_url = base_url
|
||||
.as_ref()
|
||||
.split("/api/admin/workspaces/")
|
||||
.next()
|
||||
.unwrap();
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
.post(format!("{root_url}/mcp-auth/v1/token"))
|
||||
.json(&json!({
|
||||
"grant_type": "agent_key",
|
||||
"agent_key": "crk_agent_demo_secret",
|
||||
"scope": ["tools:call"]
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), reqwest::StatusCode::FORBIDDEN);
|
||||
let payload = response.json::<Value>().await.unwrap();
|
||||
assert_eq!(payload["error"]["code"], "forbidden");
|
||||
assert_eq!(
|
||||
payload["error"]["message"],
|
||||
"short-lived machine access is not available in Community"
|
||||
);
|
||||
assert_eq!(payload["error"]["context"]["edition"], "community");
|
||||
assert_eq!(
|
||||
payload["error"]["context"]["machine_access_mode"],
|
||||
"short_lived_token"
|
||||
);
|
||||
assert_eq!(payload["error"]["context"]["grant_type"], "agent_key");
|
||||
assert_eq!(payload["error"]["context"]["upgrade_required"], json!(true));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[serial]
|
||||
async fn rejects_one_time_machine_token_issue_in_community() {
|
||||
let registry = test_registry().await;
|
||||
let storage_root = test_storage_root("community_one_time_token_contract");
|
||||
let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await;
|
||||
let root_url = base_url
|
||||
.as_ref()
|
||||
.split("/api/admin/workspaces/")
|
||||
.next()
|
||||
.unwrap();
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
.post(format!("{root_url}/mcp-auth/v1/token/one-time"))
|
||||
.json(&json!({
|
||||
"agent_key": "crk_agent_demo_secret",
|
||||
"operation_id": "op_sensitive_01",
|
||||
"scope": ["tools:call"]
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), reqwest::StatusCode::FORBIDDEN);
|
||||
let payload = response.json::<Value>().await.unwrap();
|
||||
assert_eq!(payload["error"]["code"], "forbidden");
|
||||
assert_eq!(
|
||||
payload["error"]["message"],
|
||||
"one-time machine access is not available in Community"
|
||||
);
|
||||
assert_eq!(payload["error"]["context"]["edition"], "community");
|
||||
assert_eq!(
|
||||
payload["error"]["context"]["machine_access_mode"],
|
||||
"one_time_token"
|
||||
);
|
||||
assert_eq!(
|
||||
payload["error"]["context"]["operation_id"],
|
||||
"op_sensitive_01"
|
||||
);
|
||||
assert_eq!(payload["error"]["context"]["upgrade_required"], json!(true));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[serial]
|
||||
async fn returns_community_protocol_capabilities_only_for_supported_protocols() {
|
||||
|
||||
@@ -109,6 +109,13 @@ impl ApiError {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn forbidden_with_context(message: impl Into<String>, context: Value) -> Self {
|
||||
Self::Forbidden {
|
||||
message: message.into(),
|
||||
context: Some(context),
|
||||
}
|
||||
}
|
||||
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
Self::Unauthorized { .. } => StatusCode::UNAUTHORIZED,
|
||||
|
||||
@@ -3,6 +3,7 @@ pub mod agents;
|
||||
pub mod auth;
|
||||
pub mod auth_profiles;
|
||||
pub mod capabilities;
|
||||
pub mod machine_auth;
|
||||
pub mod observability;
|
||||
pub mod operations;
|
||||
pub mod secrets;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
use axum::{Json, extract::State};
|
||||
use crank_core::{IssueAgentTokenRequest, IssueOneTimeAgentTokenRequest};
|
||||
|
||||
use crate::{error::ApiError, state::AppState};
|
||||
|
||||
pub async fn issue_agent_token(
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<IssueAgentTokenRequest>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let response = state.service.issue_agent_token(payload).await?;
|
||||
Ok(Json(serde_json::json!(response)))
|
||||
}
|
||||
|
||||
pub async fn issue_one_time_agent_token(
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<IssueOneTimeAgentTokenRequest>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let response = state.service.issue_one_time_agent_token(payload).await?;
|
||||
Ok(Json(serde_json::json!(response)))
|
||||
}
|
||||
@@ -12,12 +12,13 @@ use crank_core::{
|
||||
AsyncJobHandle, AuthConfig, AuthKind, AuthProfile, AuthProfileId, ConfigExport,
|
||||
EditionCapabilities, EditionLimits, ExecutionMode, ExportMode, GeneratedDraft,
|
||||
GeneratedDraftStatus, InvitationId, InvitationStatus, InvitationToken, InvocationLevel,
|
||||
InvocationLog, InvocationLogId, InvocationSource, InvocationStatus, JobStatus,
|
||||
MachineAccessMode, MembershipRole, OperationId, OperationSecurityLevel, OperationStatus,
|
||||
PlatformApiKey, PlatformApiKeyId, PlatformApiKeyScope, PlatformApiKeyStatus, ProductEdition,
|
||||
Protocol, SampleId, Samples, Secret, SecretId, SecretKind, SecretStatus, StreamSession,
|
||||
StreamStatus, Target, TransportBehavior, UsagePeriod, UserId, UserSessionId, Workspace,
|
||||
WorkspaceId, WorkspaceStatus,
|
||||
InvocationLog, InvocationLogId, InvocationSource, InvocationStatus, IssueAgentTokenRequest,
|
||||
IssueOneTimeAgentTokenRequest, IssuedAgentTokenResponse, JobStatus, MachineAccessMode,
|
||||
MembershipRole, OperationId, OperationSecurityLevel, OperationStatus, PlatformApiKey,
|
||||
PlatformApiKeyId, PlatformApiKeyScope, PlatformApiKeyStatus, ProductEdition, Protocol,
|
||||
SampleId, Samples, Secret, SecretId, SecretKind, SecretStatus, StreamSession, StreamStatus,
|
||||
Target, TransportBehavior, UsagePeriod, UserId, UserSessionId, Workspace, WorkspaceId,
|
||||
WorkspaceStatus,
|
||||
};
|
||||
use crank_mapping::{JsonPathRoot, MappingSet, infer_mapping_from_samples};
|
||||
use crank_proto::{ProtoService, services_from_descriptor_set_bytes};
|
||||
@@ -1584,6 +1585,42 @@ impl AdminService {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn issue_agent_token(
|
||||
&self,
|
||||
payload: IssueAgentTokenRequest,
|
||||
) -> Result<IssuedAgentTokenResponse, ApiError> {
|
||||
let edition = self.get_capabilities().await.edition;
|
||||
let machine_access_mode = MachineAccessMode::ShortLivedToken;
|
||||
|
||||
Err(ApiError::forbidden_with_context(
|
||||
"short-lived machine access is not available in Community",
|
||||
json!({
|
||||
"edition": edition,
|
||||
"machine_access_mode": machine_access_mode,
|
||||
"grant_type": payload.grant_type,
|
||||
"upgrade_required": true,
|
||||
}),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn issue_one_time_agent_token(
|
||||
&self,
|
||||
payload: IssueOneTimeAgentTokenRequest,
|
||||
) -> Result<IssuedAgentTokenResponse, ApiError> {
|
||||
let edition = self.get_capabilities().await.edition;
|
||||
let machine_access_mode = MachineAccessMode::OneTimeToken;
|
||||
|
||||
Err(ApiError::forbidden_with_context(
|
||||
"one-time machine access is not available in Community",
|
||||
json!({
|
||||
"edition": edition,
|
||||
"machine_access_mode": machine_access_mode,
|
||||
"operation_id": payload.operation_id,
|
||||
"upgrade_required": true,
|
||||
}),
|
||||
))
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub async fn list_stream_sessions(
|
||||
&self,
|
||||
|
||||
Reference in New Issue
Block a user