kcode-k1-access-discovery 0.2.0

Concrete K1 access discovery owner for the access projection
Documentation
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()))
}