use crate::access_control::AccessLevel;
use crate::bootstrap::ADMIN_BOUNDED_CONTEXT_NAME;
use crate::event_store::{BoundedContext, Event, EventOrigin, EventType};
use crate::shared::{Metadata, Tag, TagMapping};
use chrono::{DateTime, Utc};
use serde_json::{json, Value};
use std::collections::BTreeSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum AccessEventKind {
AccessGranted,
AccessRevoked,
CapabilityGranted,
CapabilityRevoked,
DelegableCapabilitiesSet,
AccessBaselined,
PrivateFieldAccessGranted,
PrivateFieldAccessRevoked,
}
impl AccessEventKind {
pub const ALL: [AccessEventKind; 8] = [
AccessEventKind::AccessGranted,
AccessEventKind::AccessRevoked,
AccessEventKind::CapabilityGranted,
AccessEventKind::CapabilityRevoked,
AccessEventKind::DelegableCapabilitiesSet,
AccessEventKind::AccessBaselined,
AccessEventKind::PrivateFieldAccessGranted,
AccessEventKind::PrivateFieldAccessRevoked,
];
pub fn is_mapping_event(self) -> bool {
!matches!(
self,
AccessEventKind::PrivateFieldAccessGranted | AccessEventKind::PrivateFieldAccessRevoked
)
}
pub fn name(self) -> &'static str {
match self {
AccessEventKind::AccessGranted => "AccessGranted",
AccessEventKind::AccessRevoked => "AccessRevoked",
AccessEventKind::CapabilityGranted => "CapabilityGranted",
AccessEventKind::CapabilityRevoked => "CapabilityRevoked",
AccessEventKind::DelegableCapabilitiesSet => "DelegableCapabilitiesSet",
AccessEventKind::AccessBaselined => "AccessBaselined",
AccessEventKind::PrivateFieldAccessGranted => "PrivateFieldAccessGranted",
AccessEventKind::PrivateFieldAccessRevoked => "PrivateFieldAccessRevoked",
}
}
pub fn from_name(name: &str) -> Option<AccessEventKind> {
Self::ALL.into_iter().find(|kind| kind.name() == name)
}
fn extra_fields(self) -> &'static [(&'static str, &'static str, bool)] {
match self {
AccessEventKind::AccessGranted => &[
("actor_role_id", "string", false),
("level", "string", true),
("can_read_sensitive", "boolean", true),
("scope", "string", false),
("capabilities", "array", false),
("delegable", "array", false),
],
AccessEventKind::AccessRevoked => &[
("actor_role_id", "string", false),
("reason", "string", true),
],
AccessEventKind::CapabilityGranted | AccessEventKind::CapabilityRevoked => &[
("actor_role_id", "string", false),
("capability", "string", true),
("via", "string", true),
],
AccessEventKind::DelegableCapabilitiesSet => &[
("actor_role_id", "string", false),
("delegable", "array", true),
],
AccessEventKind::AccessBaselined => &[
("actor_role_id", "string", false),
("level", "string", true),
("can_read_sensitive", "boolean", true),
("scope", "string", false),
("capabilities", "array", true),
("delegable", "array", true),
],
AccessEventKind::PrivateFieldAccessGranted => &[
("actor_role_id", "string", false),
("grant_id", "string", true),
("event_sequence", "integer", false),
("command_id", "string", false),
],
AccessEventKind::PrivateFieldAccessRevoked => &[
("actor_role_id", "string", false),
("grant_id", "string", true),
("reason", "string", false),
],
}
}
}
pub fn access_event_schema(kind: AccessEventKind) -> String {
let mut properties = serde_json::Map::new();
let mut required = vec![json!("role_id"), json!("bounded_context")];
properties.insert("role_id".into(), json!({ "type": "string" }));
properties.insert("bounded_context".into(), json!({ "type": "string" }));
for (name, ty, is_required) in kind.extra_fields() {
let property = if *ty == "array" {
json!({ "type": "array", "items": { "type": "string" } })
} else if !*is_required {
json!({ "type": [ty, "null"] })
} else {
json!({ "type": ty })
};
properties.insert((*name).into(), property);
if *is_required {
required.push(json!(name));
}
}
json!({
"type": "object",
"properties": properties,
"required": required,
})
.to_string()
}
pub fn access_event_type(admin: &BoundedContext, kind: AccessEventKind) -> EventType {
EventType {
bounded_context: admin.clone(),
name: kind.name().to_string(),
schema: access_event_schema(kind),
schema_version: 1,
tag_mappings: vec![
TagMapping::new("role", "role_id"),
TagMapping::new("bounded_context", "bounded_context"),
],
owner_tag_key: None,
sensitive_fields: Vec::new(),
private_fields: Vec::new(),
external_creation_allowed: false,
direct_creation_allowed: false,
system_triggered_allowed: false,
system_triggered_schedule: None,
missed_occurrence_policy: None,
schedule_position: None,
last_fired_at: None,
event_read_allowed: false,
}
}
pub fn level_str(level: AccessLevel) -> &'static str {
match level {
AccessLevel::Read => "read",
AccessLevel::Write => "write",
AccessLevel::Admin => "admin",
}
}
pub struct AccessChange {
pub kind: AccessEventKind,
pub role_id: String,
pub bounded_context: String,
pub actor_role_id: Option<String>,
pub details: serde_json::Map<String, Value>,
}
impl AccessChange {
pub fn granted(
mapping: &crate::access_control::RoleAccessMapping,
actor_role_id: Option<&str>,
) -> Self {
Self::new(
AccessEventKind::AccessGranted,
&mapping.role.id,
&mapping.bounded_context.name,
actor_role_id,
json!({
"level": level_str(mapping.level),
"can_read_sensitive": mapping.can_read_sensitive,
"scope": mapping.scope,
"capabilities": mapping.capabilities,
"delegable": mapping.delegable,
}),
)
}
pub fn private_field_granted(grant: &crate::access_control::PrivateFieldGrant) -> Self {
Self::new(
AccessEventKind::PrivateFieldAccessGranted,
&grant.grantee.id,
&grant.bounded_context.name,
Some(&grant.grantor.id),
json!({
"grant_id": grant.id,
"event_sequence": grant.event_sequence,
"command_id": grant.command_id,
}),
)
}
pub fn private_field_revoked(grant: &crate::access_control::PrivateFieldGrant) -> Self {
Self::new(
AccessEventKind::PrivateFieldAccessRevoked,
&grant.grantee.id,
&grant.bounded_context.name,
Some(&grant.grantor.id),
json!({ "grant_id": grant.id }),
)
}
pub fn revoked(
role_id: &str,
bounded_context: &str,
actor_role_id: &str,
role_revoked: bool,
) -> Self {
Self::new(
AccessEventKind::AccessRevoked,
role_id,
bounded_context,
Some(actor_role_id),
json!({ "reason": if role_revoked { "role_revoked" } else { "grant_revoked" } }),
)
}
pub fn context_ended(role_id: &str, bounded_context: &str, actor_role_id: &str) -> Self {
Self::new(
AccessEventKind::AccessRevoked,
role_id,
bounded_context,
Some(actor_role_id),
json!({ "reason": "bounded_context_deleted" }),
)
}
pub fn private_field_ended(
grant_id: &str,
grantee_id: &str,
bounded_context: &str,
actor_role_id: &str,
) -> Self {
Self::new(
AccessEventKind::PrivateFieldAccessRevoked,
grantee_id,
bounded_context,
Some(actor_role_id),
json!({ "grant_id": grant_id, "reason": "bounded_context_deleted" }),
)
}
pub fn capability(
granted: bool,
role_id: &str,
bounded_context: &str,
actor_role_id: &str,
capability: &str,
via_admin: bool,
) -> Self {
Self::new(
if granted {
AccessEventKind::CapabilityGranted
} else {
AccessEventKind::CapabilityRevoked
},
role_id,
bounded_context,
Some(actor_role_id),
json!({
"capability": capability,
"via": if via_admin { "admin" } else { "superadmin" },
}),
)
}
pub fn delegable_set(
role_id: &str,
bounded_context: &str,
actor_role_id: &str,
delegable: &BTreeSet<String>,
) -> Self {
Self::new(
AccessEventKind::DelegableCapabilitiesSet,
role_id,
bounded_context,
Some(actor_role_id),
json!({ "delegable": delegable }),
)
}
pub fn baselined(mapping: &crate::access_control::RoleAccessMapping) -> Self {
Self::new(
AccessEventKind::AccessBaselined,
&mapping.role.id,
&mapping.bounded_context.name,
None,
json!({
"level": level_str(mapping.level),
"can_read_sensitive": mapping.can_read_sensitive,
"scope": mapping.scope,
"capabilities": mapping.capabilities,
"delegable": mapping.delegable,
}),
)
}
fn new(
kind: AccessEventKind,
role_id: &str,
bounded_context: &str,
actor_role_id: Option<&str>,
details: Value,
) -> Self {
let Value::Object(details) = details else {
unreachable!("every constructor above passes a JSON object")
};
Self {
kind,
role_id: role_id.to_string(),
bounded_context: bounded_context.to_string(),
actor_role_id: actor_role_id.map(str::to_string),
details,
}
}
pub fn payload(&self) -> String {
let mut payload = self.details.clone();
payload.insert("role_id".into(), json!(self.role_id));
payload.insert("bounded_context".into(), json!(self.bounded_context));
payload.insert("actor_role_id".into(), json!(self.actor_role_id));
Value::Object(payload).to_string()
}
pub fn into_event(self, admin: &BoundedContext, sequence: i64, now: DateTime<Utc>) -> Event {
debug_assert_eq!(admin.name, ADMIN_BOUNDED_CONTEXT_NAME);
let event_type = access_event_type(admin, self.kind);
let payload = self.payload();
Event {
bounded_context: admin.clone(),
metadata: Metadata {
r#type: event_type.name.clone(),
version: event_type.schema_version,
client_id: "system".to_string(),
created_at: now,
correlation_id: Some(crate::shared::generate_token_id()),
causation_id: None,
},
event_type,
payload,
sequence,
tags: vec![
Tag {
key: "role".into(),
value: Some(self.role_id),
},
Tag {
key: "bounded_context".into(),
value: Some(self.bounded_context),
},
],
encryption_keys: Vec::new(),
origin: EventOrigin::SkiljRecorded,
}
}
}
#[derive(Debug, Clone)]
pub struct AccessEventRecord {
pub kind: AccessEventKind,
pub payload: Value,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct AccessHistory {
pub known: bool,
pub held: bool,
pub level: Option<String>,
pub can_read_sensitive: bool,
pub scope: Option<String>,
pub capabilities: BTreeSet<String>,
pub delegable: BTreeSet<String>,
}
fn string_set(value: Option<&Value>) -> BTreeSet<String> {
value
.and_then(Value::as_array)
.map(|items| {
items
.iter()
.filter_map(|item| item.as_str().map(str::to_string))
.collect()
})
.unwrap_or_default()
}
pub fn fold_access_events(events: &[AccessEventRecord], as_of: DateTime<Utc>) -> AccessHistory {
let mut state = AccessHistory::default();
for event in events
.iter()
.filter(|event| event.kind.is_mapping_event() && event.created_at <= as_of)
{
state.known = true;
let payload = &event.payload;
match event.kind {
AccessEventKind::AccessGranted | AccessEventKind::AccessBaselined => {
state.held = true;
state.level = payload["level"].as_str().map(str::to_string);
state.can_read_sensitive = payload["can_read_sensitive"].as_bool().unwrap_or(false);
state.scope = payload["scope"].as_str().map(str::to_string);
state.capabilities = string_set(payload.get("capabilities"));
state.delegable = string_set(payload.get("delegable"));
}
AccessEventKind::AccessRevoked => {
state = AccessHistory {
known: true,
..AccessHistory::default()
};
}
AccessEventKind::CapabilityGranted => {
if let Some(capability) = payload["capability"].as_str() {
state.capabilities.insert(capability.to_string());
}
}
AccessEventKind::CapabilityRevoked => {
if let Some(capability) = payload["capability"].as_str() {
state.capabilities.remove(capability);
}
}
AccessEventKind::DelegableCapabilitiesSet => {
state.delegable = string_set(payload.get("delegable"));
}
AccessEventKind::PrivateFieldAccessGranted
| AccessEventKind::PrivateFieldAccessRevoked => {}
}
}
state
}