use axum::{Json, extract::State}; use crank_core::{ IssueAgentTokenRequest, IssueOneTimeAgentTokenRequest, MachineAccessMode, MembershipRole, TokenIssuerActor, TokenIssuerError, UserId, WorkspaceId, }; use serde_json::json; use crate::{error::ApiError, state::AppState}; pub async fn issue_agent_token( State(state): State, Json(payload): Json, ) -> Result, ApiError> { let edition = state.service.capability_profile().capabilities().edition; let grant_type = payload.grant_type.clone(); let response = state .service .token_issuer() .issue_short_lived(payload, &community_token_issuer_actor()) .await .map_err(|error| { map_token_issuer_error( error, edition, MachineAccessMode::ShortLivedToken, json!({ "grant_type": grant_type, "upgrade_required": true, }), ) })?; Ok(Json(serde_json::json!(response))) } pub async fn issue_one_time_agent_token( State(state): State, Json(payload): Json, ) -> Result, ApiError> { let edition = state.service.capability_profile().capabilities().edition; let operation_id = payload.operation_id.as_str().to_owned(); let response = state .service .token_issuer() .issue_one_time(payload, &community_token_issuer_actor()) .await .map_err(|error| { map_token_issuer_error( error, edition, MachineAccessMode::OneTimeToken, json!({ "operation_id": operation_id, "upgrade_required": true, }), ) })?; Ok(Json(serde_json::json!(response))) } fn community_token_issuer_actor() -> TokenIssuerActor { TokenIssuerActor { user_id: UserId::new("user_community_public"), workspace_id: WorkspaceId::new("ws_community_public"), role: MembershipRole::Viewer, } } fn map_token_issuer_error( error: TokenIssuerError, edition: crank_core::ProductEdition, machine_access_mode: MachineAccessMode, extra_context: serde_json::Value, ) -> ApiError { match error { TokenIssuerError::NotSupportedInEdition => ApiError::forbidden_with_context( match machine_access_mode { MachineAccessMode::ShortLivedToken => { "short-lived machine access is not available in Community" } MachineAccessMode::OneTimeToken => { "one-time machine access is not available in Community" } MachineAccessMode::StaticAgentKey => { "static agent key machine access is not available for token issue" } }, merge_machine_auth_context(edition, machine_access_mode, extra_context), ), TokenIssuerError::InvalidGrant(reason) => ApiError::validation_with_context( "invalid machine token grant", json!({ "reason": reason }), ), TokenIssuerError::AgentKeyUnknown => ApiError::validation("agent key is unknown"), TokenIssuerError::OperationNotStrict => ApiError::validation("operation is not strict"), TokenIssuerError::OperationNotPublishedForAgent => { ApiError::validation("operation is not published for agent") } TokenIssuerError::RegistryFailure(details) => { ApiError::internal(format!("token issuer registry failure: {details}")) } TokenIssuerError::ReplayGuardFailure(details) => { ApiError::internal(format!("token issuer replay guard failure: {details}")) } } } fn merge_machine_auth_context( edition: crank_core::ProductEdition, machine_access_mode: MachineAccessMode, extra_context: serde_json::Value, ) -> serde_json::Value { let mut context = json!({ "edition": edition, "machine_access_mode": machine_access_mode, }); if let (Some(base), Some(extra)) = (context.as_object_mut(), extra_context.as_object()) { for (key, value) in extra { base.insert(key.clone(), value.clone()); } } context }