use std::{path::Path, sync::Arc};
use kcode_k1_access::{
AccessId, Authorizations, K1Access, ModelId, OwnerSubject, RequestPrincipal, SubsystemId,
Target, TxId, UserId, ViewerSubject,
};
use kcode_k1_access_persons::{K1AccessPersons, PersonId, PersonView};
use kcode_k1_access_profiles::{
AuthorizationProfile, K1AccessProfiles, ProfileId, ProfileOwner, ProfileSelection,
};
use kcode_k1_groups::K1Groups;
use kcode_k1_peering::K1Peering;
use kcode_k1_persons::K1Persons;
use kcode_k1_txn_ordering::K1TxnOrdering;
use tempfile::TempDir;
struct Stack {
ordering: Arc<K1TxnOrdering>,
peering: Arc<K1Peering>,
persons: Arc<K1Persons>,
access: Arc<K1Access>,
facade: K1AccessPersons,
}
impl Stack {
fn open(root: &Path) -> Self {
let ordering = Arc::new(K1TxnOrdering::open(&root.join("ordering")).unwrap());
let peering = Arc::new(K1Peering::open(&root.join("peering"), ordering.clone()).unwrap());
let groups = Arc::new(
K1Groups::open(&root.join("groups"), ordering.clone(), peering.clone()).unwrap(),
);
let profiles = Arc::new(
K1AccessProfiles::open(&root.join("profiles"), ordering.clone(), peering.clone())
.unwrap(),
);
let persons = Arc::new(
K1Persons::open(&root.join("persons"), ordering.clone(), peering.clone()).unwrap(),
);
let access = Arc::new(
K1Access::open(
&root.join("access"),
ordering.clone(),
peering.clone(),
groups,
)
.unwrap(),
);
let facade = K1AccessPersons::open(access.clone(), profiles, persons.clone()).unwrap();
Self {
ordering,
peering,
persons,
access,
facade,
}
}
}
fn tx(value: u8) -> TxId {
TxId::from_bytes([value; 12])
}
fn user(value: u8) -> UserId {
UserId::from_tx_id(tx(value))
}
fn model(value: u8) -> ModelId {
ModelId::from_bytes([value; 32])
}
fn principal(user_value: u8, model_value: u8) -> RequestPrincipal {
RequestPrincipal::new(user(user_value), model(model_value))
}
fn subsystem(name: &str) -> SubsystemId {
SubsystemId::from_str(name).unwrap()
}
fn profile() -> ProfileSelection {
ProfileSelection::Inline(
AuthorizationProfile::new(vec![ProfileOwner::RequestUser], vec![]).unwrap(),
)
}
fn view(person_id: PersonId, name: &str) -> Option<PersonView> {
Some(PersonView {
person_id,
name: name.to_owned(),
})
}
fn permissions(principal: RequestPrincipal) -> Authorizations {
Authorizations::new(
vec![OwnerSubject::User(principal.user())],
vec![ViewerSubject::Model(principal.model())],
)
.unwrap()
}
#[test]
fn universal_viewers_preserve_owner_management_and_read_current_person() {
let root = TempDir::new().unwrap();
let stack = Stack::open(root.path());
let owner = principal(1, 11);
let unrelated = principal(2, 12);
let submitted = stack
.facade
.create(owner, profile(), "Ada".to_owned())
.unwrap();
assert_eq!(
stack.facade.read(unrelated, submitted.access_id).unwrap(),
view(submitted.person_id, "Ada").unwrap()
);
assert_eq!(
stack.facade.read_person(submitted.person_id).unwrap(),
view(submitted.person_id, "Ada").unwrap()
);
stack
.facade
.update(owner, submitted.access_id, "Ada Lovelace".to_owned())
.unwrap();
assert_eq!(
stack.facade.read_person(submitted.person_id).unwrap(),
view(submitted.person_id, "Ada Lovelace").unwrap()
);
assert_eq!(
stack
.facade
.update(unrelated, submitted.access_id, "Denied".to_owned())
.unwrap_err(),
"principal must be able to view and manage person"
);
assert_eq!(
stack.persons.read(submitted.person_id).unwrap(),
view(submitted.person_id, "Ada Lovelace")
);
}
#[test]
fn update_person_requires_its_matching_managed_access_and_changes_once() {
let root = TempDir::new().unwrap();
let stack = Stack::open(root.path());
let owner = principal(1, 11);
let other = stack
.facade
.create(owner, profile(), "Other".to_owned())
.unwrap();
let submitted = stack
.facade
.create(owner, profile(), "Before".to_owned())
.unwrap();
assert_eq!(
stack
.facade
.update_person(
owner,
submitted.person_id,
other.access_id,
"Denied".to_owned(),
)
.unwrap_err(),
"person access target does not match supplied person"
);
assert_eq!(
stack.persons.read(submitted.person_id).unwrap(),
view(submitted.person_id, "Before")
);
assert_eq!(
stack.persons.read(other.person_id).unwrap(),
view(other.person_id, "Other")
);
stack
.facade
.update_person(
owner,
submitted.person_id,
submitted.access_id,
"After".to_owned(),
)
.unwrap();
assert_eq!(
stack.facade.read_person(submitted.person_id).unwrap(),
view(submitted.person_id, "After").unwrap()
);
}
#[test]
fn rejected_update_person_accesses_never_mutate() {
let root = TempDir::new().unwrap();
let stack = Stack::open(root.path());
let owner = principal(1, 11);
let viewer = principal(2, 12);
let submitted = stack
.facade
.create(owner, profile(), "Protected".to_owned())
.unwrap();
let wrong = stack
.access
.create(
Target::new(
subsystem("other"),
submitted.person_id.as_tx_id().as_bytes().to_vec(),
),
permissions(owner),
)
.unwrap()
.access_id();
assert_eq!(
stack
.facade
.update_person(
viewer,
submitted.person_id,
submitted.access_id,
"Denied".to_owned(),
)
.unwrap_err(),
"principal must be able to view and manage person"
);
assert!(
stack
.facade
.update_person(
owner,
submitted.person_id,
AccessId::new(tx(99)),
"Denied".to_owned(),
)
.is_err()
);
assert!(
stack
.facade
.update_person(owner, submitted.person_id, wrong, "Denied".to_owned(),)
.is_err()
);
assert_eq!(
stack.persons.read(submitted.person_id).unwrap(),
view(submitted.person_id, "Protected")
);
}
#[test]
fn alias_target_cannot_mutate_canonical_person() {
let root = TempDir::new().unwrap();
let stack = Stack::open(root.path());
let owner = principal(1, 11);
let submitted = stack
.facade
.create(owner, profile(), "Alias".to_owned())
.unwrap();
let canonical = stack.persons.create("Canonical".to_owned()).unwrap();
assert_eq!(
stack
.persons
.resolve(canonical, submitted.person_id)
.unwrap(),
canonical
);
assert_eq!(
stack.facade.read_person(submitted.person_id).unwrap(),
view(canonical, "Canonical").unwrap()
);
assert_eq!(
stack
.facade
.update_person(
owner,
submitted.person_id,
submitted.access_id,
"Hijacked".to_owned(),
)
.unwrap_err(),
"person access target is no longer canonical"
);
assert_eq!(
stack.persons.read(canonical).unwrap(),
view(canonical, "Canonical")
);
}
#[test]
fn profile_failure_is_pre_effect_and_access_failure_can_orphan() {
let root = TempDir::new().unwrap();
let stack = Stack::open(root.path());
let before = stack.ordering.tip();
assert_eq!(
stack
.facade
.create(
principal(1, 11),
ProfileSelection::Saved(ProfileId::new(tx(99))),
"Never".to_owned(),
)
.unwrap_err(),
"profile is unavailable"
);
assert_eq!(stack.ordering.tip(), before);
let poison = stack
.peering
.submit_txn(subsystem("k1-access-subsystem"), &[0])
.unwrap_err();
assert!(poison.contains("committed"));
let poisoned_tip = stack.ordering.tip().unwrap();
assert_eq!(
stack
.facade
.create(principal(1, 11), profile(), "Orphan".to_owned())
.unwrap_err(),
"k1 access driver unavailable"
);
let orphan = PersonId::from_tx_id(stack.ordering.tip().unwrap());
assert_ne!(orphan.as_tx_id(), poisoned_tip);
assert_eq!(stack.persons.read(orphan).unwrap(), view(orphan, "Orphan"));
}
#[test]
fn successful_access_and_person_survive_restart() {
let root = TempDir::new().unwrap();
let owner = principal(1, 11);
let submitted = {
let stack = Stack::open(root.path());
let submitted = stack
.facade
.create(owner, profile(), "Before".to_owned())
.unwrap();
stack
.facade
.update_person(
owner,
submitted.person_id,
submitted.access_id,
"After".to_owned(),
)
.unwrap();
submitted
};
let reopened = Stack::open(root.path());
assert_eq!(
reopened.facade.read_person(submitted.person_id).unwrap(),
view(submitted.person_id, "After").unwrap()
);
assert_eq!(
reopened.facade.read(owner, submitted.access_id).unwrap(),
view(submitted.person_id, "After").unwrap()
);
}