kcode-k1-access-state 0.2.0

In-memory K1 access projection and target index
Documentation
use super::*;
use kcode_k1_access_types::{FilteredAuthorities, ModelId, UserId, ViewerSubject};

fn tx(value: u32) -> TxId {
    let mut bytes = [0; 12];
    bytes[..4].copy_from_slice(&value.to_le_bytes());
    TxId::from_bytes(bytes)
}

fn access(value: u32) -> AccessId {
    AccessId::new(tx(value))
}

fn profile(value: u32) -> ProfileId {
    ProfileId::new(tx(value))
}

fn user(value: u32) -> UserId {
    UserId::from_tx_id(tx(value))
}

fn group(value: u32) -> GroupId {
    GroupId::new(tx(value))
}

fn model(value: u8) -> ModelId {
    ModelId::from_bytes([value; 32])
}

fn subsystem(value: &str) -> SubsystemId {
    SubsystemId::from_str(value).unwrap()
}

fn target(value: u32, subsystem_value: &str) -> Target {
    Target::new(subsystem(subsystem_value), value.to_le_bytes().to_vec())
}

fn context(user_value: u32, model_value: u8) -> AccessContext {
    AccessContext::new(
        user(user_value),
        model(model_value),
        FilteredAuthorities::empty(),
    )
    .unwrap()
}

fn filtered_context(user_value: u32, model_value: u8, authority: Authority) -> AccessContext {
    AccessContext::new(
        user(user_value),
        model(model_value),
        FilteredAuthorities::new(vec![authority]).unwrap(),
    )
    .unwrap()
}

fn policy(
    authority: Authority,
    editors: Vec<Authority>,
    viewers: Vec<ViewerSubject>,
) -> AccessPolicy {
    AccessPolicy::new(authority, editors, viewers).unwrap()
}

fn stored(
    id: u32,
    target_value: u32,
    subsystem_value: &str,
    revision: u32,
    policy: AccessPolicy,
) -> StoredAccess {
    StoredAccess::new(
        access(id),
        target(target_value, subsystem_value),
        profile(id + 100),
        tx(revision),
        policy,
    )
}

fn check(
    state: &AccessState,
    context: &AccessContext,
    id: u32,
    user_groups: &[GroupId],
    model_groups: &[GroupId],
) -> AccessCheck {
    state.check(
        context,
        access(id),
        subsystem("alpha"),
        user_groups,
        model_groups,
        Some(tx(8)),
    )
}

fn checks(state: &AccessState, ids: &[AccessId]) -> Vec<AccessCheck> {
    state.check_many(
        &context(1, 1),
        ids,
        subsystem("alpha"),
        &[group(1)],
        &[group(1)],
        Some(tx(8)),
    )
}

fn resolve(
    state: &AccessState,
    context: &AccessContext,
    targets: &[Target],
) -> Vec<Option<AccessId>> {
    state.resolve_visible_targets(
        context,
        targets,
        subsystem("alpha"),
        &[group(1)],
        &[group(1)],
    )
}

fn assert_check(check: &AccessCheck, can_view: bool, can_edit: bool, revision: Option<TxId>) {
    assert_eq!((check.can_view(), check.can_edit()), (can_view, can_edit));
    assert_eq!(check.target().is_some(), can_view);
    assert_eq!(check.access_revision(), revision);
    assert_eq!(check.groups_revision(), revision.map(|_| tx(8)));
}

fn assert_concealed(check: &AccessCheck) {
    assert_check(check, false, false, None);
}

fn shared(state: &AccessState, first: u32, second: u32) -> bool {
    Arc::ptr_eq(
        &state.records[&access(first)].policy,
        &state.records[&access(second)].policy,
    )
}

#[test]
fn initialization_and_mutations_preserve_invariants() {
    let original = policy(Authority::User(user(1)), vec![], vec![]);
    for records in [
        vec![
            stored(1, 1, "alpha", 10, original.clone()),
            stored(1, 2, "alpha", 11, original.clone()),
        ],
        vec![
            stored(1, 1, "alpha", 10, original.clone()),
            stored(2, 1, "alpha", 11, original.clone()),
        ],
    ] {
        assert!(AccessState::new(records).is_err());
    }
    let mut state = AccessState::new(vec![stored(1, 1, "alpha", 10, original.clone())]).unwrap();
    assert_eq!(state.profile_id(access(1)), Some(profile(101)));
    assert_eq!(state.profile_id(access(9)), None);
    assert!(
        state
            .create(stored(1, 2, "alpha", 11, original.clone()))
            .is_err()
    );
    assert!(
        state
            .create(stored(2, 1, "alpha", 11, original.clone()))
            .is_err()
    );
    assert!(state.replace(access(9), tx(12), original).is_err());
    let changed_authority = policy(Authority::Group(group(1)), vec![], vec![]);
    assert!(state.replace(access(1), tx(12), changed_authority).is_err());

    let replacement = policy(
        Authority::User(user(1)),
        vec![Authority::User(user(2))],
        vec![ViewerSubject::Model(model(1))],
    );
    state
        .replace(access(1), tx(12), replacement.clone())
        .unwrap();
    let record = &state.records[&access(1)];
    assert_eq!(record.target, target(1, "alpha"));
    assert_eq!(record.profile_id, profile(101));
    assert_eq!(record.revision, tx(12));
    assert_eq!(record.policy.as_ref(), &replacement);
    assert_eq!(state.profile_id(access(1)), Some(profile(101)));
    assert_check(
        &check(&state, &context(1, 1), 1, &[], &[]),
        true,
        true,
        Some(tx(12)),
    );
}

