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())

View File

@ -6,12 +6,13 @@ from pathlib import Path
from typing import Literal, cast
import click
from sshecret_backend.auth import hash_token
import uvicorn
from dotenv import load_dotenv
from sqlalchemy import select
from sqlalchemy.orm import Session
from .db import create_api_token, get_engine, hash_token
from .db import create_api_token, get_engine
from .models import (
APIClient,
AuditLog,

View File

@ -2,7 +2,7 @@
import uuid
from datetime import datetime
from typing import Annotated, Self, override
from typing import Annotated, Self, Sequence, override
from pydantic import AfterValidator, BaseModel, Field, IPvAnyAddress, IPvAnyNetwork
@ -173,6 +173,8 @@ class AuditView(BaseModel):
data: dict[str, str] | None = None
client_id: uuid.UUID | None = None
client_name: str | None = None
secret_id: uuid.UUID | None = None
secret_name: str | None = None
origin: str | None = None
timestamp: datetime | None = None
@ -181,3 +183,10 @@ class AuditInfo(BaseModel):
"""Information about audit information."""
entries: int
class AuditListResult(BaseModel):
"""Class to return when listing audit entries."""
results: Sequence[AuditView]
total: int
remaining: int