diff --git a/TASKS.md b/TASKS.md index 28a9681..1cb82c4 100644 --- a/TASKS.md +++ b/TASKS.md @@ -144,5 +144,6 @@ 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.5`: введены `PolicyEngine`, `SessionActor`, `PolicyAction`, `PolicyScope`, `PolicyDecision` и default `OwnerOnlyPolicyEngine` в public seam `crank_core::ext::access` - Phase 0 / task `0.7`: введены `CapabilityProfile` и `CommunityCapabilityProfile`, а `admin-api` capability payload теперь собирается через public seam вместо локального literal - backward-compatible alias `CommunityMachineCredentialVerifier` сохранен, поведение Community не изменено diff --git a/crates/crank-core/src/ext/access.rs b/crates/crank-core/src/ext/access.rs index 8b13789..259093f 100644 --- a/crates/crank-core/src/ext/access.rs +++ b/crates/crank-core/src/ext/access.rs @@ -1 +1,142 @@ +use crate::{MembershipRole, UserId, WorkspaceId}; +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SessionActor { + pub user_id: UserId, + pub workspace_id: WorkspaceId, + pub role: MembershipRole, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[allow(clippy::enum_variant_names)] +pub enum PolicyAction { + ReadWorkspace, + WriteWorkspace, + ReadWorkspaceAccess, + WriteWorkspaceAccess, + ReadOperation, + WriteOperation, + ReadAgent, + WriteAgent, + ReadPlatformApiKey, + WritePlatformApiKey, + ReadSecret, + WriteSecret, + ReadAuthProfile, + WriteAuthProfile, + ReadObservability, + ReadCapability, +} + +impl PolicyAction { + pub fn is_read(self) -> bool { + matches!( + self, + Self::ReadWorkspace + | Self::ReadWorkspaceAccess + | Self::ReadOperation + | Self::ReadAgent + | Self::ReadPlatformApiKey + | Self::ReadSecret + | Self::ReadAuthProfile + | Self::ReadObservability + | Self::ReadCapability + ) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum PolicyScope { + Workspace(WorkspaceId), + Global, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum PolicyDecision { + Allow, + Deny { reason: &'static str }, +} + +pub trait PolicyEngine: Send + Sync { + fn check( + &self, + actor: &SessionActor, + action: PolicyAction, + scope: PolicyScope, + ) -> PolicyDecision; +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct OwnerOnlyPolicyEngine; + +impl PolicyEngine for OwnerOnlyPolicyEngine { + fn check( + &self, + actor: &SessionActor, + action: PolicyAction, + _scope: PolicyScope, + ) -> PolicyDecision { + if matches!(actor.role, MembershipRole::Owner) || action.is_read() { + PolicyDecision::Allow + } else { + PolicyDecision::Deny { + reason: "write actions require owner role in community", + } + } + } +} + +#[cfg(test)] +mod tests { + use super::{ + OwnerOnlyPolicyEngine, PolicyAction, PolicyDecision, PolicyEngine, PolicyScope, + SessionActor, + }; + use crate::{MembershipRole, UserId, WorkspaceId}; + + fn actor(role: MembershipRole) -> SessionActor { + SessionActor { + user_id: UserId::new("user_01"), + workspace_id: WorkspaceId::new("ws_01"), + role, + } + } + + #[test] + fn owner_is_allowed_for_write_actions() { + let decision = OwnerOnlyPolicyEngine.check( + &actor(MembershipRole::Owner), + PolicyAction::WriteWorkspace, + PolicyScope::Workspace(WorkspaceId::new("ws_01")), + ); + + assert_eq!(decision, PolicyDecision::Allow); + } + + #[test] + fn non_owner_is_allowed_for_read_actions() { + let decision = OwnerOnlyPolicyEngine.check( + &actor(MembershipRole::Viewer), + PolicyAction::ReadWorkspace, + PolicyScope::Workspace(WorkspaceId::new("ws_01")), + ); + + assert_eq!(decision, PolicyDecision::Allow); + } + + #[test] + fn non_owner_is_denied_for_write_actions() { + let decision = OwnerOnlyPolicyEngine.check( + &actor(MembershipRole::Admin), + PolicyAction::WriteWorkspace, + PolicyScope::Workspace(WorkspaceId::new("ws_01")), + ); + + assert_eq!( + decision, + PolicyDecision::Deny { + reason: "write actions require owner role in community", + } + ); + } +} diff --git a/crates/crank-core/src/lib.rs b/crates/crank-core/src/lib.rs index 39f27fd..a38af30 100644 --- a/crates/crank-core/src/lib.rs +++ b/crates/crank-core/src/lib.rs @@ -32,6 +32,9 @@ pub use cache::{ pub use edition::{ EditionCapabilities, EditionLimits, MachineAccessMode, OperationSecurityLevel, ProductEdition, }; +pub use ext::access::{ + OwnerOnlyPolicyEngine, PolicyAction, PolicyDecision, PolicyEngine, PolicyScope, SessionActor, +}; pub use ext::auth::{ MachineCredentialVerifier, MachineCredentialVerifierError, SharedMachineCredentialVerifier, VerifiedMachineCredential,