use cyberbrain_core::{Error, Result};
use serde::{Deserialize, Serialize};
pub const DEFAULT_WINDOW_HOURS: i64 = 72;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Role {
Admin,
Auditor,
Countersigner,
}
impl Role {
pub fn parse(s: &str) -> Result<Self> {
match s {
"admin" => Ok(Role::Admin),
"auditor" => Ok(Role::Auditor),
"countersigner" => Ok(Role::Countersigner),
other => Err(Error::Config(format!(
"unknown role {other:?}; one of admin, auditor, countersigner"
))),
}
}
pub fn as_str(self) -> &'static str {
match self {
Role::Admin => "admin",
Role::Auditor => "auditor",
Role::Countersigner => "countersigner",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Principal {
pub id: String,
pub name: String,
pub role: Role,
pub created_at: String,
pub revoked_at: Option<String>,
}
impl Principal {
pub fn is_active(&self) -> bool {
self.revoked_at.is_none()
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct AccessRequest {
pub id: String,
pub requester: String,
pub requester_name: String,
pub device: Option<String>,
pub from: Option<String>,
pub to: Option<String>,
pub reason: String,
pub created_at: String,
pub approved_by: Option<String>,
pub approved_by_name: Option<String>,
pub approved_at: Option<String>,
pub expires_at: Option<String>,
pub disclosures: i64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum RequestState {
Pending,
Open,
Closed,
}
impl AccessRequest {
pub fn state(&self, now: jiff::Timestamp) -> RequestState {
match (&self.approved_at, &self.expires_at) {
(None, _) => RequestState::Pending,
(Some(_), Some(exp)) => match exp.parse::<jiff::Timestamp>() {
Ok(t) if now <= t => RequestState::Open,
_ => RequestState::Closed,
},
(Some(_), None) => RequestState::Closed,
}
}
pub fn line(&self, now: jiff::Timestamp) -> String {
let what = match &self.device {
Some(d) => d.clone(),
None => "all devices".to_string(),
};
let period = match (&self.from, &self.to) {
(Some(f), Some(t)) => format!("{f} to {t}"),
(Some(f), None) => format!("from {f}"),
(None, Some(t)) => format!("up to {t}"),
(None, None) => "the whole record".to_string(),
};
let state = match self.state(now) {
RequestState::Pending => "awaiting countersignature".to_string(),
RequestState::Open => format!(
"open until {}",
self.expires_at.as_deref().unwrap_or("unknown")
),
RequestState::Closed => "closed".to_string(),
};
format!(
"{} {}\n {} · {} · asked by {} on {}\n reason: {}\n {} disclosure(s)\n",
self.id,
state,
what,
period,
self.requester_name,
self.created_at,
self.reason,
self.disclosures
)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Denied {
NotAuthorised(String),
WrongRole { need: Role, has: Role },
NotApproved(String),
WindowClosed(String),
SamePerson,
}
impl std::fmt::Display for Denied {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Denied::NotAuthorised(m) => write!(f, "{m}"),
Denied::WrongRole { need, has } => write!(
f,
"this needs the {} role; that credential is {}",
need.as_str(),
has.as_str()
),
Denied::NotApproved(id) => write!(
f,
"request {id} has not been countersigned; activity cannot be read until \
somebody else approves it"
),
Denied::WindowClosed(id) => write!(
f,
"the window for request {id} has closed. Make a new request rather than \
extending this one, so the reason is stated again"
),
Denied::SamePerson => write!(
f,
concat!(
"a request cannot be countersigned by the person who made it. ",
"That is the rule, not an obstacle to work around"
)
),
}
}
}