use std::collections::{HashMap, HashSet};
use std::fmt::Display;
use std::hash::Hash;
use std::sync::Arc;
use super::LogLevel;
use super::interface::{Indexed, Loggable};
use crate::common::policy_store::PoliciesContainer;
use crate::jwt::Token;
use crate::log::loggable_fn::LoggableFn;
use cedar_policy::EntityUid;
use rand::Rng;
use rand::{SeedableRng, rngs::StdRng};
use smol_str::{SmolStr, ToSmolStr};
use std::sync::{LazyLock, Mutex};
use uuid7::Uuid;
pub(crate) const ISO8601: &str = "%Y-%m-%dT%H:%M:%S%.3fZ";
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct LogEntry {
#[serde(flatten)]
pub base: BaseLogEntry,
pub msg: String,
#[serde(flatten)]
pub auth_info: Option<AuthorizationLogInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_msg: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cedar_lang_version: Option<semver::Version>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cedar_sdk_version: Option<semver::Version>,
#[serde(skip_serializing_if = "Option::is_none")]
pub build_commit: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub build_timestamp: Option<String>,
}
impl LogEntry {
pub(crate) fn new(base: BaseLogEntry) -> LogEntry {
Self {
base,
auth_info: None,
msg: String::new(),
error_msg: None,
cedar_lang_version: None,
cedar_sdk_version: None,
build_commit: None,
build_timestamp: None,
}
}
pub(crate) fn set_message(mut self, message: String) -> Self {
self.msg = message;
self
}
pub(crate) fn set_error(mut self, error: String) -> Self {
self.error_msg = Some(error);
self
}
pub(crate) fn set_auth_info(mut self, auth_info: AuthorizationLogInfo) -> Self {
self.auth_info = Some(auth_info);
self
}
pub(crate) fn set_cedar_version(mut self) -> Self {
self.cedar_lang_version = Some(cedar_policy::get_lang_version());
self.cedar_sdk_version = Some(cedar_policy::get_sdk_version());
self
}
pub(crate) fn set_build_info(
mut self,
build_commit: Option<&str>,
build_timestamp: Option<&str>,
) -> Self {
self.build_commit = build_commit.map(ToString::to_string);
self.build_timestamp = build_timestamp.map(ToString::to_string);
self
}
}
impl Indexed for LogEntry {
fn get_id(&self) -> Uuid {
self.base.get_id()
}
fn get_additional_ids(&self) -> Vec<Uuid> {
self.base.get_additional_ids()
}
fn get_tags(&self) -> Vec<&str> {
self.base.get_tags()
}
}
impl Loggable for LogEntry {
fn get_log_level(&self) -> Option<LogLevel> {
self.base.get_log_level()
}
fn get_log_kind(&self) -> Option<LogType> {
self.base.get_log_kind()
}
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
serde::Serialize,
serde::Deserialize,
strum::IntoStaticStr,
derive_more::Display,
)]
pub enum LogType {
Decision,
System,
Metric,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct AuthorizationLogInfo {
pub action: String,
pub resource: String,
pub context: serde_json::Value,
pub entities: serde_json::Value,
pub authorize_info: Vec<AuthorizeInfo>,
pub authorized: bool,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct AuthorizeInfo {
pub principal: String,
pub diagnostics: Diagnostics,
pub decision: Decision,
}
#[derive(
Debug, Clone, PartialEq, Eq, Copy, serde::Serialize, serde::Deserialize, strum::AsRefStr,
)]
#[serde(rename_all = "UPPERCASE")]
pub enum Decision {
Allow,
Deny,
}
impl Display for Decision {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Decision::Allow => f.write_str("ALLOW"),
Decision::Deny => f.write_str("DENY"),
}
}
}
#[doc(hidden)]
impl From<cedar_policy::Decision> for Decision {
fn from(value: cedar_policy::Decision) -> Self {
match value {
cedar_policy::Decision::Allow => Decision::Allow,
cedar_policy::Decision::Deny => Decision::Deny,
}
}
}
impl From<bool> for Decision {
fn from(value: bool) -> Self {
if value { Self::Allow } else { Self::Deny }
}
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct PolicyEvaluationError {
pub id: String,
pub error: String,
}
#[doc(hidden)]
impl From<&cedar_policy::AuthorizationError> for PolicyEvaluationError {
fn from(value: &cedar_policy::AuthorizationError) -> Self {
match value {
cedar_policy::AuthorizationError::PolicyEvaluationError(policy_evaluation_error) => {
Self {
id: policy_evaluation_error.policy_id().to_string(),
error: policy_evaluation_error.inner().to_string(),
}
},
}
}
}
#[derive(Debug, Default, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Diagnostics {
pub reason: HashSet<PolicyInfo>,
pub errors: Vec<PolicyEvaluationError>,
}
#[derive(Debug, Default, Clone, PartialEq, serde::Serialize)]
pub(crate) struct DiagnosticsSummary {
pub reason: HashSet<PolicyInfo>,
pub errors: Vec<PolicyEvaluationError>,
}
impl DiagnosticsSummary {
pub(crate) fn from_diagnostics(diagnostics: &[Diagnostics]) -> Self {
let mut reason: HashSet<PolicyInfo> = HashSet::new();
let mut errors = Vec::new();
for diagnostic in diagnostics {
reason.extend(diagnostic.reason.iter().cloned());
errors.extend(diagnostic.errors.iter().cloned());
}
Self { reason, errors }
}
}
#[derive(Debug, Default, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct PolicyInfo {
pub id: SmolStr,
pub description: Option<SmolStr>,
}
impl Hash for PolicyInfo {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
impl Diagnostics {
pub(crate) fn new(
cedar_diagnostic: &cedar_policy::Diagnostics,
policies: &PoliciesContainer,
) -> Self {
let errors = cedar_diagnostic
.errors()
.map(std::convert::Into::into)
.collect();
let reason = cedar_diagnostic
.reason()
.map(|policy_id| {
let id: SmolStr = policy_id.to_string().into();
PolicyInfo {
description: policies
.get_policy_description(id.as_str())
.map(SmolStr::from),
id,
}
})
.collect::<HashSet<_>>();
Self { reason, errors }
}
}
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub(crate) struct DecisionLogEntry {
#[serde(flatten)]
pub base: BaseLogEntry,
pub policystore_id: SmolStr,
pub policystore_version: SmolStr,
pub principal: Vec<SmolStr>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lock_client_id: Option<String>,
pub diagnostics: DiagnosticsSummary,
pub action: String,
pub resource: String,
pub decision: Decision,
#[serde(skip_serializing_if = "LogTokensInfo::is_empty")]
pub tokens: LogTokensInfo,
pub decision_time_micro_sec: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub pushed_data: Option<PushedDataInfo>,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub(crate) struct MetricsLogEntry {
#[serde(flatten)]
pub base: BaseLogEntry,
pub policy_stats: HashMap<String, i64>,
pub error_counters: HashMap<String, i64>,
pub operational_stats: HashMap<String, i64>,
pub interval_secs: i64,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub(crate) struct PushedDataInfo {
pub keys: Vec<SmolStr>,
}
impl DecisionLogEntry {
pub(crate) fn principal(user: bool, workload: bool) -> Vec<SmolStr> {
let mut tags = Vec::with_capacity(2);
if user {
tags.push("User".into());
}
if workload {
tags.push("Workload".into());
}
tags
}
pub(crate) fn all_principals(principals: &[EntityUid]) -> Vec<SmolStr> {
principals
.iter()
.map(|uid| uid.type_name().to_smolstr())
.collect()
}
}
impl Indexed for DecisionLogEntry {
fn get_id(&self) -> Uuid {
self.base.get_id()
}
fn get_additional_ids(&self) -> Vec<Uuid> {
self.base.get_additional_ids()
}
fn get_tags(&self) -> Vec<&str> {
self.base.get_tags()
}
}
impl Loggable for DecisionLogEntry {
fn get_log_level(&self) -> Option<LogLevel> {
self.base.get_log_level()
}
fn get_log_kind(&self) -> Option<LogType> {
self.base.get_log_kind()
}
}
impl Indexed for MetricsLogEntry {
fn get_id(&self) -> Uuid {
self.base.get_id()
}
fn get_additional_ids(&self) -> Vec<Uuid> {
self.base.get_additional_ids()
}
fn get_tags(&self) -> Vec<&str> {
self.base.get_tags()
}
}
impl Loggable for MetricsLogEntry {
fn get_log_level(&self) -> Option<LogLevel> {
self.base.get_log_level()
}
fn get_log_kind(&self) -> Option<LogType> {
self.base.get_log_kind()
}
}
fn get_std_rng() -> StdRng {
StdRng::try_from_rng(&mut rand::rngs::SysRng).expect("failed to seed StdRng from OS RNG")
}
pub(crate) fn gen_uuid7() -> Uuid {
use std::cell::RefCell;
use uuid7::V7Generator;
const ROLLBACK_ALLOWANCE: u64 = 10_000;
thread_local! {
static V7_GENERATOR: RefCell<
V7Generator<uuid7::generator::with_rand010::Adapter<StdRng>>,
> = {
let mut g = V7Generator::with_rand010(get_std_rng());
g.set_rollback_allowance(ROLLBACK_ALLOWANCE);
RefCell::new(g)
};
}
let custom_unix_ts_ms = chrono::Utc::now().timestamp_millis();
V7_GENERATOR.with(|g| {
g.borrow_mut()
.generate_or_reset_with_ts(custom_unix_ts_ms.cast_unsigned())
})
}
pub(crate) fn gen_uuid4() -> Uuid {
static RND_UUID4: LazyLock<Mutex<StdRng>> = LazyLock::new(|| Mutex::new(get_std_rng()));
let mut bytes = [0u8; 16];
RND_UUID4
.lock()
.expect("RND_UUID4 should be locked")
.fill_bytes(&mut bytes);
bytes[6] = (bytes[6] & 0x0F) | 0x40;
bytes[8] = (bytes[8] & 0x3F) | 0x80;
Uuid::from(bytes)
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct BaseLogEntry {
pub id: Uuid,
#[serde(skip_serializing_if = "Option::is_none")]
pub request_id: Option<Uuid>,
pub timestamp: Option<String>,
pub log_kind: LogType,
#[serde(skip_serializing_if = "Option::is_none")]
pub level: Option<LogLevel>,
}
impl BaseLogEntry {
pub(crate) fn new_system(log_level: LogLevel, request_id: Uuid) -> Self {
Self::new_system_opt_request_id(log_level, Some(request_id))
}
pub(crate) fn new_decision(request_id: Uuid) -> Self {
Self::new_decision_opt_request_id(Some(request_id))
}
#[allow(dead_code)]
pub(crate) fn new_metric(request_id: Uuid) -> Self {
Self::new_metric_opt_request_id(Some(request_id))
}
pub(crate) fn new_system_opt_request_id(log_level: LogLevel, request_id: Option<Uuid>) -> Self {
Self::new_opt_request_id(LogType::System, Some(log_level), request_id)
}
pub(crate) fn new_decision_opt_request_id(request_id: Option<Uuid>) -> Self {
Self::new_opt_request_id(LogType::Decision, None, request_id)
}
pub(crate) fn new_metric_opt_request_id(request_id: Option<Uuid>) -> Self {
Self::new_opt_request_id(LogType::Metric, None, request_id)
}
fn new_opt_request_id(
log_type: LogType,
log_level: Option<LogLevel>,
request_id: Option<Uuid>,
) -> Self {
let local_time_string = chrono::Local::now().format(ISO8601).to_string();
let default_log_level = if log_type == LogType::System {
Some(if let Some(log_level_val) = log_level {
log_level_val
} else {
LogLevel::TRACE
})
} else {
None
};
Self {
id: gen_uuid7(),
request_id,
timestamp: Some(local_time_string),
log_kind: log_type,
level: default_log_level,
}
}
pub(crate) fn with_fn<F, R>(self, builder: F) -> LoggableFn<F>
where
R: Loggable + Indexed,
for<'a> F: Fn(BaseLogEntry) -> R,
{
LoggableFn::new(self, builder)
}
}
impl Indexed for BaseLogEntry {
fn get_id(&self) -> Uuid {
self.id
}
fn get_additional_ids(&self) -> Vec<Uuid> {
self.request_id.into_iter().collect()
}
fn get_tags(&self) -> Vec<&'static str> {
let mut tags = Vec::with_capacity(2);
tags.push(self.log_kind.into());
if let Some(level) = self.level {
tags.push(level.into());
}
tags
}
}
impl Loggable for BaseLogEntry {
fn get_log_level(&self) -> Option<LogLevel> {
self.level
}
fn get_log_kind(&self) -> Option<LogType> {
Some(self.log_kind)
}
}
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub(crate) struct LogTokensInfo(pub HashMap<String, HashMap<String, serde_json::Value>>);
impl LogTokensInfo {
pub(crate) fn new(tokens: &HashMap<String, Arc<Token>>, decision_log_jwt_id: &str) -> Self {
let tokens_logging_info = tokens
.iter()
.map(|(tkn_name, tkn)| (tkn_name.clone(), tkn.logging_info(decision_log_jwt_id)))
.collect::<HashMap<String, HashMap<String, serde_json::Value>>>();
Self(tokens_logging_info)
}
pub(crate) fn empty() -> Self {
Self(HashMap::new())
}
pub(crate) fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::{Uuid, gen_uuid7};
use std::collections::HashSet;
use std::sync::{Arc, Mutex};
use std::thread;
#[test]
fn gen_uuid7_thread_local_storm_is_unique() {
const THREADS: usize = 32;
const PER_THREAD: usize = 5_000;
let collected: Arc<Mutex<Vec<Uuid>>> = Arc::new(Mutex::new(Vec::new()));
thread::scope(|scope| {
for _ in 0..THREADS {
let collected = Arc::clone(&collected);
scope.spawn(move || {
let mut local = Vec::with_capacity(PER_THREAD);
for _ in 0..PER_THREAD {
local.push(gen_uuid7());
}
collected.lock().expect("collect lock").extend(local);
});
}
});
let ids = collected.lock().expect("collect lock");
assert_eq!(
ids.len(),
THREADS * PER_THREAD,
"all UUIDs generated by worker threads should be collected"
);
assert!(
ids.iter().all(|id| id.as_bytes()[6] >> 4 == 0x7),
"all generated UUIDs should have the UUIDv7 version nibble"
);
let unique: HashSet<&Uuid> = ids.iter().collect();
assert_eq!(
unique.len(),
ids.len(),
"uuid7 collision under thread storm"
);
}
}