use std::collections::{BTreeSet, HashSet};
use std::path::Path;
use access_types_v2::{AccessId as DiscoveryAccessId, TxId as DiscoveryTxId};
use kcode_k1_access_discovery_store::DiscoveryStore;
use kcode_k1_access_store::StoredAccess;
use kcode_k1_access_types::{
AccessContext, AccessId, AccessPolicy, Authority, GroupId, SubsystemId, Target, TxId, UserId,
ViewerSubject,
};
pub struct AccessDiscovery {
store: DiscoveryStore,
}
impl AccessDiscovery {
pub fn open(root: &Path) -> Result<Self, String> {
Ok(Self {
store: DiscoveryStore::open(root)?,
})
}
pub fn checkpoint(&self) -> Result<Option<TxId>, String> {
self.store.checkpoint()
}
pub fn complete(&self, records: &[StoredAccess]) -> Result<bool, String> {
for record in records {
let (users, groups) = subjects(record.policy());
if self
.store
.contains_missing(&users, &groups, discovery_id(record.access_id()))?
{
return Ok(false);
}
}
Ok(true)
}
pub fn commit(
&self,
callback_txid: TxId,
discovery: Option<(AccessId, &AccessPolicy)>,
) -> Result<(), String> {
match discovery {
Some((access_id, policy)) => {
let (users, groups) = subjects(policy);
self.store
.commit(callback_txid, &users, &groups, discovery_id(access_id))
.map(|_| ())
}
None => self
.store
.commit(
callback_txid,
&[],
&[],
DiscoveryAccessId::new(DiscoveryTxId::from_bytes(*callback_txid.as_bytes())),
)
.map(|_| ()),
}
}
pub fn missing(
&self,
context: &AccessContext,
records: &[StoredAccess],
access_id: AccessId,
expected_subsystem: SubsystemId,
) -> Result<bool, String> {
let Some(record) = visible_record(context, records, access_id, expected_subsystem) else {
return Ok(false);
};
let (users, groups) = subjects(record.policy());
self.store
.contains_missing(&users, &groups, discovery_id(access_id))
}
pub fn list_user(
&self,
context: &AccessContext,
records: &[StoredAccess],
subsystem: SubsystemId,
) -> Result<Vec<AccessId>, String> {
Ok(self
.store
.list_user(context.user())?
.into_iter()
.map(current_id)
.filter(|access_id| visible_record(context, records, *access_id, subsystem).is_some())
.collect())
}
pub fn list_group(
&self,
context: &AccessContext,
records: &[StoredAccess],
group: GroupId,
subsystem: SubsystemId,
) -> Result<Vec<AccessId>, String> {
Ok(self
.store
.list_group(group)?
.into_iter()
.map(current_id)
.filter(|access_id| visible_record(context, records, *access_id, subsystem).is_some())
.collect())
}
pub fn list_user_groups(
&self,
context: &AccessContext,
records: &[StoredAccess],
groups: &[GroupId],
subsystem: SubsystemId,
) -> Result<Vec<(AccessId, Target)>, String> {
let mut discovered = self.store.list_user(context.user())?;
for group in groups.iter().copied().collect::<BTreeSet<_>>() {
discovered.extend(self.store.list_group(group)?);
}
let mut seen = HashSet::new();
let mut visible = Vec::new();
for access_id in discovered.into_iter().map(current_id) {
let Some(record) = visible_record(context, records, access_id, subsystem) else {
continue;
};
if seen.insert(access_id) {
visible.push((access_id, record.target().clone()));
}
}
Ok(visible)
}
pub fn discard(&self) -> Result<(), String> {
self.store.discard()
}
}
fn subjects(policy: &AccessPolicy) -> (Vec<UserId>, Vec<GroupId>) {
let mut users = Vec::with_capacity(1 + policy.editors().len() + policy.viewers().len());
let mut groups = Vec::with_capacity(1 + policy.editors().len() + policy.viewers().len());
push_authority(&mut users, &mut groups, policy.authority());
for authority in policy.editors() {
push_authority(&mut users, &mut groups, *authority);
}
for viewer in policy.viewers() {
match viewer {
ViewerSubject::User(user) => users.push(*user),
ViewerSubject::Group(group) => groups.push(*group),
ViewerSubject::Model(_) => {}
}
}
(users, groups)
}
fn push_authority(users: &mut Vec<UserId>, groups: &mut Vec<GroupId>, authority: Authority) {
match authority {
Authority::User(user) => users.push(user),
Authority::Group(group) => groups.push(group),
}
}
fn discovery_id(access_id: AccessId) -> DiscoveryAccessId {
DiscoveryAccessId::new(DiscoveryTxId::from_bytes(*access_id.txid().as_bytes()))
}
fn current_id(access_id: DiscoveryAccessId) -> AccessId {
AccessId::new(TxId::from_bytes(*access_id.txid().as_bytes()))
}
fn visible_record<'a>(
context: &AccessContext,
records: &'a [StoredAccess],
access_id: AccessId,
subsystem: SubsystemId,
) -> Option<&'a StoredAccess> {
records
.iter()
.find(|record| record.access_id() == access_id)
.filter(|record| record.target().subsystem() == subsystem)
.filter(|record| !context.filter().contains(record.policy().authority()))
}