admin: delegate machine auth route

This commit is contained in:
github-ops
2026-05-14 19:12:11 +00:00
parent 75f76a28bf
commit c6b1456133
3 changed files with 112 additions and 40 deletions
+1 -1
View File
@@ -156,5 +156,5 @@ Progress:
- Phase 0 / task `0.12`: `apps/admin-api` и `apps/mcp-server` переведены на `community_default().with_limits(...).with_response_cache(...).build()` вместо прямой сборки runtime через `RuntimeExecutor::with_limits(...)`
- Phase 0 / task `0.13`: введены `RegistryExtension`, `ExtensionMigration` и `apply_extension_migrations(...)` в `crank-registry` как отдельный public seam для additive private migrations
- Phase 0 / task `0.14`: добавлены `AdminServiceBuilder`, seam slots (`identity_provider`, `policy_engine`, `audit_sink`, `token_issuer`, `capability_profile`) и community defaults для них; `apps/admin-api/src/main.rs` переведен на builder
- Phase 0 / task `0.15`: начат route delegation pass — `capabilities` route идет через `capability_profile`, а write handlers в `routes/access.rs` проверяют `policy_engine` и пишут generic audit events через `audit_sink`; `machine_auth` часть остается pending до actor-aware auth wiring
- Phase 0 / task `0.15`: route delegation переведен на public seams — `capabilities` route идет через `capability_profile`, write handlers в `routes/access.rs` проверяют `policy_engine` и пишут generic audit events через `audit_sink`, а `machine_auth` route использует `token_issuer` seam и сохраняет текущий Community `403` contract
- backward-compatible alias `CommunityMachineCredentialVerifier` сохранен, поведение Community не изменено
+111 -3
View File
@@ -1,5 +1,9 @@
use axum::{Json, extract::State};
use crank_core::{IssueAgentTokenRequest, IssueOneTimeAgentTokenRequest};
use crank_core::{
IssueAgentTokenRequest, IssueOneTimeAgentTokenRequest, MachineAccessMode, MembershipRole,
TokenIssuerActor, TokenIssuerError, UserId, WorkspaceId,
};
use serde_json::json;
use crate::{error::ApiError, state::AppState};
@@ -7,7 +11,22 @@ 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?;
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)))
}
@@ -15,6 +34,95 @@ 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?;
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
}
-36
View File
@@ -1614,42 +1614,6 @@ impl AdminService {
self.capability_profile().capabilities()
}
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,
}),
))
}
pub async fn list_operations(
&self,
workspace_id: &WorkspaceId,