admin: delegate machine auth route
This commit is contained in:
@@ -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 не изменено
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user