use std::sync::{Arc, Mutex}; use application::{ ResolveAgentPermissions, ResolveAgentPermissionsInput, UpdateAgentPermissions, UpdateAgentPermissionsInput, UpdateProjectPermissions, UpdateProjectPermissionsInput, }; use async_trait::async_trait; use domain::ids::{AgentId, ProjectId}; use domain::ports::{PermissionStore, StoreError}; use domain::project::{Project, ProjectPath}; use domain::remote::RemoteRef; use domain::{ Capability, Effect, PermissionRule, PermissionSet, PermissionShadowReport, Posture, ProjectPermissions, }; #[derive(Default)] struct FakePermissionStore { doc: Mutex, saves: Mutex, } #[async_trait] impl PermissionStore for FakePermissionStore { async fn load_permissions(&self, _project: &Project) -> Result { Ok(self.doc.lock().unwrap().clone()) } async fn save_permissions( &self, _project: &Project, permissions: &ProjectPermissions, ) -> Result<(), StoreError> { *self.doc.lock().unwrap() = permissions.clone(); *self.saves.lock().unwrap() += 1; Ok(()) } } fn project() -> Project { Project::new( ProjectId::new_random(), "permissions", ProjectPath::new("/home/me/proj").unwrap(), RemoteRef::local(), 1_700_000_000_000, ) .unwrap() } #[tokio::test] async fn update_project_permissions_replaces_defaults_and_persists() { let store = Arc::new(FakePermissionStore::default()); let use_case = UpdateProjectPermissions::new(store.clone()); let out = use_case .execute(UpdateProjectPermissionsInput { project: project(), permissions: Some(PermissionSet::new(vec![], Posture::Ask)), }) .await .unwrap(); assert_eq!( out.permissions.project_defaults.unwrap().fallback(), Posture::Ask ); assert_eq!(*store.saves.lock().unwrap(), 1); } #[tokio::test] async fn update_agent_permissions_adds_and_removes_sparse_override() { let store = Arc::new(FakePermissionStore::default()); let use_case = UpdateAgentPermissions::new(store.clone()); let agent = AgentId::new_random(); use_case .execute(UpdateAgentPermissionsInput { project: project(), agent_id: agent, permissions: Some(PermissionSet::new(vec![], Posture::Deny)), }) .await .unwrap(); assert_eq!( store .doc .lock() .unwrap() .agent_permissions(agent) .unwrap() .fallback(), Posture::Deny ); let out = use_case .execute(UpdateAgentPermissionsInput { project: project(), agent_id: agent, permissions: None, }) .await .unwrap(); assert!(out.permissions.agent_permissions(agent).is_none()); assert_eq!(*store.saves.lock().unwrap(), 2); } #[tokio::test] async fn resolve_agent_permissions_returns_effective_policy() { let agent = AgentId::new_random(); let store = Arc::new(FakePermissionStore { doc: Mutex::new(ProjectPermissions::new( Some(PermissionSet::new(vec![], Posture::Ask)), vec![], )), saves: Mutex::new(0), }); let use_case = ResolveAgentPermissions::new(store); let out = use_case .execute(ResolveAgentPermissionsInput { project: project(), agent_id: agent, }) .await .unwrap(); assert_eq!(out.effective.unwrap().fallback(), Posture::Ask); assert_eq!(out.shadowed, PermissionShadowReport::default()); } #[tokio::test] async fn resolve_agent_permissions_reports_shadowed_alongside_unchanged_effective() { let agent = AgentId::new_random(); let store = Arc::new(FakePermissionStore { doc: Mutex::new(ProjectPermissions::new( Some(PermissionSet::new( vec![PermissionRule::bash(Effect::Deny, vec![])], Posture::Ask, )), vec![domain::AgentPermissionOverride::new( agent, PermissionSet::new( vec![PermissionRule::bash(Effect::Allow, vec![])], Posture::Ask, ), )], )), saves: Mutex::new(0), }); let use_case = ResolveAgentPermissions::new(store); let out = use_case .execute(ResolveAgentPermissionsInput { project: project(), agent_id: agent, }) .await .unwrap(); assert!(out.shadowed.execute_bash); assert_eq!(out.effective.unwrap().decide_bash("ls"), Posture::Deny); } #[tokio::test] async fn resolve_agent_permissions_shadowed_defaults_when_no_agent_override() { let agent = AgentId::new_random(); let store = Arc::new(FakePermissionStore { doc: Mutex::new(ProjectPermissions::new( Some(PermissionSet::new( vec![PermissionRule::file( Capability::Read, Effect::Deny, domain::PathScope::new(["**".to_owned()]).unwrap(), ) .unwrap()], Posture::Deny, )), vec![], )), saves: Mutex::new(0), }); let use_case = ResolveAgentPermissions::new(store); let out = use_case .execute(ResolveAgentPermissionsInput { project: project(), agent_id: agent, }) .await .unwrap(); assert_eq!(out.shadowed, PermissionShadowReport::default()); }