kcode-k1-access-persons 0.1.0

Access-controlled facade for standalone K1 persons
Documentation
use std::sync::Arc;

use kcode_k1_access::{
    AccessCheck, Authorizations, K1Access, SubsystemId, Target, TxId, ViewerSubject,
};
use kcode_k1_access_profiles::K1AccessProfiles;
use kcode_k1_groups::{ALL_MODELS, ALL_USERS};
use kcode_k1_persons::K1Persons;

pub use kcode_k1_access::{AccessId, RequestPrincipal};
pub use kcode_k1_access_profiles::ProfileSelection;
pub use kcode_k1_persons::{PersonId, PersonView};

const PERSON_SUBSYSTEM: &str = "k1-person";
const NOT_VISIBLE: &str = "principal cannot access person";
const NOT_UPDATER: &str = "principal must be able to view and manage person";
const TARGET_UNAVAILABLE: &str = "person access target is unavailable";
const INVALID_TARGET: &str = "person access target must contain exactly 12 person bytes";
const PERSON_UNAVAILABLE: &str = "person is unavailable";
const NONCANONICAL_TARGET: &str = "person access target is no longer canonical";
const MISMATCHED_TARGET: &str = "person access target does not match supplied person";

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SubmittedPerson {
    pub access_id: AccessId,
    pub person_id: PersonId,
}

pub struct K1AccessPersons {
    access: Arc<K1Access>,
    profiles: Arc<K1AccessProfiles>,
    persons: Arc<K1Persons>,
    subsystem: SubsystemId,
}

impl K1AccessPersons {
    pub fn open(
        access: Arc<K1Access>,
        profiles: Arc<K1AccessProfiles>,
        persons: Arc<K1Persons>,
    ) -> Result<Self, String> {
        Ok(Self {
            access,
            profiles,
            persons,
            subsystem: SubsystemId::from_str(PERSON_SUBSYSTEM)?,
        })
    }

    pub fn create(
        &self,
        principal: RequestPrincipal,
        profile: ProfileSelection,
        name: String,
    ) -> Result<SubmittedPerson, String> {
        let resolved = self
            .profiles
            .resolve(principal, profile)?
            .into_authorizations();
        let mut viewers = resolved.viewers().to_vec();
        viewers.extend([
            ViewerSubject::Group(ALL_USERS),
            ViewerSubject::Group(ALL_MODELS),
        ]);
        let authorizations = Authorizations::new(resolved.owners().to_vec(), viewers)?;
        let person_id = self.persons.create(name)?;
        let target = Target::new(self.subsystem, person_id.as_tx_id().as_bytes().to_vec());
        let access_id = self.access.create(target, authorizations)?.access_id();
        Ok(SubmittedPerson {
            access_id,
            person_id,
        })
    }

    pub fn read(
        &self,
        principal: RequestPrincipal,
        access_id: AccessId,
    ) -> Result<PersonView, String> {
        self.read_person(visible_person_id(self.access.check(
            principal,
            access_id,
            self.subsystem,
        )?)?)
    }

    pub fn read_person(&self, person_id: PersonId) -> Result<PersonView, String> {
        self.persons
            .read(person_id)?
            .ok_or_else(|| PERSON_UNAVAILABLE.to_owned())
    }

    pub fn update(
        &self,
        principal: RequestPrincipal,
        access_id: AccessId,
        name: String,
    ) -> Result<(), String> {
        self.update_checked(principal, access_id, None, name)
    }

    pub fn update_person(
        &self,
        principal: RequestPrincipal,
        person_id: PersonId,
        access_id: AccessId,
        name: String,
    ) -> Result<(), String> {
        self.update_checked(principal, access_id, Some(person_id), name)
    }

    fn update_checked(
        &self,
        principal: RequestPrincipal,
        access_id: AccessId,
        expected_person_id: Option<PersonId>,
        name: String,
    ) -> Result<(), String> {
        let check = self.access.check(principal, access_id, self.subsystem)?;
        if !check.can_view() || !check.can_manage() {
            return Err(NOT_UPDATER.to_owned());
        }
        let person_id = target_person_id(&check)?;
        if expected_person_id.is_some_and(|expected| expected != person_id) {
            return Err(MISMATCHED_TARGET.to_owned());
        }
        self.update_canonical_person(person_id, name)
    }

    fn update_canonical_person(&self, person_id: PersonId, name: String) -> Result<(), String> {
        let current = self.read_person(person_id)?;
        if current.person_id != person_id {
            return Err(NONCANONICAL_TARGET.to_owned());
        }
        self.persons.update(person_id, name)
    }
}

fn visible_person_id(check: AccessCheck) -> Result<PersonId, String> {
    if !check.can_view() {
        return Err(NOT_VISIBLE.to_owned());
    }
    target_person_id(&check)
}

fn target_person_id(check: &AccessCheck) -> Result<PersonId, String> {
    let target = check
        .target()
        .ok_or_else(|| TARGET_UNAVAILABLE.to_owned())?;
    let bytes: [u8; 12] = target
        .object_id()
        .try_into()
        .map_err(|_| INVALID_TARGET.to_owned())?;
    Ok(PersonId::from_tx_id(TxId::from_bytes(bytes)))
}