photoncloud-monorepo/iam/crates/iam-audit/src/event.rs
centra 3eeb303dcb feat: Batch commit for T039.S3 deployment
Includes all pending changes needed for nixos-anywhere:
- fiberlb: L7 policy, rule, certificate types
- deployer: New service for cluster management
- nix-nos: Generic network modules
- Various service updates and fixes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 04:34:51 +09:00

475 lines
12 KiB
Rust

//! Audit event types
//!
//! Defines structured audit events for IAM operations.
use std::collections::HashMap;
use std::net::IpAddr;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use iam_types::Scope;
/// Audit event
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditEvent {
/// Unique event ID
pub id: String,
/// Event timestamp
pub timestamp: DateTime<Utc>,
/// Event kind
pub kind: AuditEventKind,
/// Principal ID (if known)
pub principal_id: Option<String>,
/// Source IP address
pub source_ip: Option<IpAddr>,
/// Request ID (for correlation)
pub request_id: Option<String>,
/// Session ID
pub session_id: Option<String>,
/// Additional metadata
pub metadata: HashMap<String, String>,
}
/// Kind of audit event
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum AuditEventKind {
/// Authentication event
Authn(AuthnEventData),
/// Authorization event
Authz(AuthzEventData),
/// Policy change event
Policy(PolicyEventData),
/// Token event (issue, revoke, refresh)
Token(TokenEventData),
/// Administrative action
Admin(AdminEventData),
}
/// Authentication event data
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthnEventData {
/// Whether authentication succeeded
pub success: bool,
/// Authentication method used (jwt, mtls, api_key, etc.)
pub method: String,
/// Failure reason (if failed)
pub failure_reason: Option<String>,
/// Token issuer (if JWT)
pub issuer: Option<String>,
/// Client certificate subject (if mTLS)
pub cert_subject: Option<String>,
}
/// Authorization event data
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthzEventData {
/// Whether authorization was granted
pub allowed: bool,
/// Action being authorized
pub action: String,
/// Resource being accessed
pub resource_kind: String,
pub resource_id: String,
/// Scope of the request
pub scope: Scope,
/// Matching policy ID (if allowed)
pub policy_id: Option<String>,
/// Denial reason (if denied)
pub denial_reason: Option<String>,
/// Roles evaluated
pub roles_evaluated: Vec<String>,
}
/// Policy change event data
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyEventData {
/// Type of change
pub change_type: PolicyChangeType,
/// Policy ID
pub policy_id: String,
/// Policy name
pub policy_name: Option<String>,
/// Scope affected
pub scope: Scope,
/// Actor who made the change
pub actor_id: String,
}
/// Type of policy change
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PolicyChangeType {
Created,
Updated,
Deleted,
Attached,
Detached,
}
/// Token event data
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenEventData {
/// Type of token event
pub event_type: TokenEventType,
/// Token ID or session ID
pub token_id: Option<String>,
/// Token TTL (if issued)
pub ttl_seconds: Option<u64>,
/// Scope of the token
pub scope: Option<Scope>,
}
/// Type of token event
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TokenEventType {
Issued,
Refreshed,
Revoked,
Expired,
}
/// Administrative action event data
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdminEventData {
/// Type of admin action
pub action: String,
/// Target entity type
pub target_type: String,
/// Target entity ID
pub target_id: String,
/// Details of the action
pub details: Option<String>,
}
impl AuditEvent {
/// Create a new audit event
pub fn new(kind: AuditEventKind) -> Self {
Self {
id: Uuid::new_v4().to_string(),
timestamp: Utc::now(),
kind,
principal_id: None,
source_ip: None,
request_id: None,
session_id: None,
metadata: HashMap::new(),
}
}
/// Create an authentication success event
pub fn authn_success(principal_id: &str, method: &str) -> Self {
Self::new(AuditEventKind::Authn(AuthnEventData {
success: true,
method: method.to_string(),
failure_reason: None,
issuer: None,
cert_subject: None,
}))
.with_principal(principal_id)
}
/// Create an authentication failure event
pub fn authn_failure(method: &str, reason: &str) -> Self {
Self::new(AuditEventKind::Authn(AuthnEventData {
success: false,
method: method.to_string(),
failure_reason: Some(reason.to_string()),
issuer: None,
cert_subject: None,
}))
}
/// Create an authorization allowed event
pub fn authz_allowed(
principal_id: &str,
action: &str,
resource_kind: &str,
resource_id: &str,
scope: Scope,
) -> Self {
Self::new(AuditEventKind::Authz(AuthzEventData {
allowed: true,
action: action.to_string(),
resource_kind: resource_kind.to_string(),
resource_id: resource_id.to_string(),
scope,
policy_id: None,
denial_reason: None,
roles_evaluated: Vec::new(),
}))
.with_principal(principal_id)
}
/// Create an authorization denied event
pub fn authz_denied(
principal_id: &str,
action: &str,
resource_kind: &str,
resource_id: &str,
scope: Scope,
reason: &str,
) -> Self {
Self::new(AuditEventKind::Authz(AuthzEventData {
allowed: false,
action: action.to_string(),
resource_kind: resource_kind.to_string(),
resource_id: resource_id.to_string(),
scope,
policy_id: None,
denial_reason: Some(reason.to_string()),
roles_evaluated: Vec::new(),
}))
.with_principal(principal_id)
}
/// Create a token issued event
pub fn token_issued(
principal_id: &str,
token_id: &str,
ttl_seconds: u64,
scope: Scope,
) -> Self {
Self::new(AuditEventKind::Token(TokenEventData {
event_type: TokenEventType::Issued,
token_id: Some(token_id.to_string()),
ttl_seconds: Some(ttl_seconds),
scope: Some(scope),
}))
.with_principal(principal_id)
}
/// Create a token revoked event
pub fn token_revoked(principal_id: &str, token_id: &str) -> Self {
Self::new(AuditEventKind::Token(TokenEventData {
event_type: TokenEventType::Revoked,
token_id: Some(token_id.to_string()),
ttl_seconds: None,
scope: None,
}))
.with_principal(principal_id)
}
/// Create a policy created event
pub fn policy_created(
actor_id: &str,
policy_id: &str,
policy_name: &str,
scope: Scope,
) -> Self {
Self::new(AuditEventKind::Policy(PolicyEventData {
change_type: PolicyChangeType::Created,
policy_id: policy_id.to_string(),
policy_name: Some(policy_name.to_string()),
scope,
actor_id: actor_id.to_string(),
}))
.with_principal(actor_id)
}
/// Create a policy deleted event
pub fn policy_deleted(actor_id: &str, policy_id: &str, scope: Scope) -> Self {
Self::new(AuditEventKind::Policy(PolicyEventData {
change_type: PolicyChangeType::Deleted,
policy_id: policy_id.to_string(),
policy_name: None,
scope,
actor_id: actor_id.to_string(),
}))
.with_principal(actor_id)
}
/// Set principal ID
pub fn with_principal(mut self, principal_id: &str) -> Self {
self.principal_id = Some(principal_id.to_string());
self
}
/// Set source IP
pub fn with_source_ip(mut self, ip: IpAddr) -> Self {
self.source_ip = Some(ip);
self
}
/// Set request ID
pub fn with_request_id(mut self, request_id: &str) -> Self {
self.request_id = Some(request_id.to_string());
self
}
/// Set session ID
pub fn with_session_id(mut self, session_id: &str) -> Self {
self.session_id = Some(session_id.to_string());
self
}
/// Add metadata
pub fn with_metadata(mut self, key: &str, value: &str) -> Self {
self.metadata.insert(key.to_string(), value.to_string());
self
}
/// Serialize to JSON
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(self)
}
/// Serialize to pretty JSON
pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::Ipv4Addr;
#[test]
fn test_authn_success_event() {
let event = AuditEvent::authn_success("alice", "jwt")
.with_source_ip(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)))
.with_request_id("req-123");
assert_eq!(event.principal_id, Some("alice".to_string()));
assert!(matches!(event.kind, AuditEventKind::Authn(ref data) if data.success));
let json = event.to_json().unwrap();
assert!(json.contains("alice"));
assert!(json.contains("jwt"));
}
#[test]
fn test_authn_failure_event() {
let event = AuditEvent::authn_failure("jwt", "Token expired");
assert!(event.principal_id.is_none());
match &event.kind {
AuditEventKind::Authn(data) => {
assert!(!data.success);
assert_eq!(data.failure_reason, Some("Token expired".to_string()));
}
_ => panic!("Expected Authn event"),
}
}
#[test]
fn test_authz_allowed_event() {
let event = AuditEvent::authz_allowed(
"alice",
"read",
"instance",
"vm-1",
Scope::project("proj-1", "org-1"),
);
assert_eq!(event.principal_id, Some("alice".to_string()));
match &event.kind {
AuditEventKind::Authz(data) => {
assert!(data.allowed);
assert_eq!(data.action, "read");
}
_ => panic!("Expected Authz event"),
}
}
#[test]
fn test_authz_denied_event() {
let event = AuditEvent::authz_denied(
"bob",
"delete",
"instance",
"vm-1",
Scope::project("proj-1", "org-1"),
"No matching policy",
);
match &event.kind {
AuditEventKind::Authz(data) => {
assert!(!data.allowed);
assert_eq!(data.denial_reason, Some("No matching policy".to_string()));
}
_ => panic!("Expected Authz event"),
}
}
#[test]
fn test_token_issued_event() {
let event = AuditEvent::token_issued("alice", "session-123", 3600, Scope::System);
match &event.kind {
AuditEventKind::Token(data) => {
assert!(matches!(data.event_type, TokenEventType::Issued));
assert_eq!(data.ttl_seconds, Some(3600));
}
_ => panic!("Expected Token event"),
}
}
#[test]
fn test_policy_created_event() {
let event = AuditEvent::policy_created(
"admin",
"policy-1",
"AllowReadInstances",
Scope::org("org-1"),
);
match &event.kind {
AuditEventKind::Policy(data) => {
assert!(matches!(data.change_type, PolicyChangeType::Created));
assert_eq!(data.policy_id, "policy-1");
}
_ => panic!("Expected Policy event"),
}
}
#[test]
fn test_event_serialization() {
let event =
AuditEvent::authn_success("alice", "jwt").with_metadata("user_agent", "curl/7.68.0");
let json = event.to_json().unwrap();
let parsed: AuditEvent = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.principal_id, event.principal_id);
assert_eq!(
parsed.metadata.get("user_agent"),
Some(&"curl/7.68.0".to_string())
);
}
}