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", } ); } }