use std::{path::Path, sync::Arc};
use kcode_k1_access_driver::K1AccessDriver;
use kcode_k1_groups::K1Groups;
use kcode_k1_peering::K1Peering;
use kcode_k1_txn_ordering::K1TxnOrdering;
pub use kcode_k1_access_types::{
AccessCheck, AccessContext, AccessId, AccessPolicy, AccessRevision, Authority,
FilteredAuthorities, GroupId, ModelId, ProfileId, SubsystemId, Target, TxId, UserId,
ViewerSubject,
};
pub use kcode_k1_groups::{ALL_MODELS, ALL_MODELS_MEMBER, ALL_USERS, LOCAL_MODELS, SentinelGroup};
const ACCESS_UNAVAILABLE: &str = "access is unavailable";
const ACCESS_GROUP_UNAVAILABLE: &str = "access group is unavailable";
const FILTERED_AUTHORITY: &str = "access authority is filtered";
pub struct K1Access {
driver: K1AccessDriver,
groups: Arc<K1Groups>,
}
impl K1Access {
pub fn open(
root: &Path,
ordering: Arc<K1TxnOrdering>,
peering: Arc<K1Peering>,
groups: Arc<K1Groups>,
) -> Result<Self, String> {
Ok(Self {
driver: K1AccessDriver::open(root, ordering, peering)?,
groups,
})
}
pub fn create(
&self,
context: &AccessContext,
target: Target,
profile_id: ProfileId,
policy: AccessPolicy,
) -> Result<AccessRevision, String> {
require_visible_authority(context, &policy)?;
self.driver.create(target, profile_id, policy)
}
pub fn profile_id(&self, access_id: AccessId) -> Result<Option<ProfileId>, String> {
self.driver.profile_id(access_id)
}
pub fn replace_policy(
&self,
context: &AccessContext,
access_id: AccessId,
editors: Vec<Authority>,
viewers: Vec<ViewerSubject>,
) -> Result<AccessRevision, String> {
let memberships = self.groups.memberships(context.user(), context.model())?;
let witness = self
.driver
.edit_witness(context, access_id, memberships.user_groups())?;
let witness = witness.ok_or_else(|| ACCESS_UNAVAILABLE.to_owned())?;
let (editors, viewers) = normalize_replacement(context.user(), editors, viewers)?;
self.driver.replace(
access_id,
context.user(),
memberships.revision(),
witness,
editors,
viewers,
)
}
pub fn check(
&self,
context: &AccessContext,
access_id: AccessId,
expected_subsystem: SubsystemId,
) -> Result<AccessCheck, String> {
if self
.driver
.discovery_missing(context, access_id, expected_subsystem)?
{
self.driver.ensure_discovery(access_id)?;
}
let memberships = self.groups.memberships(context.user(), context.model())?;
self.driver.check(
context,
access_id,
expected_subsystem,
memberships.user_groups(),
memberships.model_groups(),
memberships.revision(),
)
}
pub fn check_many(
&self,
context: &AccessContext,
access_ids: &[AccessId],
expected_subsystem: SubsystemId,
) -> Result<Vec<AccessCheck>, String> {
if access_ids.is_empty() {
return Ok(Vec::new());
}
let memberships = self.groups.memberships(context.user(), context.model())?;
self.driver.check_many(
context,
access_ids,
expected_subsystem,
memberships.user_groups(),
memberships.model_groups(),
memberships.revision(),
)
}
pub fn resolve_visible_targets(
&self,
context: &AccessContext,
targets: &[Target],
expected_subsystem: SubsystemId,
) -> Result<Vec<Option<AccessId>>, String> {
if targets.is_empty() {
return Ok(Vec::new());
}
let memberships = self.groups.memberships(context.user(), context.model())?;
self.driver.resolve_visible_targets(
context,
targets,
expected_subsystem,
memberships.user_groups(),
memberships.model_groups(),
)
}
pub fn list_user(
&self,
context: &AccessContext,
expected_subsystem: SubsystemId,
) -> Result<Vec<AccessId>, String> {
self.driver.discovered_for_user(context, expected_subsystem)
}
pub fn list_group(
&self,
context: &AccessContext,
group: GroupId,
expected_subsystem: SubsystemId,
) -> Result<Vec<AccessId>, String> {
let groups = self.groups.groups_for_user(context.user())?;
require_group(&groups, group)?;
self.driver
.discovered_for_group(context, group, expected_subsystem)
}
pub fn list_user_group_targets(
&self,
context: &AccessContext,
expected_subsystem: SubsystemId,
) -> Result<Vec<(AccessId, Target)>, String> {
let memberships = self.groups.memberships(context.user(), context.model())?;
self.driver.discovered_targets_for_user_and_groups(
context,
memberships.user_groups(),
expected_subsystem,
)
}
}
fn require_visible_authority(context: &AccessContext, policy: &AccessPolicy) -> Result<(), String> {
if context.filter().contains(policy.authority()) {
Err(FILTERED_AUTHORITY.to_owned())
} else {
Ok(())
}
}
fn normalize_replacement(
actor: UserId,
mut editors: Vec<Authority>,
mut viewers: Vec<ViewerSubject>,
) -> Result<(Vec<Authority>, Vec<ViewerSubject>), String> {
AccessPolicy::new(Authority::User(actor), editors.clone(), viewers.clone())?;
editors.sort_unstable();
editors.dedup();
viewers.sort_unstable();
viewers.dedup();
viewers.retain(|viewer| match viewer {
ViewerSubject::User(user) => editors.binary_search(&Authority::User(*user)).is_err(),
ViewerSubject::Group(group) => editors.binary_search(&Authority::Group(*group)).is_err(),
ViewerSubject::Model(_) => true,
});
Ok((editors, viewers))
}
fn require_group(groups: &[GroupId], group: GroupId) -> Result<(), String> {
if groups.contains(&group) {
Ok(())
} else {
Err(ACCESS_GROUP_UNAVAILABLE.to_owned())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn txid(byte: u8) -> TxId {
TxId::from_bytes([byte; 12])
}
fn user(byte: u8) -> UserId {
UserId::from_tx_id(txid(byte))
}
fn group(byte: u8) -> GroupId {
GroupId::new(txid(byte))
}
fn model(byte: u8) -> ModelId {
ModelId::from_bytes([byte; 32])
}
#[test]
fn filtered_create_authority_is_rejected() {
let authority = Authority::Group(group(7));
let context = AccessContext::new(
user(1),
model(2),
FilteredAuthorities::new(vec![authority]).unwrap(),
)
.unwrap();
let policy = AccessPolicy::new(authority, Vec::new(), Vec::new()).unwrap();
assert_eq!(
require_visible_authority(&context, &policy),
Err(FILTERED_AUTHORITY.to_owned())
);
}
#[test]
fn replacement_lists_are_canonical_and_editor_viewers_are_removed() {
let editor_user = user(3);
let editor_group = group(4);
let visible_model = model(5);
let (editors, viewers) = normalize_replacement(
user(1),
vec![
Authority::Group(editor_group),
Authority::User(editor_user),
Authority::Group(editor_group),
],
vec![
ViewerSubject::Model(visible_model),
ViewerSubject::User(editor_user),
ViewerSubject::Group(editor_group),
ViewerSubject::Model(visible_model),
],
)
.unwrap();
assert_eq!(
editors,
vec![Authority::User(editor_user), Authority::Group(editor_group)]
);
assert_eq!(viewers, vec![ViewerSubject::Model(visible_model)]);
}
#[test]
fn replacement_rejects_sentinel_editors_and_all_models_member() {
assert!(
normalize_replacement(user(1), vec![Authority::Group(ALL_USERS)], Vec::new()).is_err()
);
assert!(
normalize_replacement(
user(1),
Vec::new(),
vec![ViewerSubject::Model(ALL_MODELS_MEMBER)]
)
.is_err()
);
}
#[test]
fn requested_group_requires_current_membership() {
let requested = group(1);
assert_eq!(require_group(&[requested], requested), Ok(()));
assert_eq!(
require_group(&[group(2), group(3)], requested),
Err(ACCESS_GROUP_UNAVAILABLE.to_owned())
);
}
}