kcode-k1-rust-code-ktool-service 0.1.0

Coordinate authenticated K1 Rust code Ktool operations
Documentation
use kcode_k1_access_types::{AccessContext, Authority, GroupId, TxId as AccessTxId, UserId};
use kcode_k1_groups::K1Groups;
use kcode_k1_objects::K1Objects;
use kcode_k1_rust_code_document::{RustCodeDocument, RustCodeDocumentError};
use kcode_k1_rust_coding::{
    K1RustCoding, RustCodingConfig, RustCodingError, RustPublishAuthorization, VerifiedCheck,
};
use kcode_k1_rust_package::{LibraryFamily, LibraryId, SourcePackage};
use kcode_k1_rust_projection::{K1RustProjection, PublishOutcome};
use kcode_k1_rust_transaction::{TransactionError, encode};
use kcode_k1_transaction_id::TxId as RustTxId;
use std::collections::HashMap;
use std::error::Error;
use std::fmt::{Debug, Display, Formatter};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, OnceLock};

const OBJECT_FILENAME: &str = "source-package.k1rust";
const OBJECT_MEDIA_TYPE: &str = "application/vnd.kennedy.k1-rust-source-package.v1";

#[derive(Debug)]
pub enum PublicPackageError {
    Absent,
    Unsupported(RustCodeDocumentError),
    Backend(String),
}

impl Display for PublicPackageError {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
        Debug::fmt(self, formatter)
    }
}

impl Error for PublicPackageError {}

#[derive(Debug)]
pub enum RustCodeKtoolServiceError {
    State(String),
    CodingInitialization(String),
    Coding(RustCodingError),
    SourceEncoding(TransactionError),
    Object(String),
    CheckMismatch,
}

impl Display for RustCodeKtoolServiceError {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
        Debug::fmt(self, formatter)
    }
}

impl Error for RustCodeKtoolServiceError {}

pub struct ServiceCheck {
    user: UserId,
    package: SourcePackage,
    verified: VerifiedCheck,
}

#[derive(Default)]
struct UserCoding {
    value: OnceLock<Result<Arc<Mutex<K1RustCoding>>, String>>,
}

pub struct RustCodeKtoolService {
    cache_root: PathBuf,
    config: RustCodingConfig,
    projection: Arc<K1RustProjection>,
    objects: Arc<K1Objects>,
    groups: Arc<K1Groups>,
    users: Mutex<HashMap<UserId, Arc<UserCoding>>>,
}

impl RustCodeKtoolService {
    pub fn new(
        cache_root: impl AsRef<Path>,
        config: RustCodingConfig,
        projection: Arc<K1RustProjection>,
        objects: Arc<K1Objects>,
        groups: Arc<K1Groups>,
    ) -> Self {
        Self {
            cache_root: cache_root.as_ref().to_path_buf(),
            config,
            projection,
            objects,
            groups,
            users: Mutex::new(HashMap::new()),
        }
    }

    pub fn load_document(&self, id: &LibraryId) -> Result<RustCodeDocument, PublicPackageError> {
        match self.projection.load(id) {
            Ok(Some(package)) => RustCodeDocument::from_source_package(package)
                .map_err(PublicPackageError::Unsupported),
            Ok(None) => Err(PublicPackageError::Absent),
            Err(error) => Err(PublicPackageError::Backend(error)),
        }
    }

    pub fn identity_exists(&self, id: &LibraryId) -> Result<bool, PublicPackageError> {
        self.projection
            .load(id)
            .map(|package| package.is_some())
            .map_err(PublicPackageError::Backend)
    }

    pub fn check_fresh(
        &self,
        authenticated: &AccessContext,
        document: &RustCodeDocument,
    ) -> Result<ServiceCheck, RustCodeKtoolServiceError> {
        let package = document.clone().into_source_package();
        let coding = self.coding(authenticated)?;
        let mut coding = coding
            .lock()
            .map_err(|_| RustCodeKtoolServiceError::State("user coding mutex poisoned".into()))?;
        let verified = coding
            .check_fresh(&package)
            .map_err(RustCodeKtoolServiceError::Coding)?;
        Ok(ServiceCheck {
            user: authenticated.user(),
            package,
            verified,
        })
    }

