mod helpers;
use std::fs;
use std::path::Path;
use std::sync::Arc;
use omnigraph::db::{Omnigraph, ReadTarget, SchemaApplyOptions};
use omnigraph::error::OmniError;
use omnigraph::loader::LoadMode;
use omnigraph_policy::{PolicyChecker, PolicyEngine};
use helpers::*;
const POLICY_YAML: &str = r#"
version: 1
groups:
writers: [act-allowed]
readers: [act-denied]
protected_branches: [main]
rules:
- id: writers-data
allow:
actors: { group: writers }
actions: [change]
branch_scope: any
- id: writers-branches-schema
allow:
actors: { group: writers }
actions: [schema_apply, branch_create, branch_delete, branch_merge]
target_branch_scope: any
"#;
fn additive_schema() -> String {
helpers::TEST_SCHEMA.replace(
" age: I32?\n}",
" age: I32?\n nickname: String?\n}",
)
}
fn install_policy(db: Omnigraph, dir_path: &Path) -> (Omnigraph, Arc<PolicyEngine>) {
let policy_path = dir_path.join("policy.yaml");
fs::write(&policy_path, POLICY_YAML).unwrap();
let engine = PolicyEngine::load_graph(&policy_path, dir_path.to_str().unwrap()).unwrap();
let engine = Arc::new(engine);
let db = db.with_policy(Arc::clone(&engine) as Arc<dyn PolicyChecker>);
(db, engine)
}
async fn init_with_policy(dir: &tempfile::TempDir) -> (Omnigraph, Arc<PolicyEngine>) {
let db = init_and_load(dir).await;
install_policy(db, dir.path())
}
async fn init_with_policy_and_feature_branch(
dir: &tempfile::TempDir,
branch: &str,
) -> (Omnigraph, Arc<PolicyEngine>) {
let db = init_and_load(dir).await;
db.branch_create_from(ReadTarget::branch("main"), branch)
.await
.expect("setup: create feature branch before installing policy");
install_policy(db, dir.path())
}
const ONE_PERSON_JSONL: &str = r#"{"type": "Person", "data": {"name": "Eve"}}"#;
fn assert_denied(result: Result<impl std::fmt::Debug, OmniError>, what: &str) {
match result {
Err(OmniError::Policy(msg)) => {
assert!(
msg.contains("denied"),
"{what}: expected denial message, got: {msg}"
);
}
Err(other) => panic!("{what}: expected OmniError::Policy, got: {other:?}"),
Ok(value) => panic!("{what}: expected denial, got Ok({value:?})"),
}
}
#[tokio::test]
async fn apply_schema_as_denies_when_policy_rejects_actor() {
let dir = tempfile::tempdir().unwrap();
let (db, _engine) = init_with_policy(&dir).await;
let desired = additive_schema();
let result = db
.apply_schema_as(&desired, SchemaApplyOptions::default(), Some("act-denied"))
.await;
match result {
Err(OmniError::Policy(msg)) => {
assert!(
msg.contains("denied"),
"expected denial message, got: {msg}"
);
}
Err(other) => panic!("expected OmniError::Policy, got: {other:?}"),
Ok(_) => panic!("expected denial — act-denied should not be able to SchemaApply"),
}
}
#[tokio::test]
async fn apply_schema_as_allows_when_policy_permits_actor() {
let dir = tempfile::tempdir().unwrap();
let (db, _engine) = init_with_policy(&dir).await;
let desired = additive_schema();
let result = db
.apply_schema_as(&desired, SchemaApplyOptions::default(), Some("act-allowed"))
.await
.expect("act-allowed should be able to SchemaApply");
assert!(result.applied);
}
#[tokio::test]
async fn apply_schema_without_actor_when_policy_is_installed_denies() {
let dir = tempfile::tempdir().unwrap();
let (db, _engine) = init_with_policy(&dir).await;
let desired = additive_schema();
let result = db.apply_schema(&desired).await;
match result {
Err(OmniError::Policy(msg)) => {
assert!(
msg.contains("no actor"),
"expected 'no actor' message, got: {msg}"
);
}
Err(other) => panic!("expected OmniError::Policy('no actor ...'), got: {other:?}"),
Ok(_) => panic!("expected denial — policy is installed but no actor was threaded"),
}
}
#[tokio::test]
async fn apply_schema_without_policy_still_works() {
let dir = tempfile::tempdir().unwrap();
let db = init_and_load(&dir).await;
let desired = additive_schema();
db.apply_schema(&desired)
.await
.expect("no policy → no enforcement → apply succeeds");
}
#[tokio::test]
async fn mutate_as_denies_when_policy_rejects_actor() {
let dir = tempfile::tempdir().unwrap();
let (db, _engine) = init_with_policy(&dir).await;
let params = mixed_params(&[("$name", "Eve")], &[("$age", 22)]);
let result = db
.mutate_as(
"main",
MUTATION_QUERIES,
"insert_person",
¶ms,
Some("act-denied"),
)
.await;
assert_denied(result, "mutate_as");
}
#[tokio::test]
async fn mutate_as_allows_when_policy_permits_actor() {
let dir = tempfile::tempdir().unwrap();
let (db, _engine) = init_with_policy(&dir).await;
let params = mixed_params(&[("$name", "Eve")], &[("$age", 22)]);
db.mutate_as(
"main",
MUTATION_QUERIES,
"insert_person",
¶ms,
Some("act-allowed"),
)
.await
.expect("act-allowed should be able to Change on main");
}
#[tokio::test]
async fn load_as_denies_when_policy_rejects_actor() {
let dir = tempfile::tempdir().unwrap();
let (db, _engine) = init_with_policy(&dir).await;
let result = db
.load_as(
"main",
ONE_PERSON_JSONL,
LoadMode::Merge,
Some("act-denied"),
)
.await;
assert_denied(result, "load_as");
}
#[tokio::test]
async fn load_as_allows_when_policy_permits_actor() {
let dir = tempfile::tempdir().unwrap();
let (db, _engine) = init_with_policy(&dir).await;
db.load_as(
"main",
ONE_PERSON_JSONL,
LoadMode::Merge,
Some("act-allowed"),
)
.await
.expect("act-allowed should be able to load on main");
}
#[tokio::test]
async fn load_file_as_denies_when_policy_rejects_actor() {
let dir = tempfile::tempdir().unwrap();
let (db, _engine) = init_with_policy(&dir).await;
let data_path = dir.path().join("one-person.jsonl");
fs::write(&data_path, ONE_PERSON_JSONL).unwrap();
let result = db
.load_file_as(
"main",
data_path.to_str().unwrap(),
LoadMode::Merge,
Some("act-denied"),
)
.await;
assert_denied(result, "load_file_as");
}
#[tokio::test]
async fn load_file_as_allows_when_policy_permits_actor() {
let dir = tempfile::tempdir().unwrap();
let (db, _engine) = init_with_policy(&dir).await;
let data_path = dir.path().join("one-person.jsonl");
fs::write(&data_path, ONE_PERSON_JSONL).unwrap();
db.load_file_as(
"main",
data_path.to_str().unwrap(),
LoadMode::Merge,
Some("act-allowed"),
)
.await
.expect("act-allowed should be able to load_file_as on main");
}
#[tokio::test]
async fn ingest_as_denies_when_policy_rejects_actor() {
let dir = tempfile::tempdir().unwrap();
let (db, _engine) = init_with_policy(&dir).await;
let result = db
.ingest_as(
"main",
Some("main"),
ONE_PERSON_JSONL,
LoadMode::Merge,
Some("act-denied"),
)
.await;
assert_denied(result, "ingest_as");
}
#[tokio::test]
async fn ingest_as_allows_when_policy_permits_actor() {
let dir = tempfile::tempdir().unwrap();
let (db, _engine) = init_with_policy(&dir).await;
db.ingest_as(
"main",
Some("main"),
ONE_PERSON_JSONL,
LoadMode::Merge,
Some("act-allowed"),
)
.await
.expect("act-allowed should be able to ingest on main");
}
#[tokio::test]
async fn branch_create_as_denies_when_policy_rejects_actor() {
let dir = tempfile::tempdir().unwrap();
let (db, _engine) = init_with_policy(&dir).await;
let result = db.branch_create_as("feature", Some("act-denied")).await;
assert_denied(result, "branch_create_as");
}
#[tokio::test]
async fn branch_create_as_allows_when_policy_permits_actor() {
let dir = tempfile::tempdir().unwrap();
let (db, _engine) = init_with_policy(&dir).await;
db.branch_create_as("feature", Some("act-allowed"))
.await
.expect("act-allowed should be able to BranchCreate");
}
#[tokio::test]
async fn branch_create_from_as_denies_when_policy_rejects_actor() {
let dir = tempfile::tempdir().unwrap();
let (db, _engine) = init_with_policy(&dir).await;
let result = db
.branch_create_from_as(ReadTarget::branch("main"), "feature", Some("act-denied"))
.await;
assert_denied(result, "branch_create_from_as");
}
#[tokio::test]
async fn branch_create_from_as_allows_when_policy_permits_actor() {
let dir = tempfile::tempdir().unwrap();
let (db, _engine) = init_with_policy(&dir).await;
db.branch_create_from_as(ReadTarget::branch("main"), "feature", Some("act-allowed"))
.await
.expect("act-allowed should be able to BranchCreate from main");
}
#[tokio::test]
async fn branch_delete_as_denies_when_policy_rejects_actor() {
let dir = tempfile::tempdir().unwrap();
let (db, _engine) = init_with_policy_and_feature_branch(&dir, "feature").await;
let result = db.branch_delete_as("feature", Some("act-denied")).await;
assert_denied(result, "branch_delete_as");
}
#[tokio::test]
async fn branch_delete_as_allows_when_policy_permits_actor() {
let dir = tempfile::tempdir().unwrap();
let (db, _engine) = init_with_policy_and_feature_branch(&dir, "feature").await;
db.branch_delete_as("feature", Some("act-allowed"))
.await
.expect("act-allowed should be able to BranchDelete");
}
#[tokio::test]
async fn branch_merge_as_denies_when_policy_rejects_actor() {
let dir = tempfile::tempdir().unwrap();
let (db, _engine) = init_with_policy_and_feature_branch(&dir, "feature").await;
let result = db
.branch_merge_as("feature", "main", Some("act-denied"))
.await;
assert_denied(result, "branch_merge_as");
}
#[tokio::test]
async fn branch_merge_as_allows_when_policy_permits_actor() {
let dir = tempfile::tempdir().unwrap();
let (db, _engine) = init_with_policy_and_feature_branch(&dir, "feature").await;
db.branch_merge_as("feature", "main", Some("act-allowed"))
.await
.expect("act-allowed should be able to BranchMerge");
}