kcode-k1-access-profile-store 0.4.0

Policy owner for the K1 saved access profile SQLite projection
Documentation
use std::{
    path::Path,
    sync::{
        Arc,
        atomic::{AtomicBool, Ordering},
    },
    time::{Duration, Instant},
};

use kcode_k1_access_profile_sqlite::{OpenError, ProfileDatabase, Snapshot};
use kcode_k1_transaction::Transaction;
use kcode_k1_txn_ordering::K1TxnOrdering;

pub use kcode_k1_access_profile_sqlite::{
    ApplyOutcome, ProfileAction, ProfileColor, ProfileName, SavedProfile,
};
pub use kcode_k1_access_profile_types::{
    AuthorizationProfile, ProfileId, ProfileRevision, TxId, UserId,
};

const SUBSYSTEM: &str = "k1-profile-subsystem";

pub struct ProfileStore {
    database: ProfileDatabase,
    ordering: Arc<K1TxnOrdering>,
    available: AtomicBool,
}

impl ProfileStore {
    pub fn open(root: &Path, ordering: Arc<K1TxnOrdering>) -> Result<(Self, Option<TxId>), String> {
        let started = Instant::now();
        let result = Self::open_inner(root, ordering);
        if started.elapsed() > Duration::from_millis(100) {
            let outcome = if result.is_ok() { "ready" } else { "error" };
            eprintln!(
                "level=warn module=kcode-k1-access-profile-store operation=open elapsed_us={} outcome={outcome}",
                started.elapsed().as_micros()
            );
        }
        result
    }

    fn open_inner(
        root: &Path,
        ordering: Arc<K1TxnOrdering>,
    ) -> Result<(Self, Option<TxId>), String> {
        let (database, snapshot) = match ProfileDatabase::open(root) {
            Ok(value) => value,
            Err(OpenError::Rebuildable) => ProfileDatabase::rebuild(root)?,
            Err(OpenError::Fatal(error)) => return Err(error),
        };
        let (database, snapshot) = match validate_snapshot(&ordering, &snapshot) {
            Ok(()) => (database, snapshot),
            Err(ValidationIssue::Invalid) => {
                drop(database);
                ProfileDatabase::rebuild(root)?
            }
            Err(ValidationIssue::Fatal(error)) => return Err(error),
        };
        let cursor = snapshot.cursor();
        Ok((
            Self {
                database,
                ordering,
                available: AtomicBool::new(true),
            },
            cursor,
        ))
    }

    pub fn apply(
        &self,
        callback_txid: TxId,
        action: ProfileAction,
    ) -> Result<ApplyOutcome, String> {
        self.require_available()?;
        if let Err(issue) = validate_transaction(&self.ordering, callback_txid) {
            return match issue {
                ValidationIssue::Invalid => self.disable("profile callback contradicts KTO"),
                ValidationIssue::Fatal(error) => self.disable(error),
            };
        }
        self.require_available()?;
        match self.database.apply(callback_txid, action) {
            Ok(outcome) => Ok(outcome),
            Err(error) => self.disable(error),
        }
    }

    pub fn get_for_user(
        &self,
        user: UserId,
        profile_id: ProfileId,
    ) -> Result<Option<SavedProfile>, String> {
        self.require_available()?;
        match self.database.get_for_user(user, profile_id) {
            Ok(profile) => Ok(profile),
            Err(error) => self.disable(error),
        }
    }

    pub fn list_for_user(&self, user: UserId) -> Result<Vec<SavedProfile>, String> {
        self.require_available()?;
        match self.database.list_for_user(user) {
            Ok(profiles) => Ok(profiles),
            Err(error) => self.disable(error),
        }
    }

    pub fn clear(&self) -> Result<(), String> {
        self.require_available()?;
        match self.database.clear() {
            Ok(()) => Ok(()),
            Err(error) => self.disable(error),
        }
    }

    fn require_available(&self) -> Result<(), String> {
        if self.available.load(Ordering::Acquire) {
            Ok(())
        } else {
            Err("profile store is unavailable".to_owned())
        }
    }

    fn disable<T>(&self, error: impl Into<String>) -> Result<T, String> {
        self.available.store(false, Ordering::Release);
        Err(error.into())
    }
}

enum ValidationIssue {
    Invalid,
    Fatal(String),
}

fn validate_snapshot(ordering: &K1TxnOrdering, snapshot: &Snapshot) -> Result<(), ValidationIssue> {
    if let Some(cursor) = snapshot.cursor() {
        validate_transaction(ordering, cursor)?;
    }
    for profile in snapshot.profiles() {
        validate_transaction(ordering, profile.profile_id().txid())?;
        validate_transaction(ordering, profile.revision().txid())?;
    }
    Ok(())
}

fn validate_transaction(ordering: &K1TxnOrdering, id: TxId) -> Result<(), ValidationIssue> {
    let bytes = ordering
        .get_txn(id)
        .map_err(|error| ValidationIssue::Fatal(format!("KTO query failed: {error}")))?
        .ok_or(ValidationIssue::Invalid)?;
    if !id.verify(&bytes) {
        return Err(ValidationIssue::Invalid);
    }
    let transaction = Transaction::parse(&bytes).map_err(|_| ValidationIssue::Invalid)?;
    if transaction.subsystem().as_str() != SUBSYSTEM {
        return Err(ValidationIssue::Invalid);
    }
    Ok(())
}