feat(permissions): expose network permission state (#103)
This commit is contained in:
@ -32,6 +32,7 @@ pub mod project;
|
||||
pub mod remote;
|
||||
pub mod skill;
|
||||
pub mod sprints;
|
||||
pub mod system_permissions;
|
||||
pub mod template;
|
||||
pub mod terminal;
|
||||
pub mod ticket_assistant;
|
||||
@ -170,6 +171,13 @@ pub use sprints::{
|
||||
RenameSprintInput, ReorderSprints, ReorderSprintsInput, ReorderSprintsOutput, SprintListEntry,
|
||||
SprintOutput, UnassignTicketFromSprint, UnassignTicketFromSprintInput,
|
||||
};
|
||||
pub use system_permissions::{
|
||||
GetProjectSystemPermissions, GetProjectSystemPermissionsInput,
|
||||
GetProjectSystemPermissionsOutput, ResolveAgentSystemPermissions,
|
||||
ResolveAgentSystemPermissionsInput, ResolveAgentSystemPermissionsOutput,
|
||||
UpdateAgentSystemPermissions, UpdateAgentSystemPermissionsInput,
|
||||
UpdateProjectSystemPermissions, UpdateProjectSystemPermissionsInput,
|
||||
};
|
||||
pub use template::{
|
||||
AgentDrift, CreateAgentFromTemplate, CreateAgentFromTemplateInput,
|
||||
CreateAgentFromTemplateOutput, CreateTemplate, CreateTemplateInput, CreateTemplateOutput,
|
||||
|
||||
168
crates/application/src/system_permissions.rs
Normal file
168
crates/application/src/system_permissions.rs
Normal file
@ -0,0 +1,168 @@
|
||||
//! System permission use cases.
|
||||
//!
|
||||
//! These use cases persist the wanted project/agent policies separately from
|
||||
//! filesystem/bash permissions and resolve them through a read-only runtime
|
||||
//! probe.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::ports::{RuntimePermissionProbe, SystemPermissionStore};
|
||||
use domain::{
|
||||
resolve_agent_system_permissions, AgentId, Project, ProjectSystemPermissions,
|
||||
ResolvedAgentSystemPermissions, SystemPermissionSet,
|
||||
};
|
||||
|
||||
use crate::error::AppError;
|
||||
|
||||
/// Reads the full project system permission document.
|
||||
pub struct GetProjectSystemPermissions {
|
||||
store: Arc<dyn SystemPermissionStore>,
|
||||
}
|
||||
|
||||
impl GetProjectSystemPermissions {
|
||||
/// Builds the use case.
|
||||
#[must_use]
|
||||
pub fn new(store: Arc<dyn SystemPermissionStore>) -> Self {
|
||||
Self { store }
|
||||
}
|
||||
|
||||
/// Executes the read.
|
||||
pub async fn execute(
|
||||
&self,
|
||||
input: GetProjectSystemPermissionsInput,
|
||||
) -> Result<GetProjectSystemPermissionsOutput, AppError> {
|
||||
let permissions = self.store.load_system_permissions(&input.project).await?;
|
||||
Ok(GetProjectSystemPermissionsOutput { permissions })
|
||||
}
|
||||
}
|
||||
|
||||
/// Input for [`GetProjectSystemPermissions`].
|
||||
pub struct GetProjectSystemPermissionsInput {
|
||||
/// Target project.
|
||||
pub project: Project,
|
||||
}
|
||||
|
||||
/// Output for project system permission reads/mutations.
|
||||
pub struct GetProjectSystemPermissionsOutput {
|
||||
/// Persisted system permission document.
|
||||
pub permissions: ProjectSystemPermissions,
|
||||
}
|
||||
|
||||
/// Replaces the project default system permissions.
|
||||
pub struct UpdateProjectSystemPermissions {
|
||||
store: Arc<dyn SystemPermissionStore>,
|
||||
}
|
||||
|
||||
impl UpdateProjectSystemPermissions {
|
||||
/// Builds the use case.
|
||||
#[must_use]
|
||||
pub fn new(store: Arc<dyn SystemPermissionStore>) -> Self {
|
||||
Self { store }
|
||||
}
|
||||
|
||||
/// Executes the mutation.
|
||||
pub async fn execute(
|
||||
&self,
|
||||
input: UpdateProjectSystemPermissionsInput,
|
||||
) -> Result<GetProjectSystemPermissionsOutput, AppError> {
|
||||
let mut doc = self.store.load_system_permissions(&input.project).await?;
|
||||
doc.set_project_default(input.permissions);
|
||||
self.store
|
||||
.save_system_permissions(&input.project, &doc)
|
||||
.await?;
|
||||
Ok(GetProjectSystemPermissionsOutput { permissions: doc })
|
||||
}
|
||||
}
|
||||
|
||||
/// Input for [`UpdateProjectSystemPermissions`].
|
||||
pub struct UpdateProjectSystemPermissionsInput {
|
||||
/// Target project.
|
||||
pub project: Project,
|
||||
/// New project default policy. `None` removes project defaults.
|
||||
pub permissions: Option<SystemPermissionSet>,
|
||||
}
|
||||
|
||||
/// Replaces one agent system permission override.
|
||||
pub struct UpdateAgentSystemPermissions {
|
||||
store: Arc<dyn SystemPermissionStore>,
|
||||
}
|
||||
|
||||
impl UpdateAgentSystemPermissions {
|
||||
/// Builds the use case.
|
||||
#[must_use]
|
||||
pub fn new(store: Arc<dyn SystemPermissionStore>) -> Self {
|
||||
Self { store }
|
||||
}
|
||||
|
||||
/// Executes the mutation.
|
||||
pub async fn execute(
|
||||
&self,
|
||||
input: UpdateAgentSystemPermissionsInput,
|
||||
) -> Result<GetProjectSystemPermissionsOutput, AppError> {
|
||||
let mut doc = self.store.load_system_permissions(&input.project).await?;
|
||||
doc.set_agent_permissions(input.agent_id, input.permissions);
|
||||
self.store
|
||||
.save_system_permissions(&input.project, &doc)
|
||||
.await?;
|
||||
Ok(GetProjectSystemPermissionsOutput { permissions: doc })
|
||||
}
|
||||
}
|
||||
|
||||
/// Input for [`UpdateAgentSystemPermissions`].
|
||||
pub struct UpdateAgentSystemPermissionsInput {
|
||||
/// Target project.
|
||||
pub project: Project,
|
||||
/// Target agent.
|
||||
pub agent_id: AgentId,
|
||||
/// New agent policy. `None` removes the override.
|
||||
pub permissions: Option<SystemPermissionSet>,
|
||||
}
|
||||
|
||||
/// Resolves effective system permissions for one agent.
|
||||
pub struct ResolveAgentSystemPermissions {
|
||||
store: Arc<dyn SystemPermissionStore>,
|
||||
runtime_probe: Arc<dyn RuntimePermissionProbe>,
|
||||
}
|
||||
|
||||
impl ResolveAgentSystemPermissions {
|
||||
/// Builds the use case.
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
store: Arc<dyn SystemPermissionStore>,
|
||||
runtime_probe: Arc<dyn RuntimePermissionProbe>,
|
||||
) -> Self {
|
||||
Self {
|
||||
store,
|
||||
runtime_probe,
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes the resolution.
|
||||
pub async fn execute(
|
||||
&self,
|
||||
input: ResolveAgentSystemPermissionsInput,
|
||||
) -> Result<ResolveAgentSystemPermissionsOutput, AppError> {
|
||||
let doc = self.store.load_system_permissions(&input.project).await?;
|
||||
let runtime = self
|
||||
.runtime_probe
|
||||
.probe_runtime_permissions(&input.project, input.agent_id)
|
||||
.await?;
|
||||
Ok(ResolveAgentSystemPermissionsOutput {
|
||||
permissions: resolve_agent_system_permissions(&doc, input.agent_id, runtime),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Input for [`ResolveAgentSystemPermissions`].
|
||||
pub struct ResolveAgentSystemPermissionsInput {
|
||||
/// Target project.
|
||||
pub project: Project,
|
||||
/// Target agent.
|
||||
pub agent_id: AgentId,
|
||||
}
|
||||
|
||||
/// Output for [`ResolveAgentSystemPermissions`].
|
||||
pub struct ResolveAgentSystemPermissionsOutput {
|
||||
/// Resolved system permissions.
|
||||
pub permissions: ResolvedAgentSystemPermissions,
|
||||
}
|
||||
154
crates/application/tests/system_permission_usecases.rs
Normal file
154
crates/application/tests/system_permission_usecases.rs
Normal file
@ -0,0 +1,154 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use application::{
|
||||
ResolveAgentSystemPermissions, ResolveAgentSystemPermissionsInput,
|
||||
UpdateAgentSystemPermissions, UpdateAgentSystemPermissionsInput,
|
||||
UpdateProjectSystemPermissions, UpdateProjectSystemPermissionsInput,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use domain::ids::{AgentId, ProjectId};
|
||||
use domain::ports::{RuntimeError, RuntimePermissionProbe, StoreError, SystemPermissionStore};
|
||||
use domain::project::{Project, ProjectPath};
|
||||
use domain::remote::RemoteRef;
|
||||
use domain::{
|
||||
NetworkPolicy, ProjectSystemPermissions, RuntimeLockState, RuntimePermissionSnapshot,
|
||||
SystemPermissionControlMode, SystemPermissionSet,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
struct FakeSystemPermissionStore {
|
||||
doc: Mutex<ProjectSystemPermissions>,
|
||||
saves: Mutex<usize>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SystemPermissionStore for FakeSystemPermissionStore {
|
||||
async fn load_system_permissions(
|
||||
&self,
|
||||
_project: &Project,
|
||||
) -> Result<ProjectSystemPermissions, StoreError> {
|
||||
Ok(self.doc.lock().unwrap().clone())
|
||||
}
|
||||
|
||||
async fn save_system_permissions(
|
||||
&self,
|
||||
_project: &Project,
|
||||
permissions: &ProjectSystemPermissions,
|
||||
) -> Result<(), StoreError> {
|
||||
*self.doc.lock().unwrap() = permissions.clone();
|
||||
*self.saves.lock().unwrap() += 1;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct LockedProbe;
|
||||
|
||||
#[async_trait]
|
||||
impl RuntimePermissionProbe for LockedProbe {
|
||||
async fn probe_runtime_permissions(
|
||||
&self,
|
||||
_project: &Project,
|
||||
_agent_id: AgentId,
|
||||
) -> Result<RuntimePermissionSnapshot, RuntimeError> {
|
||||
Ok(RuntimePermissionSnapshot::locked_uninspectable())
|
||||
}
|
||||
}
|
||||
|
||||
fn project() -> Project {
|
||||
Project::new(
|
||||
ProjectId::new_random(),
|
||||
"system-permissions",
|
||||
ProjectPath::new("/home/me/proj").unwrap(),
|
||||
RemoteRef::local(),
|
||||
1_700_000_000_000,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_project_system_permissions_replaces_defaults_and_persists() {
|
||||
let store = Arc::new(FakeSystemPermissionStore::default());
|
||||
let use_case = UpdateProjectSystemPermissions::new(store.clone());
|
||||
|
||||
let out = use_case
|
||||
.execute(UpdateProjectSystemPermissionsInput {
|
||||
project: project(),
|
||||
permissions: Some(SystemPermissionSet::new(Some(NetworkPolicy::Ask))),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
out.permissions.project_default.unwrap().network,
|
||||
Some(NetworkPolicy::Ask)
|
||||
);
|
||||
assert_eq!(*store.saves.lock().unwrap(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_agent_system_permissions_adds_and_removes_sparse_override() {
|
||||
let store = Arc::new(FakeSystemPermissionStore::default());
|
||||
let use_case = UpdateAgentSystemPermissions::new(store.clone());
|
||||
let agent = AgentId::new_random();
|
||||
|
||||
use_case
|
||||
.execute(UpdateAgentSystemPermissionsInput {
|
||||
project: project(),
|
||||
agent_id: agent,
|
||||
permissions: Some(SystemPermissionSet::new(Some(NetworkPolicy::Deny))),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
store
|
||||
.doc
|
||||
.lock()
|
||||
.unwrap()
|
||||
.agent_permissions(agent)
|
||||
.unwrap()
|
||||
.network,
|
||||
Some(NetworkPolicy::Deny)
|
||||
);
|
||||
|
||||
let out = use_case
|
||||
.execute(UpdateAgentSystemPermissionsInput {
|
||||
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_system_permissions_exposes_locked_runtime_read_only() {
|
||||
let agent = AgentId::new_random();
|
||||
let store = Arc::new(FakeSystemPermissionStore {
|
||||
doc: Mutex::new(ProjectSystemPermissions::new(
|
||||
Some(SystemPermissionSet::new(Some(NetworkPolicy::Allow))),
|
||||
vec![],
|
||||
)),
|
||||
saves: Mutex::new(0),
|
||||
});
|
||||
let use_case = ResolveAgentSystemPermissions::new(store, Arc::new(LockedProbe));
|
||||
|
||||
let out = use_case
|
||||
.execute(ResolveAgentSystemPermissionsInput {
|
||||
project: project(),
|
||||
agent_id: agent,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(out.permissions.wanted, Some(NetworkPolicy::Allow));
|
||||
assert_eq!(out.permissions.effective, NetworkPolicy::Deny);
|
||||
assert_eq!(out.permissions.runtime_lock.state, RuntimeLockState::Locked);
|
||||
assert_eq!(
|
||||
out.permissions.control.mode,
|
||||
SystemPermissionControlMode::ReadOnly
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user