mod common;
use std::collections::BTreeSet;
use std::sync::Arc;
use common::*;
use ikigai_core::{Capability, Error, Space, Verb};
use ikigai_script::authority::{effective, grant_at_publish, same_for_all, Ceiling};
use ikigai_script::plan::expand_families;
const LEDGER: &str = "library";
const GONK_CEILING: [&str; 2] = [
"urn:cap:ledger:read:*",
"urn:cap:store:read:graph:urn:iki:ledger:graph:*",
];
const SECRET_GRAPH: &str = "urn:test:secret:graph";
fn ledger_read(ledger: &str) -> [String; 2] {
[
format!("urn:cap:ledger:read:{ledger}"),
format!("urn:cap:store:read:graph:urn:iki:ledger:graph:{ledger}"),
]
}
fn reading(id: &str, resource: &str) -> String {
format!(
"@prefix ik: <https://ikigai-rs.dev/ns#> .\n\
<urn:plan:{id}> a ik:Process ; ik:step <urn:plan:{id}:step:1> ;\n \
ik:result <urn:plan:{id}:step:1> .\n\
<urn:plan:{id}:step:1> a ik:Step ; ik:verb \"Source\" ;\n \
ik:resolves <urn:iki:ledger:{LEDGER}:{resource}> .\n"
)
}
fn outside() -> String {
format!(
"@prefix ik: <https://ikigai-rs.dev/ns#> .\n\
<urn:plan:outside> a ik:Process ; ik:step <urn:plan:outside:step:1> ;\n \
ik:result <urn:plan:outside:step:1> .\n\
<urn:plan:outside:step:1> a ik:Step ; ik:verb \"Source\" ;\n \
ik:resolves <urn:iki:store:graph-select> ;\n \
ik:argument <urn:plan:outside:step:1:arg:graph> , \
<urn:plan:outside:step:1:arg:query> .\n\
<urn:plan:outside:step:1:arg:graph> a ik:Argument ; ik:inputName \"graph\" ;\n \
ik:value \"{SECRET_GRAPH}\" .\n\
<urn:plan:outside:step:1:arg:query> a ik:Argument ; ik:inputName \"query\" ;\n \
ik:value \"SELECT ?o WHERE {{ ?s ?p ?o }}\" .\n"
)
}
fn host(doors: Doors, ceiling: Ceiling) -> PlanHost {
let store = ikigai_store::DurableStore::in_memory().expect("an in-memory store");
let host = plan_host_on_over(
doors,
same_for_all(ceiling),
vec![
Arc::new(ikigai_store::space(store)) as Arc<dyn Space>,
Arc::new(ikigai_ledger::space()) as Arc<dyn Space>,
],
);
ok(
&host.kernel,
Verb::Sink,
&format!("urn:iki:ledger:{LEDGER}:append"),
&[("content", "The first item")],
);
seed(
&host.kernel,
&format!("INSERT DATA {{ GRAPH <{SECRET_GRAPH}> {{ <urn:s> <urn:p> \"secret\" }} }}"),
);
host
}
fn gonk() -> Ceiling {
Ceiling::scoped(GONK_CEILING)
}
#[derive(Clone, Copy, Debug)]
enum Who {
Root,
Exact,
}
fn capability(who: Who, script: &str, act: &str, extra: &[&str]) -> Capability {
match who {
Who::Root => Capability::root(),
Who::Exact => {
let mut scopes: Vec<String> = ledger_read(LEDGER).to_vec();
scopes.push(format!("urn:cap:script:{act}:{script}"));
scopes.extend(extra.iter().map(|s| s.to_string()));
Capability::scoped(scopes)
}
}
}
fn publish_as(host: &PlanHost, who: Who, name: &str, plan: &str, extra: &[&str]) {
call(
&host.kernel,
&capability(who, name, "write", extra),
Verb::Sink,
&format!("urn:script:{name}"),
&[("content", plan), ("language", "plan")],
)
.unwrap_or_else(|e| panic!("publishing {name} as {who:?}: {e}"));
}
fn run_as(
host: &PlanHost,
who: Who,
name: &str,
extra: &[&str],
) -> std::result::Result<String, Error> {
call(
&host.kernel,
&capability(who, name, "run", extra),
Verb::Source,
&format!("urn:script:{name}:result"),
&[],
)
}
const READS: [(&str, &str); 2] = [("nextwork", "next"), ("allitems", "items")];
#[test]
fn an_exact_publisher_or_runner_reads_next_and_items_under_gonks_ceiling() {
both(|doors| {
for (publisher, runner) in [
(Who::Root, Who::Exact),
(Who::Exact, Who::Root),
(Who::Exact, Who::Exact),
] {
let host = host(doors, gonk());
for (name, resource) in READS {
publish_as(&host, publisher, name, &reading(name, resource), &[]);
let answer = run_as(&host, runner, name, &[]).unwrap_or_else(|e| {
panic!("{resource}, published {publisher:?}, run {runner:?}: {e}")
});
assert!(
answer.contains("The first item"),
"{resource}, published {publisher:?}, run {runner:?}: {answer}"
);
}
}
});
}
#[test]
fn root_published_and_root_run_is_refused_legibly_under_a_family_only_ceiling() {
both(|doors| {
let host = host(doors, gonk());
for (name, resource) in READS {
publish_as(&host, Who::Root, name, &reading(name, resource), &[]);
let message = denied(run_as(&host, Who::Root, name, &[]));
assert!(
message.contains(&format!("urn:cap:ledger:read:{LEDGER}")),
"{resource}: {message}"
);
assert!(
message.contains("a family held is not a grant")
&& message.contains("urn:cap:store:read:graph:urn:iki:ledger:graph:*")
&& message.contains("ceiling"),
"{resource}: {message}"
);
}
});
}
#[test]
fn a_root_run_keeps_the_meet_of_each_declared_family_and_the_ceiling() {
let declared: BTreeSet<String> = ["urn:cap:ledger:read:*", "urn:cap:store:read:graph:*"]
.iter()
.map(|s| s.to_string())
.collect();
let grant = grant_at_publish(&Capability::root(), &declared).unwrap();
let keep = effective(&declared, &grant.granted, &grant.exclusions, &gonk());
let expanded = expand_families(keep, &Capability::root(), &gonk());
assert_eq!(
expanded,
GONK_CEILING.iter().map(|s| s.to_string()).collect(),
"the ledger family whole (the ceiling admits it whole) and the store family as \
narrowed by the ceiling"
);
let runner = Capability::scoped(ledger_read(LEDGER));
let keep = effective(&declared, &grant.granted, &grant.exclusions, &gonk());
assert_eq!(
expand_families(keep, &runner, &gonk()),
ledger_read(LEDGER).into_iter().collect()
);
}
#[test]
fn a_ceiling_naming_the_members_runs_root_published_root_run() {
both(|doors| {
let mut lines: Vec<String> = GONK_CEILING.iter().map(|s| s.to_string()).collect();
lines.extend(ledger_read(LEDGER));
let host = host(doors, Ceiling::scoped(lines));
for (name, resource) in READS {
publish_as(&host, Who::Root, name, &reading(name, resource), &[]);
let answer = run_as(&host, Who::Root, name, &[])
.unwrap_or_else(|e| panic!("{resource}, root/root, members named: {e}"));
assert!(answer.contains("The first item"), "{resource}: {answer}");
}
});
}
#[test]
fn a_step_outside_the_ceiling_is_refused_in_every_combination() {
both(|doors| {
let secret = format!("urn:cap:store:read:graph:{SECRET_GRAPH}");
for (publisher, runner) in [
(Who::Root, Who::Root),
(Who::Root, Who::Exact),
(Who::Exact, Who::Root),
(Who::Exact, Who::Exact),
] {
let host = host(doors, gonk());
publish_as(&host, publisher, "outside", &outside(), &[&secret]);
let message = denied(run_as(&host, runner, "outside", &[&secret]));
assert!(
message.contains("urn:cap:store:read:graph"),
"published {publisher:?}, run {runner:?}: {message}"
);
if let (Who::Exact, Who::Exact) = (publisher, runner) {
let open = self::host(doors, Ceiling::unbounded());
publish_as(&open, publisher, "outside", &outside(), &[&secret]);
let answer = run_as(&open, runner, "outside", &[&secret]).unwrap();
assert!(answer.contains("secret"), "{answer}");
}
}
});
}