core: add community policy engine seam
This commit is contained in:
@@ -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 не изменено
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user