#[test]
fn policy_matrix_concealment_and_witnesses_are_exact() {
    let matrix_policy = policy(
        Authority::Group(group(1)),
        vec![Authority::User(user(2)), Authority::Group(group(2))],
        vec![
            ViewerSubject::User(user(3)),
            ViewerSubject::Group(group(3)),
            ViewerSubject::Model(model(3)),
        ],
    );
    let state = AccessState::new(vec![stored(1, 1, "alpha", 9, matrix_policy.clone())]).unwrap();
    let cases = vec![
        (context(2, 3), vec![], vec![], true, true),
        (context(2, 4), vec![], vec![], false, true),
        (context(4, 4), vec![group(1)], vec![group(1)], true, true),
        (context(4, 4), vec![group(2)], vec![group(2)], true, true),
        (context(3, 3), vec![], vec![], true, false),
        (context(4, 4), vec![group(3)], vec![group(3)], true, false),
        (context(3, 4), vec![], vec![], false, false),
        (context(4, 3), vec![], vec![], false, false),
    ];
    for (context, user_groups, model_groups, can_view, can_edit) in cases {
        let direct = evaluate(&matrix_policy, &context, &user_groups, &model_groups);
        assert_eq!((direct.can_view(), direct.can_edit()), (can_view, can_edit));
        let actual = check(&state, &context, 1, &user_groups, &model_groups);
        assert_check(
            &actual,
            can_view,
            can_edit,
            (can_view || can_edit).then_some(tx(9)),
        );
    }

    let filtered = filtered_context(4, 3, Authority::Group(group(1)));
    assert_concealed(&check(&state, &filtered, 1, &[group(1)], &[group(1)]));
    assert_concealed(&state.check(
        &context(2, 3),
        access(1),
        subsystem("other"),
        &[],
        &[],
        Some(tx(8)),
    ));
    assert_concealed(&check(&state, &context(2, 3), 99, &[], &[]));

    for witness in [
        Authority::Group(group(1)),
        Authority::User(user(2)),
        Authority::Group(group(2)),
    ] {
        assert!(state.accepts_edit_witness(access(1), witness));
    }
    assert!(!state.accepts_edit_witness(access(1), Authority::User(user(3))));
    assert!(!state.accepts_edit_witness(access(1), Authority::Group(group(3))));
    assert!(!state.accepts_edit_witness(access(99), Authority::Group(group(1))));
}

#[test]
fn batches_and_reverse_resolution_preserve_positions() {
    let visible = policy(Authority::Group(group(1)), vec![], vec![]);
    let hidden = policy(Authority::Group(group(2)), vec![], vec![]);
    let state = AccessState::new(vec![
        stored(1, 1, "alpha", 11, visible.clone()),
        stored(2, 2, "alpha", 12, hidden),
        stored(3, 3, "beta", 13, visible),
    ])
    .unwrap();
    let ids = [access(1), access(99), access(1), access(2), access(3)];
    let results = checks(&state, &ids);
    assert_eq!(results.len(), ids.len());
    assert_eq!(results[0], results[2]);
    assert_check(&results[0], true, true, Some(tx(11)));
    for result in [&results[1], &results[3], &results[4]] {
        assert_concealed(result);
    }
    assert!(checks(&state, &[]).is_empty());

    let targets = [
        target(1, "alpha"),
        target(99, "alpha"),
        target(1, "alpha"),
        target(2, "alpha"),
        target(3, "beta"),
    ];
    assert_eq!(
        resolve(&state, &context(1, 1), &targets),
        vec![Some(access(1)), None, Some(access(1)), None, None]
    );
    let filtered = filtered_context(1, 1, Authority::Group(group(1)));
    assert_eq!(resolve(&state, &filtered, &targets[..1]), vec![None]);
    assert!(resolve(&state, &context(1, 1), &[]).is_empty());
}

#[test]
fn policies_are_shared_reused_cleaned_and_scaled() {
    let first = policy(Authority::User(user(1)), vec![], vec![]);
    let second = policy(
        Authority::User(user(1)),
        vec![],
        vec![ViewerSubject::Model(model(1))],
    );
    let mut state = AccessState::new(vec![
        stored(1, 1, "alpha", 1, first.clone()),
        stored(2, 2, "alpha", 2, first.clone()),
        stored(3, 3, "alpha", 3, second.clone()),
    ])
    .unwrap();
    assert!(shared(&state, 1, 2));
    assert!(!shared(&state, 1, 3));
    state
        .create(stored(4, 4, "alpha", 4, first.clone()))
        .unwrap();
    assert!(shared(&state, 1, 4));
    for id in [1, 2, 4] {
        state
            .replace(access(id), tx(10 + id), second.clone())
            .unwrap();
        assert!(shared(&state, id, 3));
        assert_eq!(state.profile_id(access(id)), Some(profile(id + 100)));
    }
    assert!(!state.policies.contains_key(&first));
    assert_eq!(state.policies.len(), 1);

    let one_policy = policy(Authority::User(user(1)), vec![], vec![]);
    let records = (1..=10_000)
        .map(|value| stored(value, value, "alpha", 1, one_policy.clone()))
        .collect();
    let large = AccessState::new(records).unwrap();
    assert_eq!(large.records.len(), 10_000);
    assert_eq!(large.policies.len(), 1);
    assert_eq!(large.profile_id(access(10_000)), Some(profile(10_100)));
    assert!(shared(&large, 1, 10_000));
    assert_eq!(Arc::strong_count(&large.records[&access(1)].policy), 10_000);
}