Update audit logging and dashboard

This commit is contained in:
2025-05-13 21:54:40 +02:00
parent 60026a485d
commit 3055f5277b
20 changed files with 788 additions and 285 deletions

View File

@ -4,46 +4,96 @@
import logging
from collections.abc import Sequence
from typing import Any, cast
from fastapi import APIRouter, Depends, Request, Query
from pydantic import TypeAdapter
from sqlalchemy import select, func
from sqlalchemy.orm import Session
from pydantic import BaseModel, Field, TypeAdapter
from sqlalchemy import select, func, and_
from sqlalchemy.orm import InstrumentedAttribute, Session
from sqlalchemy.sql.expression import ColumnExpressionArgument
from typing import Annotated
from sshecret_backend.models import AuditLog
from sshecret_backend.models import AuditLog, Operation, SubSystem
from sshecret_backend.types import DBSessionDep
from sshecret_backend.view_models import AuditInfo, AuditView
from sshecret_backend.view_models import AuditInfo, AuditView, AuditListResult
LOG = logging.getLogger(__name__)
class AuditFilter(BaseModel):
"""Audit filter."""
offset: int = Field(0, ge=0)
limit: int = Field(100, le=100)
subsystem: SubSystem | None = None
operation: Operation | None = None
client_id: str | None = None
client_name: str | None = None
secret_id: str | None = None
secret_name: str | None = None
origin: str | None = None
@property
def filter_mapping(self) -> list[ColumnExpressionArgument[bool]]:
"""Construct filter mapping."""
fields = self.model_dump(
exclude_none=True, exclude_unset=True, exclude_defaults=True
)
fieldmap: dict[str, InstrumentedAttribute[Any]] = {
"subsystem": AuditLog.subsystem,
"operation": AuditLog.operation,
"client_id": AuditLog.client_id,
"client_name": AuditLog.client_name,
"secret_id": AuditLog.secret_id,
"secret_name": AuditLog.secret_name,
"origin": AuditLog.origin,
}
return [
column == value
for key, value in fields.items()
if (column := fieldmap.get(key)) is not None
]
def get_audit_api(get_db_session: DBSessionDep) -> APIRouter:
"""Construct audit sub-api."""
router = APIRouter()
@router.get("/audit/", response_model=list[AuditView])
@router.get("/audit/", response_model=AuditListResult)
async def get_audit_logs(
request: Request,
session: Annotated[Session, Depends(get_db_session)],
offset: Annotated[int, Query()] = 0,
limit: Annotated[int, Query(le=100)] = 100,
filter_client: Annotated[str | None, Query()] = None,
filter_subsystem: Annotated[str | None, Query()] = None,
) -> Sequence[AuditView]:
filters: Annotated[AuditFilter, Depends()],
) -> AuditListResult:
"""Get audit logs."""
#audit.audit_access_audit_log(session, request)
statement = select(AuditLog).offset(offset).limit(limit).order_by(AuditLog.timestamp.desc())
if filter_client:
statement = statement.where(AuditLog.client_name == filter_client)
# audit.audit_access_audit_log(session, request)
if filter_subsystem:
statement = statement.where(AuditLog.subsystem == filter_subsystem)
total = session.scalars(
select(func.count("*"))
.select_from(AuditLog)
.where(and_(True, *filters.filter_mapping))
).one()
remaining = total - filters.offset
statement = (
select(AuditLog)
.offset(filters.offset)
.limit(filters.limit)
.order_by(AuditLog.timestamp.desc())
.where(and_(True, *filters.filter_mapping))
)
LogAdapt = TypeAdapter(list[AuditView])
results = session.scalars(statement).all()
return LogAdapt.validate_python(results, from_attributes=True)
entries = LogAdapt.validate_python(results, from_attributes=True)
return AuditListResult(
results=entries,
total=total,
remaining=remaining,
)
@router.post("/audit/")
async def add_audit_log(
@ -58,10 +108,13 @@ def get_audit_api(get_db_session: DBSessionDep) -> APIRouter:
return AuditView.model_validate(audit_log, from_attributes=True)
@router.get("/audit/info")
async def get_audit_info(request: Request, session: Annotated[Session, Depends(get_db_session)]) -> AuditInfo:
async def get_audit_info(
request: Request, session: Annotated[Session, Depends(get_db_session)]
) -> AuditInfo:
"""Get audit info."""
audit_count = session.scalars(select(func.count('*')).select_from(AuditLog)).one()
audit_count = session.scalars(
select(func.count("*")).select_from(AuditLog)
).one()
return AuditInfo(entries=audit_count)
return router

View File

@ -172,7 +172,7 @@ def get_secrets_api(get_db_session: DBSessionDep) -> APIRouter:
client_secret_map[client_secret.name] = []
continue
client_secret_map[client_secret.name].append(client_secret.client.name)
audit.audit_client_secret_list(session, request)
#audit.audit_client_secret_list(session, request)
return [
ClientSecretList(name=secret_name, clients=clients)
for secret_name, clients in client_secret_map.items()
@ -191,7 +191,7 @@ def get_secrets_api(get_db_session: DBSessionDep) -> APIRouter:
if not client_secret.client:
continue
client_secrets[client_secret.name].clients.append(ClientReference(id=str(client_secret.client.id), name=client_secret.client.name))
audit.audit_client_secret_list(session, request)
#`audit.audit_client_secret_list(session, request)
return list(client_secrets.values())