mod auth;
mod critical;
pub mod data_dir;
mod error;
pub mod journal;
mod policy;
mod records;
pub mod route;
mod tokens;
mod vaults;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::backend::{CommitHook, JournalRecord, Store, StoreError};
use crate::proto::api::{Capabilities, ErrorCode};
use serde::Serialize;
pub use auth::{Caller, Principal};
pub use error::CoreError;
pub use journal::{FileJournal, Journal, ReplayError, replay_journal};
pub use policy::{Admission, Admitted, AllowAll, Policy};
pub use route::{AuthScheme, ROUTES, Route, RouteId, Served};
pub trait Clock: Send + Sync {
fn now(&self) -> i64;
}
pub struct SystemClock;
impl Clock for SystemClock {
fn now(&self) -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_secs() as i64)
}
}
pub trait Log: Send + Sync {
fn error(&self, message: &str);
fn warn(&self, message: &str);
}
pub struct NoLog;
impl Log for NoLog {
fn error(&self, _message: &str) {}
fn warn(&self, _message: &str) {}
}
#[derive(Clone, Copy)]
pub struct CanonicalRequest<'a> {
pub method: &'a str,
pub path_and_query: &'a str,
pub authorization: Option<&'a str>,
pub if_match: Option<&'a str>,
pub if_none_match: Option<&'a str>,
pub body: &'a [u8],
}
impl std::fmt::Debug for CanonicalRequest<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CanonicalRequest")
.field("method", &self.method)
.field("path_and_query", &self.path_and_query)
.field("authorized", &self.authorization.is_some())
.field("if_match", &self.if_match)
.field("if_none_match", &self.if_none_match)
.field("body_len", &self.body.len())
.finish()
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct CoreResponse {
pub status: u16,
pub error: Option<ErrorCode>,
pub etag: Option<u64>,
pub expires_at: Option<i64>,
pub retry_after: Option<u64>,
pub body: Vec<u8>,
}
impl std::fmt::Debug for CoreResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CoreResponse")
.field("status", &self.status)
.field("error", &self.error)
.field("etag", &self.etag)
.field("expires_at", &self.expires_at)
.field("retry_after", &self.retry_after)
.field("body_len", &self.body.len())
.finish()
}
}
impl CoreResponse {
pub(crate) fn json<T: Serialize>(status: u16, value: &T) -> Result<CoreResponse, CoreError> {
let body = serde_json::to_vec(value)
.map_err(|e| CoreError::internal().logged(format!("encoding a response: {e}")))?;
Ok(CoreResponse {
status,
error: None,
etag: None,
expires_at: None,
retry_after: None,
body,
})
}
pub(crate) fn with_etag(mut self, version: u64) -> CoreResponse {
self.etag = Some(version);
self
}
}
pub struct Core {
store: Arc<dyn Store>,
journal: Arc<dyn Journal>,
policy: Policy,
clock: Arc<dyn Clock>,
log: Arc<dyn Log>,
replayed: usize,
}
impl std::fmt::Debug for Core {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Core")
.field("journal", &self.journal.describe())
.field("policy", &self.policy)
.field("replayed", &self.replayed)
.finish_non_exhaustive()
}
}
impl Core {
pub fn open(
store: Arc<dyn Store>,
journal: Arc<dyn Journal>,
policy: Policy,
clock: Arc<dyn Clock>,
) -> Result<Core, ReplayError> {
let replayed = replay_journal(&*store, &*journal)?;
Ok(Core {
store,
journal,
policy,
clock,
log: Arc::new(NoLog),
replayed,
})
}
pub fn with_log(mut self, log: Arc<dyn Log>) -> Core {
self.log = log;
self
}
pub fn replayed(&self) -> usize {
self.replayed
}
pub fn policy(&self) -> &Policy {
&self.policy
}
pub fn capabilities(&self) -> Capabilities {
self.policy.capabilities()
}
pub fn now(&self) -> i64 {
self.clock.now()
}
pub fn journal_ready(&self) -> bool {
self.journal.check().is_ok()
}
pub fn call(&self, request: CanonicalRequest<'_>) -> CoreResponse {
route::dispatch(self, &request).unwrap_or_else(|e| self.answer(e))
}
pub(crate) fn store(&self) -> &dyn Store {
&*self.store
}
pub(crate) fn answer(&self, e: CoreError) -> CoreResponse {
if let Some(detail) = e.detail() {
self.log.error(detail);
}
e.to_response()
}
pub(crate) fn warn(&self, message: &str) {
self.log.warn(message);
}
pub(crate) fn authed(
&self,
request: &CanonicalRequest<'_>,
op: impl FnOnce(&Caller) -> Result<CoreResponse, CoreError>,
) -> Result<CoreResponse, CoreError> {
let caller = auth::authenticate(self, request)?;
self.policy.admission.admit_request(&caller, self.now())?;
let mut response = op(&caller).unwrap_or_else(|e| self.answer(e));
response.expires_at = caller.expires_at;
Ok(response)
}
pub(crate) fn critical<T>(
&self,
f: impl FnOnce(&dyn Store, CommitHook<'_>) -> Result<T, StoreError>,
) -> Result<T, CoreError> {
let journal = &self.journal;
let mut hook = |record: &JournalRecord| journal.put(record);
f(&*self.store, &mut hook).map_err(CoreError::from)
}
}