use kcode_k1_access::{
AccessContext, AccessId, AccessPolicy, Authority, FilteredAuthorities, GroupId, K1Access,
ModelId, SubsystemId, Target, TxId, UserId, ViewerSubject,
};
use kcode_k1_access_profiles::{
K1AccessProfiles, ProfileColor, ProfileId, ProfileName, ProfileRevision, ProfileSelection,
ProfileSource, SavedProfile,
};
use kcode_k1_groups::{GroupName, GroupRole, K1Groups};
use kcode_k1_peering::K1Peering;
use kcode_k1_txn_ordering::K1TxnOrdering;
use std::fmt::Debug;
use std::path::Path;
use std::sync::Arc;
use tempfile::TempDir;
const SUBSYSTEM: &str = "k1-profile-access";
const TARGET_PREFIX: &[u8] = b"\x00profile-access\xff";
pub fn verify() -> Result<(), String> {
let root =
TempDir::new().map_err(|error| format!("temporary root creation failed: {error}"))?;
scenario(&Stack::open(root.path())?)
}
struct Stack {
groups: Arc<K1Groups>,
profiles: K1AccessProfiles,
access: K1Access,
}
impl Stack {
fn open(root: &Path) -> Result<Self, String> {
let ordering = Arc::new(op(
K1TxnOrdering::open(&root.join("ordering")),
"open Ordering",
)?);
let peering = Arc::new(op(
K1Peering::open(&root.join("peering"), ordering.clone()),
"open Peering",
)?);
let groups = Arc::new(op(
K1Groups::open(&root.join("groups"), ordering.clone(), peering.clone()),
"open Groups",
)?);
let profiles = op(
K1AccessProfiles::open(&root.join("profiles"), ordering.clone(), peering.clone()),
"open Profiles",
)?;
let access = op(
K1Access::open(&root.join("access"), ordering, peering, groups.clone()),
"open Access",
)?;
Ok(Self {
groups,
profiles,
access,
})
}
}
#[derive(Clone, Copy)]
struct Ids {
saved_owner: UserId,
outsider: UserId,
group_owner: UserId,
group_admin: UserId,
group_user: UserId,
direct_editor: UserId,
direct_viewer: UserId,
second_authority: UserId,
second_editor: UserId,
second_viewer: UserId,
group_model: ModelId,
direct_model: ModelId,
unrelated_model: ModelId,
second_model: ModelId,
}
impl Ids {
fn new() -> Self {
Self {
saved_owner: user(1),
outsider: user(2),
group_owner: user(3),
group_admin: user(4),
group_user: user(5),
direct_editor: user(6),
direct_viewer: user(7),
second_authority: user(8),
second_editor: user(9),
second_viewer: user(10),
group_model: model(21),
direct_model: model(22),
unrelated_model: model(23),
second_model: model(24),
}
}
fn first_cases(self) -> [Case; 9] {
[
(
self.group_owner,
self.direct_model,
true,
true,
"group Owner",
),
(
self.group_admin,
self.direct_model,
true,
true,
"group Admin",
),
(self.group_user, self.direct_model, true, true, "group User"),
(
self.direct_editor,
self.direct_model,
true,
true,
"direct editor",
),
(
self.direct_viewer,
self.direct_model,
true,
false,
"direct user viewer",
),
(
self.direct_viewer,
self.group_model,
true,
false,
"authority-group model",
),
(
self.direct_viewer,
self.unrelated_model,
false,
false,
"model gate",
),
(self.outsider, self.direct_model, false, false, "human gate"),
(
self.saved_owner,
self.direct_model,
false,
false,
"saved owner",
),
]
}
fn second_cases(self) -> [Case; 7] {
[
(
self.second_authority,
self.second_model,
true,
true,
"user authority",
),
(
self.second_editor,
self.second_model,
true,
true,
"direct editor",
),
(
self.second_viewer,
self.second_model,
true,
false,
"direct viewer",
),
(
self.second_viewer,
self.unrelated_model,
false,
false,
"model gate",
),
(
self.saved_owner,
self.second_model,
false,
false,
"saved owner",
),
(
self.direct_editor,
self.second_model,
false,
false,
"old editor",
),
(
self.second_viewer,
self.direct_model,
false,
false,
"old model",
),
]
}
}
type Case = (UserId, ModelId, bool, bool, &'static str);
struct Profile {
owner: UserId,
id: ProfileId,
revision: ProfileRevision,
name: ProfileName,
color: Option<ProfileColor>,
policy: AccessPolicy,
}
impl Profile {
fn check(&self, profiles: &K1AccessProfiles, label: &str) -> Result<(), String> {
let actual = op(profiles.get_for_user(self.owner, self.id), label)?
.ok_or_else(|| format!("{label}: profile is unavailable"))?;
equal(
&actual,
&SavedProfile::new(
self.id,
self.owner,
self.revision,
false,
self.name.clone(),
self.color.clone(),
self.policy.clone(),
),
&format!("{label} fields"),
)
}
fn resolve(&self, profiles: &K1AccessProfiles) -> Result<AccessPolicy, String> {
let resolved = op(
profiles.resolve(self.owner, ProfileSelection::Saved(self.id)),
"resolve saved profile",
)?;
equal(
&(resolved.policy(), resolved.source()),
&(
&self.policy,
ProfileSource::Saved {
profile_id: self.id,
revision: self.revision,
},
),
"resolved policy, source and revision",
)?;
Ok(resolved.into_policy())
}
fn advance(&mut self, revision: ProfileRevision, label: &str) -> Result<(), String> {
equal(
&revision.profile_id(),
&self.id,
&format!("{label} profile ID"),
)?;
require(
revision.txid() != self.revision.txid(),
&format!("{label} revision did not advance"),
)?;
self.revision = revision;
Ok(())
}
}
struct Snapshot {
id: AccessId,
revision: TxId,
subsystem: SubsystemId,
target: Vec<u8>,
profile_id: ProfileId,
}
impl Snapshot {
fn create(
access: &K1Access,
subsystem: SubsystemId,
tag: u8,
profile_id: ProfileId,
policy: AccessPolicy,
creator: (UserId, ModelId),
label: &str,
) -> Result<Self, String> {
let target = [TARGET_PREFIX, &[tag]].concat();
let created = op(
access.create(
&context(creator.0, creator.1)?,
Target::new(subsystem, target.clone()),
profile_id,
policy,
),
label,
)?;
Ok(Self {
id: created.access_id(),
revision: created.txid(),
subsystem,
target,
profile_id,
})
}
fn check(&self, access: &K1Access, cases: &[Case], stage: &str) -> Result<(), String> {
require(
access.profile_id(self.id)? == Some(self.profile_id),
&format!("{stage} saved profile link changed"),
)?;
for &(user, model, view, edit, label) in cases {
let result = op(
access.check(&context(user, model)?, self.id, self.subsystem),
&format!("{stage} {label} check"),
)?;
equal(
&(
result.can_view(),
result.can_edit(),
result.access_revision(),
),
&(view, edit, (view || edit).then_some(self.revision)),
&format!("{stage} {label} rights and revision"),
)?;
match (view, result.target()) {
(true, Some(target)) => equal(
&(target.subsystem(), target.object_id()),
&(self.subsystem, self.target.as_slice()),
&format!("{stage} {label} target"),
)?,
(false, None) => {}
_ => return Err(format!("{stage} {label}: target visibility mismatch")),
}
}
let resolver = &cases[0];
let found = op(
access.resolve_visible_targets(
&context(resolver.0, resolver.1)?,
&[Target::new(self.subsystem, self.target.clone())],
self.subsystem,
),
&format!("{stage} target resolution"),
)?;
equal(
found.as_slice(),
&[Some(self.id)],
&format!("{stage} Access ID"),
)
}
}
fn scenario(stack: &Stack) -> Result<(), String> {
let ids = Ids::new();
let subsystem = op(SubsystemId::from_str(SUBSYSTEM), "target subsystem")?;
let group = create_group(stack, ids)?;
let first_policy = op(
AccessPolicy::new(
Authority::Group(group),
vec![Authority::User(ids.direct_editor)],
vec![
ViewerSubject::User(ids.direct_viewer),
ViewerSubject::Model(ids.direct_model),
],
),
"first policy",
)?;
let created = op(
stack.profiles.create(
ids.saved_owner,
profile_name("Owner-private group policy", "profile name")?,
Some(op(ProfileColor::new("cyan".into()), "profile color")?),
first_policy.clone(),
),
"create profile",
)?;
let mut profile = Profile {
owner: ids.saved_owner,
id: created.profile_id(),
revision: created,
name: profile_name("Owner-private group policy", "profile name")?,
color: Some(op(ProfileColor::new("cyan".into()), "profile color")?),
policy: first_policy,
};
profile.check(&stack.profiles, "created profile")?;
require(
op(
stack.profiles.get_for_user(ids.outsider, profile.id),
"foreign profile lookup",
)?
.is_none(),
"foreign user could read owner-private profile",
)?;
let first = Snapshot::create(
&stack.access,
subsystem,
1,
profile.id,
profile.resolve(&stack.profiles)?,
(ids.saved_owner, ids.direct_model),
"create first Access",
)?;
let first_cases = ids.first_cases();
first.check(&stack.access, &first_cases, "initial first snapshot")?;
profile.name = profile_name("Renamed group policy", "renamed profile")?;
profile.advance(
op(
stack
.profiles
.rename(ids.saved_owner, profile.id, profile.name.clone()),
"rename profile",
)?,
"rename",
)?;
profile.color = Some(op(
ProfileColor::new("violet".into()),
"changed profile color",
)?);
profile.advance(
op(
stack
.profiles
.set_color(ids.saved_owner, profile.id, profile.color.clone()),
"recolor profile",
)?,
"recolor",
)?;
profile.check(&stack.profiles, "presented profile")?;
first.check(
&stack.access,
&first_cases,
"first snapshot after presentation changes",
)?;
profile.policy = op(
AccessPolicy::new(
Authority::User(ids.second_authority),
vec![Authority::User(ids.second_editor)],
vec![
ViewerSubject::User(ids.second_viewer),
ViewerSubject::Model(ids.second_model),
],
),
"replacement policy",
)?;
profile.advance(
op(
stack
.profiles
.replace(ids.saved_owner, profile.id, profile.policy.clone()),
"replace profile policy",
)?,
"replace",
)?;
profile.check(&stack.profiles, "replaced profile")?;
let second = Snapshot::create(
&stack.access,
subsystem,
2,
profile.id,
profile.resolve(&stack.profiles)?,
(ids.saved_owner, ids.second_model),
"create second Access",
)?;
let second_cases = ids.second_cases();
first.check(
&stack.access,
&first_cases,
"first snapshot after policy replacement",
)?;
second.check(
&stack.access,
&second_cases,
"second snapshot after creation",
)?;
profile.advance(
op(
stack.profiles.archive(ids.saved_owner, profile.id),
"archive profile",
)?,
"archive",
)?;
require(
op(
stack.profiles.get_for_user(ids.saved_owner, profile.id),
"post-archive lookup",
)?
.is_none(),
"archived profile remained available",
)?;
match stack
.profiles
.resolve(ids.saved_owner, ProfileSelection::Saved(profile.id))
{
Err(error) => equal(
error.as_str(),
"profile is unavailable",
"archived resolution",
)?,
Ok(_) => return Err("archived profile still resolved".into()),
}
match stack.profiles.archive(ids.saved_owner, profile.id) {
Err(error) => equal(error.as_str(), "profile is unavailable", "repeated archive")?,
Ok(_) => return Err("archived profile was archived again".into()),
}
first.check(&stack.access, &first_cases, "first snapshot after archival")?;
second.check(
&stack.access,
&second_cases,
"second snapshot after archival",
)
}
fn create_group(stack: &Stack, ids: Ids) -> Result<GroupId, String> {
let group = op(
stack.groups.create(
ids.group_owner,
op(GroupName::new("Access authority".into()), "group name")?,
),
"create group",
)?
.group_id();
for (user, role, label) in [
(ids.group_admin, GroupRole::Admin, "add group Admin"),
(ids.group_user, GroupRole::User, "add group User"),
] {
op(
stack
.groups
.set_user_role(ids.group_owner, group, user, Some(role)),
label,
)?;
}
op(
stack
.groups
.set_model_membership(ids.group_owner, group, ids.group_model, true),
"add group model",
)?;
Ok(group)
}
fn context(user: UserId, model: ModelId) -> Result<AccessContext, String> {
op(
AccessContext::new(user, model, FilteredAuthorities::empty()),
"empty Access context",
)
}
fn user(value: u8) -> UserId {
UserId::from_tx_id(TxId::from_bytes([value; 12]))
}
fn model(value: u8) -> ModelId {
ModelId::from_bytes([value; 32])
}
fn profile_name(value: &str, label: &str) -> Result<ProfileName, String> {
op(ProfileName::new(value.into()), label)
}
fn op<T>(result: Result<T, String>, label: &str) -> Result<T, String> {
result.map_err(|error| format!("{label}: {error}"))
}
fn equal<T: Debug + PartialEq + ?Sized>(
actual: &T,
expected: &T,
label: &str,
) -> Result<(), String> {
if actual == expected {
Ok(())
} else {
Err(format!(
"{label}: expected {expected:?}, received {actual:?}"
))
}
}
fn require(condition: bool, label: &str) -> Result<(), String> {
condition.then_some(()).ok_or_else(|| label.to_owned())
}
#[cfg(test)]
mod tests {
#[test]
fn release_conformance() {
super::verify().unwrap();
}
}