Add complete project structure and all files
This commit is contained in:
267
backend/app/rbac/audit.py
Normal file
267
backend/app/rbac/audit.py
Normal file
@@ -0,0 +1,267 @@
|
||||
"""
|
||||
RBAC Audit Service - Logs all permission and role changes
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional, Dict, Any, List
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import desc
|
||||
from loguru import logger
|
||||
|
||||
from .permissions import PERMISSIONS, resolve_permission
|
||||
|
||||
|
||||
def utcnow() -> datetime:
|
||||
"""Get current UTC time"""
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
class RBACAuditService:
|
||||
"""
|
||||
Service for auditing RBAC changes.
|
||||
Logs role creation, permission changes, role assignments, etc.
|
||||
"""
|
||||
|
||||
AUDIT_ACTIONS = {
|
||||
"role_created": "Role Created",
|
||||
"role_updated": "Role Updated",
|
||||
"role_deleted": "Role Deleted",
|
||||
"role_assigned": "Role Assigned to User",
|
||||
"role_removed": "Role Removed from User",
|
||||
"permission_added": "Permission Added",
|
||||
"permission_removed": "Permission Removed",
|
||||
"permission_denied": "Permission Denied",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def log_role_created(
|
||||
db: Session,
|
||||
role_id: int,
|
||||
role_name: str,
|
||||
permissions: Dict[str, bool],
|
||||
created_by_id: int,
|
||||
created_by_username: str,
|
||||
ip_address: Optional[str] = None
|
||||
) -> None:
|
||||
"""Log role creation"""
|
||||
from ..models import AuditLog
|
||||
|
||||
log_entry = AuditLog(
|
||||
level="info",
|
||||
category="rbac",
|
||||
action="role_created",
|
||||
message=f"Role '{role_name}' created by {created_by_username}",
|
||||
user_id=created_by_id,
|
||||
username=created_by_username,
|
||||
ip_address=ip_address,
|
||||
resource_type="role",
|
||||
resource_id=str(role_id),
|
||||
resource_name=role_name,
|
||||
details={
|
||||
"permissions": permissions,
|
||||
"permissions_count": sum(1 for v in permissions.values() if v)
|
||||
},
|
||||
success=True
|
||||
)
|
||||
db.add(log_entry)
|
||||
db.commit()
|
||||
|
||||
logger.info(f"RBAC Audit: Role '{role_name}' created by {created_by_username}")
|
||||
|
||||
@staticmethod
|
||||
def log_role_updated(
|
||||
db: Session,
|
||||
role_id: int,
|
||||
role_name: str,
|
||||
old_permissions: Dict[str, bool],
|
||||
new_permissions: Dict[str, bool],
|
||||
updated_by_id: int,
|
||||
updated_by_username: str,
|
||||
ip_address: Optional[str] = None
|
||||
) -> None:
|
||||
"""Log role update with permission diff"""
|
||||
from ..models import AuditLog
|
||||
|
||||
# Calculate diff
|
||||
added = []
|
||||
removed = []
|
||||
|
||||
for perm, enabled in new_permissions.items():
|
||||
old_value = old_permissions.get(perm, False)
|
||||
if enabled and not old_value:
|
||||
added.append(perm)
|
||||
elif not enabled and old_value:
|
||||
removed.append(perm)
|
||||
|
||||
log_entry = AuditLog(
|
||||
level="info",
|
||||
category="rbac",
|
||||
action="role_updated",
|
||||
message=f"Role '{role_name}' updated by {updated_by_username}: +{len(added)} -{len(removed)} permissions",
|
||||
user_id=updated_by_id,
|
||||
username=updated_by_username,
|
||||
ip_address=ip_address,
|
||||
resource_type="role",
|
||||
resource_id=str(role_id),
|
||||
resource_name=role_name,
|
||||
details={
|
||||
"permissions_added": added,
|
||||
"permissions_removed": removed,
|
||||
"old_permissions_count": sum(1 for v in old_permissions.values() if v),
|
||||
"new_permissions_count": sum(1 for v in new_permissions.values() if v)
|
||||
},
|
||||
success=True
|
||||
)
|
||||
db.add(log_entry)
|
||||
db.commit()
|
||||
|
||||
logger.info(f"RBAC Audit: Role '{role_name}' updated: +{len(added)} -{len(removed)}")
|
||||
|
||||
@staticmethod
|
||||
def log_role_deleted(
|
||||
db: Session,
|
||||
role_id: int,
|
||||
role_name: str,
|
||||
deleted_by_id: int,
|
||||
deleted_by_username: str,
|
||||
affected_users: int = 0,
|
||||
ip_address: Optional[str] = None
|
||||
) -> None:
|
||||
"""Log role deletion"""
|
||||
from ..models import AuditLog
|
||||
|
||||
log_entry = AuditLog(
|
||||
level="warning",
|
||||
category="rbac",
|
||||
action="role_deleted",
|
||||
message=f"Role '{role_name}' deleted by {deleted_by_username}, {affected_users} users affected",
|
||||
user_id=deleted_by_id,
|
||||
username=deleted_by_username,
|
||||
ip_address=ip_address,
|
||||
resource_type="role",
|
||||
resource_id=str(role_id),
|
||||
resource_name=role_name,
|
||||
details={
|
||||
"affected_users": affected_users
|
||||
},
|
||||
success=True
|
||||
)
|
||||
db.add(log_entry)
|
||||
db.commit()
|
||||
|
||||
logger.warning(f"RBAC Audit: Role '{role_name}' deleted, {affected_users} users affected")
|
||||
|
||||
@staticmethod
|
||||
def log_role_assigned(
|
||||
db: Session,
|
||||
user_id: int,
|
||||
username: str,
|
||||
role_id: int,
|
||||
role_name: str,
|
||||
old_role_name: Optional[str],
|
||||
assigned_by_id: int,
|
||||
assigned_by_username: str,
|
||||
ip_address: Optional[str] = None
|
||||
) -> None:
|
||||
"""Log role assignment to user"""
|
||||
from ..models import AuditLog
|
||||
|
||||
msg = f"Role '{role_name}' assigned to user '{username}'"
|
||||
if old_role_name:
|
||||
msg += f" (was: {old_role_name})"
|
||||
msg += f" by {assigned_by_username}"
|
||||
|
||||
log_entry = AuditLog(
|
||||
level="info",
|
||||
category="rbac",
|
||||
action="role_assigned",
|
||||
message=msg,
|
||||
user_id=assigned_by_id,
|
||||
username=assigned_by_username,
|
||||
ip_address=ip_address,
|
||||
resource_type="user",
|
||||
resource_id=str(user_id),
|
||||
resource_name=username,
|
||||
details={
|
||||
"new_role_id": role_id,
|
||||
"new_role_name": role_name,
|
||||
"old_role_name": old_role_name,
|
||||
"target_user_id": user_id,
|
||||
"target_username": username
|
||||
},
|
||||
success=True
|
||||
)
|
||||
db.add(log_entry)
|
||||
db.commit()
|
||||
|
||||
logger.info(f"RBAC Audit: {msg}")
|
||||
|
||||
@staticmethod
|
||||
def log_permission_denied(
|
||||
db: Session,
|
||||
user_id: int,
|
||||
username: str,
|
||||
permission: str,
|
||||
resource_type: Optional[str] = None,
|
||||
resource_id: Optional[str] = None,
|
||||
ip_address: Optional[str] = None,
|
||||
request_path: Optional[str] = None
|
||||
) -> None:
|
||||
"""Log denied permission attempt"""
|
||||
from ..models import AuditLog
|
||||
|
||||
log_entry = AuditLog(
|
||||
level="warning",
|
||||
category="rbac",
|
||||
action="permission_denied",
|
||||
message=f"Permission '{permission}' denied for user '{username}'",
|
||||
user_id=user_id,
|
||||
username=username,
|
||||
ip_address=ip_address,
|
||||
resource_type=resource_type,
|
||||
resource_id=resource_id,
|
||||
request_path=request_path,
|
||||
details={
|
||||
"permission": permission,
|
||||
"resolved_permission": resolve_permission(permission)
|
||||
},
|
||||
success=False
|
||||
)
|
||||
db.add(log_entry)
|
||||
db.commit()
|
||||
|
||||
logger.warning(f"RBAC Audit: Permission '{permission}' denied for '{username}'")
|
||||
|
||||
@staticmethod
|
||||
def get_rbac_logs(
|
||||
db: Session,
|
||||
limit: int = 100,
|
||||
offset: int = 0,
|
||||
user_id: Optional[int] = None,
|
||||
action: Optional[str] = None
|
||||
) -> List:
|
||||
"""Get RBAC audit logs"""
|
||||
from ..models import AuditLog
|
||||
|
||||
query = db.query(AuditLog).filter(AuditLog.category == "rbac")
|
||||
|
||||
if user_id:
|
||||
query = query.filter(AuditLog.user_id == user_id)
|
||||
if action:
|
||||
query = query.filter(AuditLog.action == action)
|
||||
|
||||
return query.order_by(desc(AuditLog.created_at)).offset(offset).limit(limit).all()
|
||||
|
||||
@staticmethod
|
||||
def get_user_permission_history(
|
||||
db: Session,
|
||||
user_id: int,
|
||||
limit: int = 50
|
||||
) -> List:
|
||||
"""Get permission change history for a user"""
|
||||
from ..models import AuditLog
|
||||
|
||||
return db.query(AuditLog).filter(
|
||||
AuditLog.category == "rbac",
|
||||
AuditLog.details.contains({"target_user_id": user_id})
|
||||
).order_by(desc(AuditLog.created_at)).limit(limit).all()
|
||||
Reference in New Issue
Block a user