Initial curated Unraid-AppOps-Gateway source from 3bd6f6fee310125bfbb4d4650389a817f498724d
This commit is contained in:
commit
1dfbca08a1
528 files changed
+131054
No files matched your search
@@ -0,0 +1,151 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import uuid
|
||||
from datetime import timedelta
|
||||
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from ..db.models import Approval
|
||||
from ..core.time import utc_now
|
||||
|
||||
|
||||
VALID_DECISIONS = {'approved', 'rejected'}
|
||||
|
||||
|
||||
class ApprovalStateError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def approval_fingerprint(action: str, target: str, source: str, risk: str, context: dict | None = None) -> str:
|
||||
canonical_context = json.dumps(context or {}, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
|
||||
canonical = "\x1f".join([*(part.strip() for part in (action, target, source, risk)), canonical_context])
|
||||
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def get_approval(session: Session, approval_id: str) -> Approval | None:
|
||||
approval = session.get(Approval, approval_id)
|
||||
if approval and not approval.action_fingerprint:
|
||||
approval.action_fingerprint = approval_fingerprint(approval.action, approval.target, approval.source, approval.risk, approval.request_context_json)
|
||||
session.add(approval)
|
||||
session.commit()
|
||||
session.refresh(approval)
|
||||
if approval and _expire_if_needed(session, approval):
|
||||
session.refresh(approval)
|
||||
return approval
|
||||
|
||||
|
||||
def list_approvals(session: Session, *, include_dismissed: bool = False):
|
||||
statement = select(Approval).order_by(Approval.created_at.desc())
|
||||
if not include_dismissed:
|
||||
statement = statement.where(Approval.dismissed_at.is_(None))
|
||||
approvals = session.exec(statement).all()
|
||||
changed = any(_expire_if_needed(session, approval, commit=False) for approval in approvals)
|
||||
if changed:
|
||||
session.commit()
|
||||
for approval in approvals:
|
||||
session.refresh(approval)
|
||||
return approvals
|
||||
|
||||
|
||||
def dismiss_expired_approvals(session: Session) -> list[Approval]:
|
||||
"""Archive only terminal expired approvals while retaining their audit evidence."""
|
||||
approvals = list_approvals(session)
|
||||
dismissed_at = utc_now()
|
||||
dismissed = [approval for approval in approvals if approval.status == 'expired']
|
||||
for approval in dismissed:
|
||||
approval.dismissed_at = dismissed_at
|
||||
session.add(approval)
|
||||
if dismissed:
|
||||
session.commit()
|
||||
for approval in dismissed:
|
||||
session.refresh(approval)
|
||||
return dismissed
|
||||
|
||||
|
||||
def _expire_if_needed(session: Session, approval: Approval, *, commit: bool = True) -> bool:
|
||||
if approval.status != 'pending' or approval.expires_at is None or approval.expires_at > utc_now():
|
||||
return False
|
||||
approval.status = 'expired'
|
||||
approval.decided_at = utc_now()
|
||||
session.add(approval)
|
||||
if commit:
|
||||
session.commit()
|
||||
return True
|
||||
|
||||
|
||||
def create_approval(
|
||||
session: Session,
|
||||
*,
|
||||
action: str,
|
||||
target: str,
|
||||
source: str,
|
||||
risk: str,
|
||||
reason: str | None = None,
|
||||
requester: str = 'manual',
|
||||
expires_in_minutes: int = 30,
|
||||
context: dict | None = None,
|
||||
) -> Approval:
|
||||
approval = Approval(
|
||||
id=f'appr-{uuid.uuid4().hex[:12]}',
|
||||
action=action,
|
||||
target=target,
|
||||
source=source,
|
||||
risk=risk,
|
||||
status='pending',
|
||||
requester=requester,
|
||||
reason=reason,
|
||||
expires_at=(utc_now() + timedelta(minutes=expires_in_minutes)),
|
||||
request_context_json=context or {},
|
||||
action_fingerprint=approval_fingerprint(action, target, source, risk, context),
|
||||
)
|
||||
session.add(approval)
|
||||
session.commit()
|
||||
session.refresh(approval)
|
||||
return approval
|
||||
|
||||
|
||||
def update_approval_status(session: Session, approval_id: str, status: str, *, actor: str = "operator") -> Approval | None:
|
||||
approval = get_approval(session, approval_id)
|
||||
if not approval:
|
||||
return None
|
||||
|
||||
if status not in VALID_DECISIONS:
|
||||
raise ApprovalStateError('Approval status must be approved or rejected.')
|
||||
if approval.status == 'expired':
|
||||
raise ApprovalStateError('Approval has expired and can no longer be decided.')
|
||||
if approval.status != 'pending':
|
||||
raise ApprovalStateError(f'Approval is already {approval.status}.')
|
||||
|
||||
approval.status = status
|
||||
approval.decided_at = utc_now()
|
||||
approval.approved_by = actor if status == 'approved' else None
|
||||
session.add(approval)
|
||||
session.commit()
|
||||
session.refresh(approval)
|
||||
return approval
|
||||
|
||||
|
||||
def mark_approval_executed(
|
||||
session: Session,
|
||||
approval: Approval,
|
||||
*,
|
||||
job_id: str,
|
||||
audit_event_id: str,
|
||||
actor: str = "operator",
|
||||
) -> Approval:
|
||||
if approval.status != 'approved':
|
||||
raise ApprovalStateError('Only an approved request can be executed.')
|
||||
expected = approval_fingerprint(approval.action, approval.target, approval.source, approval.risk, approval.request_context_json)
|
||||
if not approval.action_fingerprint or approval.action_fingerprint != expected:
|
||||
raise ApprovalStateError('Approval action context changed after review.')
|
||||
approval.status = 'executed'
|
||||
approval.executed_at = utc_now()
|
||||
approval.job_id = job_id
|
||||
approval.audit_event_id = audit_event_id
|
||||
approval.executed_by = actor
|
||||
session.add(approval)
|
||||
session.commit()
|
||||
session.refresh(approval)
|
||||
return approval
|
||||
Reference in new issue
Block a user