diff --git a/Cargo.lock b/Cargo.lock index 73a6448..910311f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -404,6 +404,7 @@ dependencies = [ "serde_yaml", "thiserror", "time", + "tokio", ] [[package]] diff --git a/TASKS.md b/TASKS.md index a568c09..47e0188 100644 --- a/TASKS.md +++ b/TASKS.md @@ -144,6 +144,7 @@ Progress: - done: - Phase 0 / task `0.1`: создан `crank-core::ext` module skeleton - Phase 0 / task `0.2`: `MachineCredentialVerifier` и связанные типы вынесены из `apps/mcp-server` в public seam `crank_core::ext::auth` + - Phase 0 / task `0.3`: введены `MachineTokenIssuer`, `NoMachineTokenIssuer`, `TokenIssuerActor` и `TokenIssuerError` в public seam `crank_core::ext::auth` - Phase 0 / task `0.5`: введены `PolicyEngine`, `SessionActor`, `PolicyAction`, `PolicyScope`, `PolicyDecision` и default `OwnerOnlyPolicyEngine` в public seam `crank_core::ext::access` - Phase 0 / task `0.6`: введены `AuditSink`, `NoopAuditSink`, `AuditEventId` и базовые audit-типы в public seam `crank_core::ext::audit` - Phase 0 / task `0.7`: введены `CapabilityProfile` и `CommunityCapabilityProfile`, а `admin-api` capability payload теперь собирается через public seam вместо локального literal diff --git a/crates/crank-core/Cargo.toml b/crates/crank-core/Cargo.toml index 60af9cf..773f4c5 100644 --- a/crates/crank-core/Cargo.toml +++ b/crates/crank-core/Cargo.toml @@ -14,3 +14,4 @@ time.workspace = true [dev-dependencies] serde_yaml.workspace = true +tokio.workspace = true diff --git a/crates/crank-core/src/ext/auth.rs b/crates/crank-core/src/ext/auth.rs index c182dd0..620f270 100644 --- a/crates/crank-core/src/ext/auth.rs +++ b/crates/crank-core/src/ext/auth.rs @@ -2,7 +2,11 @@ use std::sync::Arc; use async_trait::async_trait; -use crate::{MachineAccessMode, OperationSecurityLevel, PlatformApiKeyScope}; +use crate::{ + IssueAgentTokenRequest, IssueOneTimeAgentTokenRequest, IssuedAgentTokenResponse, + MachineAccessMode, MembershipRole, OperationSecurityLevel, PlatformApiKeyScope, UserId, + WorkspaceId, +}; #[derive(Clone, Debug, PartialEq, Eq)] pub struct VerifiedMachineCredential { @@ -26,3 +30,117 @@ pub trait MachineCredentialVerifier: Send + Sync { } pub type SharedMachineCredentialVerifier = Arc; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TokenIssuerActor { + pub user_id: UserId, + pub workspace_id: WorkspaceId, + pub role: MembershipRole, +} + +#[derive(Debug, thiserror::Error, PartialEq, Eq)] +pub enum TokenIssuerError { + #[error("machine tokens are not available in this edition")] + NotSupportedInEdition, + #[error("invalid grant: {0}")] + InvalidGrant(String), + #[error("agent key is unknown")] + AgentKeyUnknown, + #[error("operation is not strict")] + OperationNotStrict, + #[error("operation is not published for agent")] + OperationNotPublishedForAgent, + #[error("registry failure: {0}")] + RegistryFailure(String), + #[error("replay guard failure: {0}")] + ReplayGuardFailure(String), +} + +#[async_trait] +pub trait MachineTokenIssuer: Send + Sync { + async fn issue_short_lived( + &self, + request: IssueAgentTokenRequest, + actor: &TokenIssuerActor, + ) -> Result; + + async fn issue_one_time( + &self, + request: IssueOneTimeAgentTokenRequest, + actor: &TokenIssuerActor, + ) -> Result; +} + +pub type SharedMachineTokenIssuer = Arc; + +#[derive(Clone, Copy, Debug, Default)] +pub struct NoMachineTokenIssuer; + +#[async_trait] +impl MachineTokenIssuer for NoMachineTokenIssuer { + async fn issue_short_lived( + &self, + _request: IssueAgentTokenRequest, + _actor: &TokenIssuerActor, + ) -> Result { + Err(TokenIssuerError::NotSupportedInEdition) + } + + async fn issue_one_time( + &self, + _request: IssueOneTimeAgentTokenRequest, + _actor: &TokenIssuerActor, + ) -> Result { + Err(TokenIssuerError::NotSupportedInEdition) + } +} + +#[cfg(test)] +mod tests { + use super::{MachineTokenIssuer, NoMachineTokenIssuer, TokenIssuerActor, TokenIssuerError}; + use crate::{ + AgentTokenGrantType, IssueAgentTokenRequest, IssueOneTimeAgentTokenRequest, MembershipRole, + UserId, WorkspaceId, + }; + + fn actor() -> TokenIssuerActor { + TokenIssuerActor { + user_id: UserId::new("user_01"), + workspace_id: WorkspaceId::new("ws_01"), + role: MembershipRole::Owner, + } + } + + #[tokio::test] + async fn no_machine_token_issuer_rejects_short_lived_issue() { + let result = NoMachineTokenIssuer + .issue_short_lived( + IssueAgentTokenRequest { + grant_type: AgentTokenGrantType::AgentKey, + agent_key: Some("crk_agent_secret".to_owned()), + refresh_token: None, + scope: vec![], + }, + &actor(), + ) + .await; + + assert_eq!(result, Err(TokenIssuerError::NotSupportedInEdition)); + } + + #[tokio::test] + async fn no_machine_token_issuer_rejects_one_time_issue() { + let result = NoMachineTokenIssuer + .issue_one_time( + IssueOneTimeAgentTokenRequest { + agent_key: "crk_agent_secret".to_owned(), + operation_id: "op_01".into(), + scope: vec![], + }, + &actor(), + ) + .await; + + assert_eq!(result, Err(TokenIssuerError::NotSupportedInEdition)); + } +} diff --git a/crates/crank-core/src/lib.rs b/crates/crank-core/src/lib.rs index 0ceba0a..da15678 100644 --- a/crates/crank-core/src/lib.rs +++ b/crates/crank-core/src/lib.rs @@ -40,8 +40,9 @@ pub use ext::audit::{ AuditTargetKind, NoopAuditSink, SharedAuditSink, }; pub use ext::auth::{ - MachineCredentialVerifier, MachineCredentialVerifierError, SharedMachineCredentialVerifier, - VerifiedMachineCredential, + MachineCredentialVerifier, MachineCredentialVerifierError, MachineTokenIssuer, + NoMachineTokenIssuer, SharedMachineCredentialVerifier, SharedMachineTokenIssuer, + TokenIssuerActor, TokenIssuerError, VerifiedMachineCredential, }; pub use ext::capability::{CapabilityProfile, CommunityCapabilityProfile}; pub use ids::{