kcode-k1-access-persons 0.1.0

Access-controlled facade for standalone K1 persons
Documentation
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()
    );
}