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.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.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.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 не изменено
|
- backward-compatible alias `CommunityMachineCredentialVerifier` сохранен, поведение Community не изменено
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
use axum::{Json, extract::State};
|
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};
|
use crate::{error::ApiError, state::AppState};
|
||||||
|
|
||||||
@@ -7,7 +11,22 @@ pub async fn issue_agent_token(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(payload): Json<IssueAgentTokenRequest>,
|
Json(payload): Json<IssueAgentTokenRequest>,
|
||||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
) -> 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)))
|
Ok(Json(serde_json::json!(response)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,6 +34,95 @@ pub async fn issue_one_time_agent_token(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(payload): Json<IssueOneTimeAgentTokenRequest>,
|
Json(payload): Json<IssueOneTimeAgentTokenRequest>,
|
||||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
) -> 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)))
|
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()
|
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(
|
pub async fn list_operations(
|
||||||
&self,
|
&self,
|
||||||
workspace_id: &WorkspaceId,
|
workspace_id: &WorkspaceId,
|
||||||
|
|||||||
Reference in New Issue
Block a user