169 lines
6.3 KiB
Python
169 lines
6.3 KiB
Python
import logging
|
|
from datetime import datetime, timedelta
|
|
from typing import Optional, Any, Dict
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select, func, and_
|
|
from app.models.security import PendingAction, ActionStatus
|
|
from app.models.history import AuditLog, LogSeverity
|
|
from app.models.identity import User
|
|
from app.models import SystemParameter
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class SecurityService:
|
|
|
|
@staticmethod
|
|
async def get_sec_config(db: AsyncSession) -> Dict[str, Any]:
|
|
"""Lekéri a biztonsági korlátokat a központi rendszerparaméterekből."""
|
|
keys = ["SECURITY_MAX_RECORDS_PER_HOUR", "SECURITY_DUAL_CONTROL_ENABLED"]
|
|
stmt = select(SystemParameter).where(SystemParameter.key.in_(keys))
|
|
res = await db.execute(stmt)
|
|
params = {p.key: p.value for p in res.scalars().all()}
|
|
|
|
return {
|
|
"max_records": int(params.get("SECURITY_MAX_RECORDS_PER_HOUR", 500)),
|
|
"dual_control": str(params.get("SECURITY_DUAL_CONTROL_ENABLED", "true")).lower() == "true"
|
|
}
|
|
|
|
# --- 1. SZINT: AUDIT & LOGGING (A Mindenlátó Szem) ---
|
|
async def log_event(
|
|
self,
|
|
db: AsyncSession,
|
|
user_id: Optional[int],
|
|
action: str,
|
|
severity: LogSeverity,
|
|
old_data: Optional[Dict] = None,
|
|
new_data: Optional[Dict] = None,
|
|
ip: Optional[str] = None,
|
|
ua: Optional[str] = None,
|
|
target_type: Optional[str] = None,
|
|
target_id: Optional[str] = None,
|
|
reason: Optional[str] = None
|
|
):
|
|
"""Minden rendszerművelet rögzítése és azonnali biztonsági elemzése."""
|
|
new_log = AuditLog(
|
|
user_id=user_id,
|
|
severity=severity,
|
|
action=action,
|
|
target_type=target_type,
|
|
target_id=target_id,
|
|
old_data=old_data,
|
|
new_data=new_data,
|
|
ip_address=ip,
|
|
user_agent=ua
|
|
)
|
|
db.add(new_log)
|
|
|
|
# Ha a szint EMERGENCY, azonnal lőjük le a júzert
|
|
if severity == LogSeverity.emergency:
|
|
await self._execute_emergency_lock(db, user_id, f"Auto-lock triggered by: {action}")
|
|
|
|
await db.commit()
|
|
|
|
# --- 2. SZINT: PENDING ACTIONS (Négy szem elv) ---
|
|
async def request_action(
|
|
self,
|
|
db: AsyncSession,
|
|
requester_id: int,
|
|
action_type: str,
|
|
payload: Dict,
|
|
reason: str
|
|
):
|
|
"""Kritikus művelet kezdeményezése jóváhagyásra (nem hajtódik végre azonnal)."""
|
|
new_action = PendingAction(
|
|
requester_id=requester_id,
|
|
action_type=action_type,
|
|
payload=payload,
|
|
reason=reason,
|
|
status=ActionStatus.pending
|
|
)
|
|
db.add(new_action)
|
|
|
|
await self.log_event(
|
|
db, requester_id,
|
|
action=f"REQUEST_{action_type}",
|
|
severity=LogSeverity.critical,
|
|
new_data=payload,
|
|
reason=f"Approval requested: {reason}"
|
|
)
|
|
|
|
await db.commit()
|
|
return new_action
|
|
|
|
async def approve_action(self, db: AsyncSession, approver_id: int, action_id: int):
|
|
"""Művelet végrehajtása egy második admin által."""
|
|
stmt = select(PendingAction).where(PendingAction.id == action_id)
|
|
action = (await db.execute(stmt)).scalar_one_or_none()
|
|
|
|
if not action or action.status != ActionStatus.pending:
|
|
raise Exception("A művelet nem található vagy már feldolgozták.")
|
|
|
|
if action.requester_id == approver_id:
|
|
raise Exception("Önmagad kérését nem hagyhatod jóvá! (Négy szem elv)")
|
|
|
|
# ITT TÖRTÉNIK A TÉNYLEGES ÜZLETI LOGIKA (Példa: Rangmódosítás)
|
|
if action.action_type == "CHANGE_ROLE":
|
|
user_id = action.payload.get("user_id")
|
|
new_role = action.payload.get("new_role")
|
|
|
|
user_stmt = select(User).where(User.id == user_id)
|
|
user = (await db.execute(user_stmt)).scalar_one_or_none()
|
|
if user:
|
|
user.role = new_role
|
|
logger.info(f"Role for user {user_id} changed to {new_role} via approved action {action_id}")
|
|
|
|
action.status = ActionStatus.approved
|
|
action.approver_id = approver_id
|
|
action.processed_at = func.now()
|
|
|
|
await self.log_event(
|
|
db, approver_id,
|
|
action=f"APPROVE_{action.action_type}",
|
|
severity=LogSeverity.info,
|
|
target_id=str(action.id),
|
|
reason=f"Approved action requested by {action.requester_id}"
|
|
)
|
|
|
|
await db.commit()
|
|
return True
|
|
|
|
# --- 3. SZINT: DATA THROTTLING & EMERGENCY LOCK ---
|
|
async def check_data_access_limit(self, db: AsyncSession, user_id: int):
|
|
"""Figyeli a tömeges adatlekérést (Adatlopás elleni védelem)."""
|
|
config = await self.get_sec_config(db)
|
|
one_hour_ago = datetime.now() - timedelta(hours=1)
|
|
|
|
# Megszámoljuk az utolsó egy óra GET (lekérési) logjait
|
|
stmt = select(func.count(AuditLog.id)).where(
|
|
and_(
|
|
AuditLog.user_id == user_id,
|
|
AuditLog.timestamp >= one_hour_ago,
|
|
AuditLog.action.like("GET_%")
|
|
)
|
|
)
|
|
count = (await db.execute(stmt)).scalar() or 0
|
|
|
|
if count > config["max_records"]:
|
|
await self.log_event(
|
|
db, user_id,
|
|
action="MASS_DATA_ACCESS_DETECTED",
|
|
severity=LogSeverity.emergency,
|
|
reason=f"Access count: {count} (Limit: {config['max_records']})"
|
|
)
|
|
# A log_event automatikusan hívja a _execute_emergency_lock-ot
|
|
return False
|
|
return True
|
|
|
|
async def _execute_emergency_lock(self, db: AsyncSession, user_id: int, reason: str):
|
|
"""Azonnali fiókfelfüggesztés vészhelyzet esetén."""
|
|
if not user_id: return
|
|
|
|
stmt = select(User).where(User.id == user_id)
|
|
user = (await db.execute(stmt)).scalar_one_or_none()
|
|
|
|
if user:
|
|
user.is_active = False
|
|
logger.critical(f"🚨 SECURITY EMERGENCY LOCK: User {user_id} suspended. Reason: {reason}")
|
|
# Itt lehetne bekötni egy külső SMS/Slack/Email riasztást
|
|
|
|
security_service = SecurityService() |