kcode-k1-access 0.3.0

K1 access Groups-aware authorization facade
Documentation
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, AccessId, AccessRevision, Authorizations, GroupId, ModelId, OwnerSubject,
    RequestPrincipal, Target, TxId, UserId, ViewerSubject,
};
pub use kcode_k1_transaction::SubsystemId;

const ACCESS_DENIED: &str = "access denied";

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,
        target: Target,
        authorizations: Authorizations,
    ) -> Result<AccessRevision, String> {
        self.driver.create(target, authorizations)
    }

    pub fn set_authorizations(
        &self,
        principal: RequestPrincipal,
        access_id: AccessId,
        authorizations: Authorizations,
    ) -> Result<AccessRevision, String> {
        let memberships = self
            .groups
            .memberships(principal.user(), principal.model())?;
        let witness =
            self.driver
                .owner_witness(access_id, principal.user(), memberships.user_groups())?;
        let witness = witness.ok_or_else(|| "principal is not an access owner".to_owned())?;
        self.driver.replace(
            access_id,
            principal.user(),
            memberships.revision(),
            witness,
            authorizations,
        )
    }

    pub fn check(
        &self,
        principal: RequestPrincipal,
        access_id: AccessId,
        expected_subsystem: SubsystemId,
    ) -> Result<AccessCheck, String> {
        if self
            .driver
            .discovery_missing(access_id, expected_subsystem)?
        {
            self.driver.ensure_discovery(access_id)?;
        }
        let memberships = self
            .groups
            .memberships(principal.user(), principal.model())?;
        self.driver.check(
            principal,
            access_id,
            expected_subsystem,
            memberships.user_groups(),
            memberships.model_groups(),
            memberships.revision(),
        )
    }

    pub fn list_user(
        &self,
        principal: RequestPrincipal,
        expected_subsystem: SubsystemId,
    ) -> Result<Vec<AccessId>, String> {
        self.driver
            .discovered_for_user(principal.user(), expected_subsystem)
    }

    pub fn list_group(
        &self,
        principal: RequestPrincipal,
        group: GroupId,
        expected_subsystem: SubsystemId,
    ) -> Result<Vec<AccessId>, String> {
        let groups = self.groups.groups_for_user(principal.user())?;
        require_group(&groups, group)?;
        self.driver.discovered_for_group(group, expected_subsystem)
    }
}

fn require_group(groups: &[GroupId], group: GroupId) -> Result<(), String> {
    if groups.contains(&group) {
        Ok(())
    } else {
        Err(ACCESS_DENIED.to_owned())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn group(byte: u8) -> GroupId {
        GroupId::new(TxId::from_bytes([byte; 12]))
    }

    #[test]
    fn requested_group_is_required() {
        let requested = group(1);
        assert_eq!(require_group(&[requested], requested), Ok(()));
        assert_eq!(
            require_group(&[group(2), group(3)], requested),
            Err(ACCESS_DENIED.to_owned())
        );
    }
}