use std::sync::Arc;
use kcode_k1_access::{
AccessContext, AccessId, AccessPolicy, Authority, FilteredAuthorities, K1Access, ModelId,
SubsystemId, Target, TxId, UserId, ViewerSubject,
};
use kcode_k1_groups::{ALL_MODELS, ALL_USERS, GroupId, GroupName, GroupRole, K1Groups};
use kcode_k1_peering::K1Peering;
use kcode_k1_txn_ordering::K1TxnOrdering;
struct Stack {
_ordering: Arc<K1TxnOrdering>,
_peering: Arc<K1Peering>,
groups: Arc<K1Groups>,
access: Arc<K1Access>,
}
impl Stack {
fn open(root: &std::path::Path) -> Result<Self, String> {
let ordering = Arc::new(at(
"open ordering",
K1TxnOrdering::open(&root.join("ordering")),
)?);
let peering = Arc::new(at(
"open peering",
K1Peering::open(&root.join("peering"), ordering.clone()),
)?);
let groups = Arc::new(at(
"open groups",
K1Groups::open(&root.join("groups"), ordering.clone(), peering.clone()),
)?);
let access = Arc::new(at(
"open access",
K1Access::open(
&root.join("access"),
ordering.clone(),
peering.clone(),
groups.clone(),
),
)?);
Ok(Self {
_ordering: ordering,
_peering: peering,
groups,
access,
})
}
fn group(&self, owner: UserId, label: &str) -> Result<GroupId, String> {
let name = GroupName::new(label.to_owned())?;
Ok(at(label, self.groups.create(owner, name))?.group_id())
}
fn role(
&self,
owner: UserId,
group: GroupId,
member: UserId,
value: Option<GroupRole>,
) -> Result<(), String> {
at(
"set user role",
self.groups.set_user_role(owner, group, member, value),
)?;
Ok(())
}
fn model_member(
&self,
owner: UserId,
group: GroupId,
model: ModelId,
present: bool,
) -> Result<(), String> {
at(
"set model membership",
self.groups
.set_model_membership(owner, group, model, present),
)?;
Ok(())
}
}
fn at<T>(label: &str, result: Result<T, String>) -> Result<T, String> {
result.map_err(|error| format!("{label}: {error}"))
}
fn require(condition: bool, label: &str) -> Result<(), String> {
condition.then_some(()).ok_or_else(|| label.to_owned())
}
fn require_error<T>(result: Result<T, String>, expected: &str, label: &str) -> Result<(), String> {
match result {
Err(error) if error == expected => Ok(()),
_ => Err(label.to_owned()),
}
}
fn user(byte: u8) -> UserId {
UserId::from_tx_id(TxId::from_bytes([byte; 12]))
}
fn model(byte: u8) -> ModelId {
ModelId::from_bytes([byte; 32])
}
fn context(user: UserId, model: ModelId) -> Result<AccessContext, String> {
AccessContext::new(user, model, FilteredAuthorities::empty())
}
fn exact(user: UserId, model: ModelId, group: GroupId) -> Result<AccessContext, String> {
AccessContext::new(
user,
model,
FilteredAuthorities::new(vec![Authority::Group(group)])?,
)
}
fn editors(user: UserId, group: GroupId) -> Vec<Authority> {
vec![Authority::User(user), Authority::Group(group)]
}
fn viewers(
user: UserId,
outsider: UserId,
user_group: GroupId,
model_group: GroupId,
model: ModelId,
) -> Vec<ViewerSubject> {
vec![
ViewerSubject::User(user),
ViewerSubject::User(outsider),
ViewerSubject::Group(user_group),
ViewerSubject::Group(model_group),
ViewerSubject::Model(model),
]
}
struct Scenario<'a> {
stack: &'a Stack,
access: AccessId,
subsystem: SubsystemId,
}
impl Scenario<'_> {
fn rights(
&self,
user: UserId,
model: ModelId,
view: bool,
edit: bool,
label: &str,
) -> Result<(), String> {
let result = at(
label,
self.stack
.access
.check(&context(user, model)?, self.access, self.subsystem),
)?;
require(
result.can_view() == view
&& result.can_edit() == edit
&& result.target().is_some() == view,
label,
)
}
fn group_list(
&self,
user: UserId,
model: ModelId,
group: GroupId,
expected: &[AccessId],
label: &str,
) -> Result<(), String> {
let actual = at(
label,
self.stack
.access
.list_group(&context(user, model)?, group, self.subsystem),
)?;
require(actual == expected, label)
}
fn user_list(
&self,
user: UserId,
model: ModelId,
expected: &[AccessId],
label: &str,
) -> Result<(), String> {
let actual = at(
label,
self.stack
.access
.list_user(&context(user, model)?, self.subsystem),
)?;
require(actual == expected, label)
}
fn concealed(&self, context: &AccessContext, label: &str) -> Result<(), String> {
let result = at(
label,
self.stack
.access
.check(context, self.access, self.subsystem),
)?;
require(
!result.can_view()
&& !result.can_edit()
&& result.target().is_none()
&& result.access_revision().is_none()
&& result.groups_revision().is_none(),
label,
)
}
}
pub fn verify() -> Result<(), String> {
let root = tempfile::tempdir().map_err(|error| format!("create root: {error}"))?;
let stack = Stack::open(root.path())?;
let owner = user(1);
let member = user(2);
let admin = user(3);
let group_editor = user(4);
let viewer = user(5);
let outsider = user(6);
let late = user(7);
let model_only = user(8);
let direct_editor = user(9);
let authority_model = model(11);
let editor_model = model(12);
let viewer_model = model(13);
let direct_model = model(14);
let no_view_model = model(15);
let subsystem = SubsystemId::from_str("audio-fragment")?;
let authority_group = stack.group(owner, "authority group")?;
let editor_group = stack.group(owner, "editor group")?;
let user_viewer_group = stack.group(owner, "user viewer group")?;
let model_viewer_group = stack.group(owner, "model viewer group")?;
let replacement_group = stack.group(owner, "replacement viewer group")?;
for (group, member, value) in [
(authority_group, member, GroupRole::User),
(authority_group, admin, GroupRole::Admin),
(editor_group, group_editor, GroupRole::User),
(user_viewer_group, viewer, GroupRole::User),
(replacement_group, late, GroupRole::User),
] {
stack.role(owner, group, member, Some(value))?;
}
for (group, member) in [
(authority_group, authority_model),
(editor_group, editor_model),
(model_viewer_group, viewer_model),
] {
stack.model_member(owner, group, member, true)?;
}
let group_access = at(
"create group access",
stack.access.create(
&context(owner, authority_model)?,
Target::new(subsystem, vec![0, 255, 7, 0]),
AccessPolicy::new(
Authority::Group(authority_group),
editors(direct_editor, editor_group),
viewers(
member,
outsider,
user_viewer_group,
model_viewer_group,
direct_model,
),
)?,
),
)?
.access_id();
let personal_access = at(
"create personal access",
stack.access.create(
&context(member, direct_model)?,
Target::new(subsystem, vec![2, 0, 2, 0]),
AccessPolicy::new(
Authority::User(member),
Vec::new(),
vec![
ViewerSubject::Group(ALL_USERS),
ViewerSubject::Group(ALL_MODELS),
],
)?,
),
)?
.access_id();
let scenario = Scenario {
stack: &stack,
access: group_access,
subsystem,
};
for (user, model, view, edit, label) in [
(owner, no_view_model, false, true, "authority owner edit"),
(member, no_view_model, false, true, "authority user edit"),
(admin, no_view_model, false, true, "authority admin edit"),
(
outsider,
authority_model,
true,
false,
"authority model view",
),
(
group_editor,
no_view_model,
false,
true,
"editor human edit",
),
(outsider, editor_model, true, false, "editor model view"),
(
group_editor,
editor_model,
true,
true,
"editor combined rights",
),
(viewer, viewer_model, true, false, "split viewer groups"),
(viewer, direct_model, true, false, "direct model viewer"),
(viewer, no_view_model, false, false, "user-only viewer"),
(model_only, direct_model, false, false, "model-only viewer"),
] {
scenario.rights(user, model, view, edit, label)?;
}
for (user, group, label) in [
(owner, authority_group, "authority discovery"),
(group_editor, editor_group, "group editor discovery"),
(viewer, user_viewer_group, "user viewer group discovery"),
(owner, model_viewer_group, "model viewer group discovery"),
] {
scenario.group_list(user, no_view_model, group, &[group_access], label)?;
}
for (user, expected, label) in [
(
direct_editor,
&[group_access][..],
"direct editor discovery",
),
(
outsider,
&[group_access][..],
"direct user viewer discovery",
),
(
member,
&[group_access, personal_access][..],
"member discovery",
),
(model_only, &[][..], "models excluded from discovery"),
] {
scenario.user_list(user, direct_model, expected, label)?;
}
scenario.group_list(
model_only,
direct_model,
ALL_USERS,
&[personal_access],
"all users discovery",
)?;
let filtered = exact(member, direct_model, authority_group)?;
scenario.concealed(&filtered, "filtered check was not concealed")?;
let personal = at(
"filtered personal authority",
stack.access.check(&filtered, personal_access, subsystem),
)?;
require(
personal.can_view() && personal.can_edit() && personal.target().is_some(),
"filter hid personal authority",
)?;
let listed = at(
"filtered group listing",
stack
.access
.list_group(&filtered, authority_group, subsystem),
)?;
require(listed.is_empty(), "filtered group listing was visible")?;
let listed = at(
"filtered user listing",
stack.access.list_user(&filtered, subsystem),
)?;
require(
listed == [personal_access],
"filtered user listing was incorrect",
)?;
stack.role(owner, user_viewer_group, late, Some(GroupRole::User))?;
scenario.group_list(
late,
no_view_model,
user_viewer_group,
&[group_access],
"late member discovery",
)?;
stack.role(owner, user_viewer_group, late, None)?;
require_error(
stack
.access
.list_group(&context(late, no_view_model)?, user_viewer_group, subsystem),
"access group is unavailable",
"former member group listing",
)?;
for (group, user, model, value, label) in [
(
authority_group,
admin,
authority_model,
GroupRole::Admin,
"authority",
),
(
editor_group,
group_editor,
editor_model,
GroupRole::User,
"editor",
),
(
user_viewer_group,
viewer,
viewer_model,
GroupRole::User,
"viewer",
),
] {
stack.role(owner, group, user, None)?;
scenario.rights(user, model, false, false, label)?;
stack.role(owner, group, user, Some(value))?;
}
for (group, user, model, view, edit, label) in [
(
authority_group,
owner,
authority_model,
false,
true,
"authority model",
),
(
editor_group,
group_editor,
editor_model,
false,
true,
"editor model",
),
(
model_viewer_group,
viewer,
viewer_model,
false,
false,
"viewer model",
),
] {
stack.model_member(owner, group, model, false)?;
scenario.rights(user, model, view, edit, label)?;
stack.model_member(owner, group, model, true)?;
}
scenario.rights(
direct_editor,
no_view_model,
false,
true,
"editor without model view",
)?;
require_error(
stack.access.replace_policy(
&exact(direct_editor, no_view_model, authority_group)?,
group_access,
editors(direct_editor, editor_group),
viewers(
member,
outsider,
replacement_group,
model_viewer_group,
direct_model,
),
),
"access is unavailable",
"filtered replacement",
)?;
at(
"replace policy",
stack.access.replace_policy(
&context(direct_editor, no_view_model)?,
group_access,
editors(direct_editor, editor_group),
viewers(
member,
outsider,
replacement_group,
model_viewer_group,
direct_model,
),
),
)?;
scenario.rights(
owner,
authority_model,
true,
true,
"authority after replacement",
)?;
scenario.concealed(&filtered, "replacement changed filtered authority")?;
scenario.group_list(
late,
no_view_model,
replacement_group,
&[group_access],
"replacement discovery",
)?;
scenario.group_list(
viewer,
no_view_model,
user_viewer_group,
&[group_access],
"monotonic discovery",
)
}
#[cfg(test)]
mod tests {
#[test]
fn groups_backed_access_conformance() {
super::verify().unwrap();
}
}