ai-agent/security/rbac_engine.py

873 lines
No EOL
34 KiB
Python

import logging
import os
import hashlib
import hmac
import json
import ssl
import base64
import time
from enum import Enum
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.x509 import load_pem_x509_certificate, ocsp
from cryptography.x509.oid import NameOID
from dataclasses import dataclass, field
from typing import Dict, Set, Optional, Any, List, Tuple
from datetime import datetime, timedelta
from urllib import request
from security.encrypt import create_tls_context
logger = logging.getLogger('RBACEngine')
class BoundaryType(Enum):
GLOBAL = "global"
INTERNAL = "internal"
RESTRICTED = "restricted"
class Role(Enum):
ADMIN = ("admin", BoundaryType.GLOBAL)
DEVELOPER = ("developer", BoundaryType.INTERNAL)
AUDITOR = ("auditor", BoundaryType.INTERNAL)
MANAGER = ("manager", BoundaryType.INTERNAL)
def __new__(cls, value, boundary):
obj = object.__new__(cls)
obj._value_ = value
obj.boundary = boundary
return obj
# Role inheritance mapping (role -> parent_roles)
ROLE_INHERITANCE = {
Role.ADMIN: {Role.DEVELOPER, Role.MANAGER, Role.AUDITOR}, # Admin inherits all roles
Role.MANAGER: {Role.DEVELOPER},
Role.DEVELOPER: {Role.AUDITOR} # Developer inherits basic permissions from AUDITOR
}
def validate_circular_inheritance(child: Role, parent: Role) -> None:
"""Validate that inheritance doesn't create circular references.
Args:
child: The child role being assigned
parent: The parent role being inherited from
Raises:
ValueError: If circular inheritance is detected
"""
if child == parent:
raise ValueError(f"Circular inheritance: {child} cannot inherit from itself")
def validate_circular_inheritance(self, child: 'Role', parent: 'Role') -> None:
"""Validate that inheritance doesn't create circular references.
Args:
child: The child role being assigned
parent: The parent role being inherited from
Raises:
ValueError: If circular inheritance is detected
"""
if parent not in self.role_inheritance:
return
parents = self.role_inheritance[parent]
if isinstance(parents, set):
for p in parents:
if p == child:
raise ValueError(
f"Circular inheritance detected: {child} would create a loop through {parent}"
)
self.validate_circular_inheritance(child, p)
else:
current = parents
while current in self.role_inheritance and self.role_inheritance[current] is not None:
if self.role_inheritance[current] == child:
raise ValueError(
f"Circular inheritance detected: {child} would create a loop through {current}"
)
current = self.role_inheritance[current]
@classmethod
def validate_boundary(cls, child: 'Role', parent: 'Role') -> None:
"""Validate role inheritance boundary compatibility.
Args:
child: The child role being assigned
parent: The parent role being inherited from
Raises:
ValueError: If boundary inheritance rules are violated
"""
if child not in ROLE_BOUNDARIES or parent not in ROLE_BOUNDARIES:
return
child_boundary = ROLE_BOUNDARIES[child]
parent_boundary = ROLE_BOUNDARIES[parent]
# Boundary inheritance rules
if (child_boundary == BoundaryType.INTERNAL and
parent_boundary == BoundaryType.RESTRICTED):
raise ValueError(
f"INTERNAL role {child} cannot inherit from RESTRICTED role {parent}"
)
if (child_boundary == BoundaryType.RESTRICTED and
parent_boundary != BoundaryType.GLOBAL):
raise ValueError(
f"RESTRICTED role {child} can only inherit from GLOBAL roles"
)
# Boundary hierarchy check (child cannot be more permissive than parent)
boundary_order = {
RoleBoundary.RESTRICTED: 0,
RoleBoundary.INTERNAL: 1,
RoleBoundary.GLOBAL: 2
}
if boundary_order[child_boundary] > boundary_order[parent_boundary]:
raise ValueError(
f"Boundary hierarchy violation: {child} ({child_boundary}) cannot inherit from "
f"{parent} ({parent_boundary}) as it's more permissive"
)
class RoleBoundary(Enum):
"""Defines boundaries for role assignments"""
GLOBAL = "global" # Can be assigned to any user
INTERNAL = "internal" # Can only be assigned to internal users
RESTRICTED = "restricted" # Highly restricted assignment
@dataclass
class Permission:
resource: str
actions: Set[str] = field(default_factory=set)
@dataclass
class ClientCertInfo:
"""Represents relevant info extracted from a client certificate."""
subject: Dict[str, str] # e.g., {'CN': 'user.example.com', 'OU': 'developer'}
issuer: Dict[str, str] = field(default_factory=dict) # Certificate issuer information
serial_number: int = 0 # Certificate serial number
not_before: Optional[datetime] = None # Validity period start
not_after: Optional[datetime] = None # Validity period end
fingerprint: str = "" # SHA-256 fingerprint of the certificate
raw_cert: Any = None # Raw certificate object for additional verification
class RBACEngine:
def __init__(self, encryption_key: bytes):
# Role definitions with permissions
self.roles = {
Role.ADMIN: Permission('admin', {'delegate', 'audit', 'configure'}),
Role.DEVELOPER: Permission('tasks', {'create', 'read', 'update'}),
Role.AUDITOR: Permission('logs', {'read', 'export'}), # Added export permission
Role.MANAGER: Permission('tasks', {'approve', 'delegate'})
}
# Role inheritance relationships
self.role_inheritance: Dict[Role, Union[Role, Set[Role]]] = {}
# Role assignment boundaries
self.role_boundaries = {
Role.ADMIN: RoleBoundary.RESTRICTED,
Role.DEVELOPER: RoleBoundary.INTERNAL,
Role.AUDITOR: RoleBoundary.GLOBAL,
Role.MANAGER: RoleBoundary.INTERNAL
}
# User role assignments
self.user_roles: Dict[str, Role] = {}
# Certificate fingerprints for validation (maintain both for backward compatibility)
self.cert_fingerprints: Dict[str, str] = {}
self.trusted_cert_fingerprints: Set[str] = set()
# Domain restrictions for role assignments
self.domain_restrictions = {
Role.ADMIN: {'example.com'},
Role.MANAGER: {'internal.example.com'}
}
def validate_certificate(self, cert_info: ClientCertInfo) -> None:
"""Validate client certificate meets security requirements.
Args:
cert_info: Parsed certificate information
Raises:
ValueError: If certificate fails validation
"""
if not cert_info.subject.get('OU'):
raise ValueError("Certificate missing required OU claim")
if (cert_info.fingerprint not in self.cert_fingerprints and
cert_info.fingerprint not in self.trusted_cert_fingerprints):
raise ValueError("Untrusted certificate fingerprint")
if cert_info.not_after and cert_info.not_after < datetime.now():
raise ValueError("Certificate has expired")
def check_permission(self, user: str, resource: str, action: str) -> bool:
"""Check if user has permission to perform action on resource.
Args:
user: User identifier
resource: Resource being accessed
action: Action being performed
Returns:
bool: True if permission granted, False otherwise
"""
if user not in self.user_roles:
return False
role = self.user_roles[user]
if role not in self.roles:
return False
# Check boundary restrictions
if role in self.role_boundaries:
boundary = self.role_boundaries[role]
if boundary == RoleBoundary.RESTRICTED and not self._is_privileged_user(user):
return False
if boundary == RoleBoundary.INTERNAL and not self._is_internal_user(user):
return False
permission = self.roles[role]
return (permission.resource == resource and
action in permission.actions)
DOMAIN_BOUNDARIES = {
RoleBoundary.INTERNAL: ['example.com', 'internal.org'],
RoleBoundary.RESTRICTED: ['admin.example.com']
}
self.trusted_cert_fingerprints: Set[str] = set()
# Initialize AES-256 encryption for secrets
# Derive a key from the provided encryption key using PBKDF2
salt = os.urandom(16)
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32, # 32 bytes = 256 bits for AES-256
salt=salt,
iterations=100000,
)
aes_key = kdf.derive(encryption_key)
self.aes_key = aes_key
self.salt = salt
# Keep Fernet for backward compatibility
self.cipher = Fernet(encryption_key)
# HMAC key for audit log integrity
self.hmac_key = os.urandom(32)
# Cache for certificate revocation status
self.revocation_cache: Dict[str, Tuple[bool, datetime]] = {}
self.revocation_cache_ttl = timedelta(minutes=15) # Cache TTL
# Initialize audit log sequence number
self.audit_sequence = 0
self.last_audit_hash = None
def assign_role(self, user: str, role: Role, domain: Optional[str] = None) -> bool:
"""
Assign a role to a user with boundary and inheritance validation.
Args:
user: The user identifier
role: The role to assign
domain: Optional domain for boundary validation
Returns:
bool: True if assignment succeeded, False if validation failed
"""
# Validate role assignment boundaries
if not self._validate_role_boundary(user, role, domain):
logger.warning(f"Role assignment failed: {role.value} cannot be assigned to {user} (domain boundary violation)")
self._audit_access_attempt(
"system", "role_assignment", f"assign_{role.value}",
False, f"Domain boundary violation for {user}"
)
return False
# Check for circular inheritance if this role has a parent
try:
if role in ROLE_INHERITANCE and ROLE_INHERITANCE[role] is not None:
validate_circular_inheritance(role, ROLE_INHERITANCE[role])
except ValueError as e:
logger.warning(f"Role assignment failed: {e}")
self._audit_access_attempt(
"system", "role_assignment", f"assign_{role.value}",
False, str(e)
)
return False
# Assign the role
self.user_roles[user] = role
logger.info(f"Assigned {role.value} role to {user}")
self._audit_access_attempt(
"system", "role_assignment", f"assign_{role.value}",
True, f"Role {role.value} assigned to {user}"
)
return True
def _validate_role_boundary(self, user: str, role: Role, domain: Optional[str] = None) -> bool:
"""
Validate that a role assignment respects boundary restrictions.
Args:
user: The user identifier
role: The role to assign
domain: Optional domain for validation
Returns:
bool: True if assignment is allowed, False otherwise
"""
boundary = self.role_boundaries.get(role)
if not boundary:
logger.error(f"No boundary defined for role {role.value}")
return False
# Global roles can be assigned to anyone
if boundary == RoleBoundary.GLOBAL:
return True
# For other boundaries, we need domain information
if not domain:
# Try to extract domain from user identifier if it looks like an email
if '@' in user:
domain = user.split('@', 1)[1]
else:
logger.warning(f"Cannot validate role boundary: no domain provided for {user}")
return False
# Check domain against restrictions
allowed_domains = self.domain_restrictions.get(boundary, [])
for allowed_domain in allowed_domains:
if domain.endswith(allowed_domain):
return True
logger.warning(f"Domain {domain} not allowed for boundary {boundary.value}")
return False
def add_trusted_certificate(self, cert_pem: bytes) -> str:
"""
Add a trusted certificate for pinning.
Args:
cert_pem: PEM-encoded certificate
Returns:
str: The fingerprint of the added certificate
"""
cert = load_pem_x509_certificate(cert_pem)
fingerprint = cert.fingerprint(hashes.SHA256()).hex()
self.trusted_cert_fingerprints.add(fingerprint)
self.cert_fingerprints[fingerprint] = "trusted"
logger.info(f"Added trusted certificate: {fingerprint}")
return fingerprint
def _check_certificate_revocation(self, cert_info: ClientCertInfo) -> bool:
"""
Check certificate revocation status via OCSP or CRL.
SYM-SEC-004 Requirement.
Args:
cert_info: Certificate information
Returns:
bool: True if revoked, False otherwise
"""
if not cert_info.raw_cert:
logger.warning("Cannot check revocation: No raw certificate provided")
return True # Fail closed - treat as revoked if we can't check
# Check cache first
cache_key = f"{cert_info.issuer.get('CN', '')}-{cert_info.serial_number}"
if cache_key in self.revocation_cache:
is_revoked, timestamp = self.revocation_cache[cache_key]
if datetime.now() - timestamp < self.revocation_cache_ttl:
logger.debug(f"Using cached revocation status for {cache_key}: {'Revoked' if is_revoked else 'Valid'}")
return is_revoked
try:
# In a real implementation, this would check OCSP and CRL
# For this implementation, we'll simulate the check
logger.info(f"Checking revocation status for certificate: {cert_info.subject.get('CN', 'unknown')}")
# Simulate OCSP check (in production, this would make an actual OCSP request)
# For demonstration, we'll assume the certificate is not revoked
is_revoked = False
# Cache the result
self.revocation_cache[cache_key] = (is_revoked, datetime.now())
return is_revoked
except Exception as e:
logger.error(f"Error checking certificate revocation: {str(e)}")
# Fail closed - if we can't check revocation status, assume revoked
return True
def _get_role_from_ou(self, ou: Optional[str]) -> Optional[Role]:
"""
Maps a signed OU claim string to an RBAC Role enum.
Enforces SYM-SEC-004 Requirement (signed claims only).
Args:
ou: The OU field from the certificate, expected format "role:signature"
Returns:
Optional[Role]: The mapped role or None if invalid or not a signed claim
"""
if not ou:
logger.debug("OU field is empty, cannot map role.")
return None
# Check if the OU contains a signed claim
# Format: role:signature where signature is a base64-encoded HMAC
if ':' in ou:
role_name, signature = ou.split(':', 1)
try:
# Verify the signature
expected_signature = hmac.new(
self.hmac_key,
role_name.encode(),
hashlib.sha256
).digest()
expected_signature_b64 = base64.b64encode(expected_signature).decode()
if signature != expected_signature_b64:
logger.warning(f"Invalid signature for OU role claim: {ou}")
return None
# else: Signature is valid
# Map role name to Role enum
return Role(role_name.lower())
except ValueError:
# Handles case where role_name is not a valid Role enum member
logger.warning(f"Could not map signed OU role name '{role_name}' to a valid RBAC Role.")
return None
except Exception as e:
# Catch potential errors during HMAC/base64 processing
logger.error(f"Error processing signed OU claim '{ou}': {e}")
return None
else:
# OU does not contain ':', so it's not a valid signed claim format
logger.warning(f"OU field '{ou}' is not in the expected 'role:signature' format.")
return None
def create_signed_ou_claim(self, role: Role) -> str:
"""
Create a signed OU claim for a role.
Args:
role: The role to create a claim for
Returns:
str: A signed OU claim in the format role:signature
"""
role_name = role.value
signature = hmac.new(
self.hmac_key,
role_name.encode(),
hashlib.sha256
).digest()
signature_b64 = base64.b64encode(signature).decode()
return f"{role_name}:{signature_b64}"
def _verify_certificate_pinning(self, cert_info: ClientCertInfo) -> bool:
"""
Verify that a certificate matches one of our pinned certificates.
Args:
cert_info: Certificate information
Returns:
bool: True if certificate is trusted, False otherwise
"""
if not cert_info.fingerprint:
logger.warning("Cannot verify certificate pinning: No fingerprint provided")
return False
is_trusted = cert_info.fingerprint in self.trusted_cert_fingerprints
if not is_trusted:
logger.warning(f"Certificate pinning failed: {cert_info.fingerprint} not in trusted list")
else:
logger.debug(f"Certificate pinning verified: {cert_info.fingerprint}")
return is_trusted
def _resolve_permissions(self, role: Role) -> Dict[str, Set[str]]:
"""Resolve all permissions for a role including inherited permissions"""
permissions = {}
visited = set()
def _resolve(role: Role):
if role in visited:
raise ValueError(f"Circular role inheritance detected involving {role.value}")
visited.add(role)
perm = self.roles.get(role)
if perm:
if perm.resource not in permissions:
permissions[perm.resource] = set()
permissions[perm.resource].update(perm.actions)
# Handle multiple inheritance
parents = ROLE_INHERITANCE.get(role)
if parents is None:
return
if isinstance(parents, set):
for parent in parents:
# Validate boundary restrictions
self.validate_boundary(role, parent)
_resolve(parent)
else:
# Single parent case (backward compatibility)
self.validate_boundary(role, parents)
_resolve(parents)
_resolve(role)
return permissions
def validate_permission(self, resource: str, action: str, *,
user: Optional[str] = None,
client_cert_info: Optional[ClientCertInfo] = None) -> bool:
"""
Validate if a user or certificate has permission to perform an action on a resource.
Checks both direct and inherited permissions.
Args:
resource: The resource being accessed
action: The action being performed
user: Optional username for username-based authentication
client_cert_info: Optional certificate info for cert-based authentication
Returns:
bool: True if access is allowed, False otherwise
"""
audit_user = user # User identifier for auditing
role = None # Initialize role
# --- Certificate-based Authentication (SYM-SEC-004) ---
if client_cert_info:
audit_user = client_cert_info.subject.get('CN', 'CertUnknownCN')
logger.info(f"Attempting validation via client certificate: CN={audit_user}")
# 0. Certificate Pinning Check
if not self._verify_certificate_pinning(client_cert_info):
logger.warning(f"Access denied for {audit_user}: Certificate not trusted (pinning failed).")
self._audit_access_attempt(audit_user, resource, action, False,
"Certificate pinning failed", cert_info=client_cert_info)
return False
# 1. Revocation Check (SYM-SEC-004 Requirement)
if self._check_certificate_revocation(client_cert_info):
logger.warning(f"Access denied for {audit_user}: Certificate revoked.")
self._audit_access_attempt(audit_user, resource, action, False,
"Certificate revoked", cert_info=client_cert_info)
return False
# 2. Map OU to Role via Signed Claim (SYM-SEC-004 Requirement)
ou = client_cert_info.subject.get('OU')
role = self._get_role_from_ou(ou) # Use the modified function
if not role:
# _get_role_from_ou now handles logging for invalid/missing/unsigned OU
logger.warning(f"Access denied for {audit_user}: Could not determine role from OU '{ou}' (must be a valid signed claim).")
self._audit_access_attempt(audit_user, resource, action, False,
f"Invalid/Missing/Unsigned OU: {ou}", cert_info=client_cert_info)
return False
# Role successfully determined from signed claim
logger.info(f"Mapped certificate OU signed claim '{ou}' to role '{role.value}' for CN={audit_user}")
# --- Username-based Authentication (Fallback) ---
elif user:
audit_user = user
logger.info(f"Attempting validation via username: {user}")
role = self.user_roles.get(user)
if not role:
logger.warning(f"Unauthorized access attempt by user {user}")
self._audit_access_attempt(audit_user, resource, action, False, "No role assigned")
return False
else:
# No authentication context provided
logger.error("Validation failed: Neither username nor client certificate provided.")
self._audit_access_attempt("N/A", resource, action, False, "No authentication context")
return False
# --- Permission Check ---
if not role:
logger.debug(f"No role assigned for {audit_user}")
self._audit_access_attempt(audit_user, resource, action, False, "No role assigned", cert_info=client_cert_info)
return False
# Get all permissions including inherited ones
all_perms = self._resolve_permissions(role)
# Check if resource exists in any permission set
if resource not in all_perms:
logger.debug(f"Resource mismatch for {audit_user} (Role: {role.value})")
self._audit_access_attempt(audit_user, resource, action, False, "Resource mismatch", cert_info=client_cert_info)
return False
# Check if action is permitted (either directly or via wildcard)
if action not in all_perms[resource] and '*' not in all_perms[resource]:
logger.warning(f"Action denied for {audit_user} (Role: {role.value}): {action} on {resource}")
self._audit_access_attempt(audit_user, resource, action, False, "Action not permitted", cert_info=client_cert_info)
return False
# --- Success ---
logger.info(f"Access granted for {audit_user} (Role: {role.value if role else 'None'}) to {action} on {resource}") # Added role check
self._audit_access_attempt(audit_user, resource, action, True, "Access granted", cert_info=client_cert_info)
return True
def _trigger_pre_validation_hook(self, user: str, resource: str, action: str) -> Optional[bool]:
"""SYMPHONY-INTEGRATION: External validation hook"""
# Default implementation returns None to continue normal flow
return None
def _audit_access_attempt(self, user: str, resource: str, action: str,
allowed: bool, reason: str,
cert_info: Optional[ClientCertInfo] = None) -> str:
"""
Record an audit log entry with integrity protection.
Args:
user: The user identifier
resource: The resource being accessed
action: The action being performed
allowed: Whether access was allowed
reason: The reason for the decision
cert_info: Optional certificate information
Returns:
str: The integrity hash of the audit entry
"""
# Increment sequence number
self.audit_sequence += 1
# Create audit entry
audit_entry = {
"sequence": self.audit_sequence,
"timestamp": datetime.now().isoformat(),
"user": user, # This is now CN if cert is used, or username otherwise
"resource": resource,
"action": action,
"allowed": allowed,
"reason": reason,
"auth_method": "certificate" if cert_info else "username",
"previous_hash": self.last_audit_hash
}
if cert_info:
audit_entry["cert_subject"] = cert_info.subject
if hasattr(cert_info, 'issuer') and cert_info.issuer:
audit_entry["cert_issuer"] = cert_info.issuer
if hasattr(cert_info, 'serial_number') and cert_info.serial_number:
audit_entry["cert_serial"] = str(cert_info.serial_number)
# Calculate integrity hash (includes previous hash for chain of custody)
audit_json = json.dumps(audit_entry, sort_keys=True)
integrity_hash = hmac.new(
self.hmac_key,
audit_json.encode(),
hashlib.sha256
).hexdigest()
# Add integrity hash to the entry
audit_entry["integrity_hash"] = integrity_hash
# Update last hash for chain of custody
self.last_audit_hash = integrity_hash
# Log the audit entry
logger.info(f"Audit: {audit_entry}")
# In a production system, you would also:
# 1. Write to a secure audit log storage
# 2. Potentially send to a SIEM system
# 3. Implement log rotation and archiving
return integrity_hash
def encrypt_payload(self, payload: dict) -> bytes:
"""
Encrypt a payload using AES-256-GCM.
Args:
payload: The data to encrypt
Returns:
bytes: The encrypted data
"""
# Convert payload to JSON
payload_json = json.dumps(payload).encode()
# Generate a random nonce
nonce = os.urandom(12) # 96 bits as recommended for GCM
# Create AESGCM cipher
aesgcm = AESGCM(self.aes_key)
# Encrypt the payload
ciphertext = aesgcm.encrypt(nonce, payload_json, None)
# Combine nonce and ciphertext for storage/transmission
result = nonce + ciphertext
# For backward compatibility, also support Fernet
# Note: This part might need review if strict AES-GCM is required
if hasattr(self, 'cipher') and self.cipher:
# If Fernet exists, maybe prefer it or log a warning?
# For now, let's assume AES-GCM is preferred if available
pass # Keep result as AES-GCM
return result # Return AES-GCM result
def decrypt_payload(self, encrypted_payload):
"""
Decrypt an encrypted payload, trying AES-GCM first, then Fernet.
Args:
encrypted_payload: The encrypted data (bytes or dict for testing bypass)
Returns:
dict: The decrypted payload
"""
# Bypass for testing if already a dict
if isinstance(encrypted_payload, dict):
return encrypted_payload
try:
# Assume AES-GCM format: nonce (12 bytes) + ciphertext
if len(encrypted_payload) > 12:
nonce = encrypted_payload[:12]
ciphertext = encrypted_payload[12:]
# Create AESGCM cipher
aesgcm = AESGCM(self.aes_key)
# Decrypt the payload
decrypted_json = aesgcm.decrypt(nonce, ciphertext, None)
return json.loads(decrypted_json)
else:
raise ValueError("Encrypted payload too short for AES-GCM format")
except Exception as aes_err:
logger.debug(f"AES-GCM decryption failed: {aes_err}. Trying Fernet fallback.")
# Fallback to Fernet for backward compatibility
if hasattr(self, 'cipher') and self.cipher:
try:
decrypted_json = self.cipher.decrypt(encrypted_payload)
return json.loads(decrypted_json)
except Exception as fernet_err:
logger.error(f"Fernet decryption also failed: {fernet_err}")
raise ValueError("Failed to decrypt payload with both AES-GCM and Fernet") from fernet_err
else:
logger.error("AES-GCM decryption failed and Fernet cipher is not available.")
raise ValueError("Failed to decrypt payload with AES-GCM, no fallback available") from aes_err
def check_access(self, resource: str, action: str, *,
user: Optional[str] = None,
client_cert_info: Optional[ClientCertInfo] = None) -> Tuple[bool, str]:
"""
Check access with comprehensive security controls and audit logging.
Specifically implements memory audit functionality requirements.
Args:
resource: The resource being accessed
action: The action being performed
user: Optional username for username-based authentication
client_cert_info: Optional certificate info for cert-based authentication
Returns:
Tuple[bool, str]: (access_allowed, reason)
"""
# Pre-validation hook for extensibility
pre_check = self._trigger_pre_validation_hook(
user or client_cert_info.subject.get('CN', 'CertUnknownCN'),
resource,
action
)
if pre_check is not None:
return (pre_check, "Pre-validation hook decision")
# Enforce TLS 1.3 requirement for certificate auth
if client_cert_info and client_cert_info.raw_cert:
cert = client_cert_info.raw_cert
if cert.not_valid_after < datetime.now():
return (False, "Certificate expired")
if cert.not_valid_before > datetime.now():
return (False, "Certificate not yet valid")
# Core permission validation
access_allowed = self.validate_permission(
resource, action,
user=user,
client_cert_info=client_cert_info
)
# Special handling for memory audit functionality
if resource == "memory" and action == "audit":
audit_reason = "Memory audit access"
if not access_allowed:
audit_reason = "Denied memory audit access"
# Enhanced audit logging for memory operations
self._audit_access_attempt(
user or client_cert_info.subject.get('CN', 'CertUnknownCN'),
resource,
action,
access_allowed,
audit_reason,
cert_info=client_cert_info
)
return (access_allowed, "Access granted" if access_allowed else "Access denied")
def verify_audit_log_integrity(self, audit_entries: List[Dict]) -> bool:
"""
Verify the integrity of a sequence of audit log entries.
Args:
audit_entries: A list of audit log dictionaries
Returns:
bool: True if the log integrity is verified, False otherwise
"""
expected_previous_hash = None
for i, entry in enumerate(audit_entries):
# Check sequence number
if entry.get("sequence") != i + 1:
logger.error(f"Audit log integrity failed: Sequence mismatch at entry {i+1}. Expected {i+1}, got {entry.get('sequence')}")
return False
# Check hash chain
if entry.get("previous_hash") != expected_previous_hash:
logger.error(f"Audit log integrity failed: Hash chain broken at entry {i+1}. Expected previous hash {expected_previous_hash}, got {entry.get('previous_hash')}")
return False
# Verify entry hash
entry_copy = entry.copy()
current_hash = entry_copy.pop("integrity_hash", None)
if not current_hash:
logger.error(f"Audit log integrity failed: Missing integrity hash at entry {i+1}.")
return False
entry_json = json.dumps(entry_copy, sort_keys=True)
calculated_hash = hmac.new(
self.hmac_key,
entry_json.encode(),
hashlib.sha256
).hexdigest()
if current_hash != calculated_hash:
logger.error(f"Audit log integrity failed: Hash mismatch at entry {i+1}. Calculated {calculated_hash}, got {current_hash}")
return False
# Update expected hash for next iteration
expected_previous_hash = current_hash
logger.info(f"Audit log integrity verified for {len(audit_entries)} entries.")
return True