    pub fn current_check(
        &self,
        authenticated: &AccessContext,
        document: &RustCodeDocument,
    ) -> Result<Option<ServiceCheck>, RustCodeKtoolServiceError> {
        let package = document.clone().into_source_package();
        let coding = self.coding(authenticated)?;
        let mut coding = coding
            .lock()
            .map_err(|_| RustCodeKtoolServiceError::State("user coding mutex poisoned".into()))?;
        let verified = coding
            .current_check(&package)
            .map_err(RustCodeKtoolServiceError::Coding)?;
        Ok(verified.map(|verified| ServiceCheck {
            user: authenticated.user(),
            package,
            verified,
        }))
    }

    pub fn publish_checked(
        &self,
        authenticated: &AccessContext,
        document: &RustCodeDocument,
        checked: ServiceCheck,
    ) -> Result<PublishOutcome, RustCodeKtoolServiceError> {
        let package = document.clone().into_source_package();
        if checked.user != authenticated.user() || checked.package != package {
            return Err(RustCodeKtoolServiceError::CheckMismatch);
        }
        let coding = self.coding(authenticated)?;
        let mut coding = coding
            .lock()
            .map_err(|_| RustCodeKtoolServiceError::State("user coding mutex poisoned".into()))?;
        let bytes = encode(&package).map_err(RustCodeKtoolServiceError::SourceEncoding)?;
        self.objects
            .save(OBJECT_FILENAME, OBJECT_MEDIA_TYPE, "", &bytes)
            .map_err(RustCodeKtoolServiceError::Object)?;
        let gate = PublishGate {
            authenticated,
            groups: &self.groups,
        };
        coding
            .publish_checked(&package, &checked.verified, &gate)
            .map_err(RustCodeKtoolServiceError::Coding)
    }

    fn coding(
        &self,
        authenticated: &AccessContext,
    ) -> Result<Arc<Mutex<K1RustCoding>>, RustCodeKtoolServiceError> {
        let slot = {
            let mut users = self.users.lock().map_err(|_| {
                RustCodeKtoolServiceError::State("user coding map mutex poisoned".into())
            })?;
            users
                .entry(authenticated.user())
                .or_insert_with(|| Arc::new(UserCoding::default()))
                .clone()
        };
        match slot.value.get_or_init(|| {
            let user = RustTxId::from_bytes(*authenticated.user().as_tx_id().as_bytes());
            K1RustCoding::open(
                &self.cache_root,
                user,
                self.config.clone(),
                self.projection.clone(),
            )
            .map(|coding| Arc::new(Mutex::new(coding)))
            .map_err(|error| error.to_string())
        }) {
            Ok(coding) => Ok(coding.clone()),
            Err(error) => Err(RustCodeKtoolServiceError::CodingInitialization(
                error.clone(),
            )),
        }
    }
}

struct PublishGate<'a> {
    authenticated: &'a AccessContext,
    groups: &'a K1Groups,
}

impl RustPublishAuthorization for PublishGate<'_> {
    fn authorize_publish(&self, family: &LibraryFamily) -> Result<bool, String> {
        let authority_id = family.authority();
        let authority = authority_id.transaction_id();
        if authority.as_bytes() == self.authenticated.user().as_tx_id().as_bytes() {
            return Ok(true);
        }
        let group = GroupId::new(AccessTxId::from_bytes(*authority.as_bytes()));
        if group.sentinel().is_some()
            || self
                .authenticated
                .filter()
                .contains(Authority::Group(group))
        {
            return Ok(false);
        }
        let memberships = self
            .groups
            .memberships(self.authenticated.user(), self.authenticated.model())?;
        Ok(memberships.shared_groups().contains(&group))
    }
}