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>
475 lines
12 KiB
Rust
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())
|
|
);
|
|
}
|
|
}
|