Stats and error handling
This commit is contained in:
@ -18,7 +18,7 @@ from sshecret_admin.services.models import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from sshecret.backend.identifiers import ClientIdParam, FlexID, KeySpec
|
from sshecret.backend.identifiers import ClientIdParam, FlexID, KeySpec
|
||||||
from sshecret.backend.models import ClientQueryResult, ClientReference, FilterType
|
from sshecret.backend.models import ClientQueryResult, ClientReference, FilterType, SystemStats
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -210,4 +210,11 @@ def create_router(dependencies: AdminDependencies) -> APIRouter:
|
|||||||
"""Add secret to a client."""
|
"""Add secret to a client."""
|
||||||
await admin.create_client_secret(_id(id), secret_name)
|
await admin.create_client_secret(_id(id), secret_name)
|
||||||
|
|
||||||
|
@app.get("/stats")
|
||||||
|
async def get_system_stats(
|
||||||
|
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||||
|
) -> SystemStats:
|
||||||
|
"""Get system stats."""
|
||||||
|
return await admin.get_system_stats()
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|||||||
@ -25,6 +25,8 @@ from sshecret_admin.core.db import setup_database
|
|||||||
from sshecret_admin.frontend.exceptions import RedirectException
|
from sshecret_admin.frontend.exceptions import RedirectException
|
||||||
from sshecret_admin.services.secret_manager import setup_private_key
|
from sshecret_admin.services.secret_manager import setup_private_key
|
||||||
|
|
||||||
|
from sshecret.backend.exceptions import BackendError, BackendValidationError
|
||||||
|
|
||||||
from .dependencies import BaseDependencies
|
from .dependencies import BaseDependencies
|
||||||
from .settings import AdminServerSettings
|
from .settings import AdminServerSettings
|
||||||
|
|
||||||
@ -72,13 +74,13 @@ def create_admin_app(
|
|||||||
|
|
||||||
app = FastAPI(lifespan=lifespan)
|
app = FastAPI(lifespan=lifespan)
|
||||||
app.add_middleware(SessionMiddleware, secret_key=settings.secret_key)
|
app.add_middleware(SessionMiddleware, secret_key=settings.secret_key)
|
||||||
origins = [ settings.frontend_origin ]
|
origins = [settings.frontend_origin]
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=origins,
|
allow_origins=origins,
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["*"],
|
allow_methods=["*"],
|
||||||
allow_headers=["*"]
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.exception_handler(RequestValidationError)
|
@app.exception_handler(RequestValidationError)
|
||||||
@ -90,6 +92,24 @@ def create_admin_app(
|
|||||||
content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}),
|
content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@app.exception_handler(BackendValidationError)
|
||||||
|
async def validation_backend_validation_exception_handler(
|
||||||
|
request: Request, exc: BackendValidationError
|
||||||
|
):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||||
|
content=jsonable_encoder({"detail": exc.errors()}),
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.exception_handler(BackendError)
|
||||||
|
async def validation_backend_exception_handler(
|
||||||
|
request: Request, exc: BackendValidationError
|
||||||
|
):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
content=jsonable_encoder({"detail": [str(exc)]}),
|
||||||
|
)
|
||||||
|
|
||||||
@app.exception_handler(RedirectException)
|
@app.exception_handler(RedirectException)
|
||||||
async def redirect_handler(request: Request, exc: RedirectException) -> Response:
|
async def redirect_handler(request: Request, exc: RedirectException) -> Response:
|
||||||
"""Handle redirect exceptions."""
|
"""Handle redirect exceptions."""
|
||||||
|
|||||||
@ -17,8 +17,14 @@ from sshecret.backend import (
|
|||||||
Operation,
|
Operation,
|
||||||
SubSystem,
|
SubSystem,
|
||||||
)
|
)
|
||||||
|
from sshecret.backend.exceptions import BackendError, BackendValidationError
|
||||||
from sshecret.backend.identifiers import KeySpec
|
from sshecret.backend.identifiers import KeySpec
|
||||||
from sshecret.backend.models import ClientQueryResult, ClientReference, DetailedSecrets
|
from sshecret.backend.models import (
|
||||||
|
ClientQueryResult,
|
||||||
|
ClientReference,
|
||||||
|
DetailedSecrets,
|
||||||
|
SystemStats,
|
||||||
|
)
|
||||||
from sshecret.backend.api import AuditAPI
|
from sshecret.backend.api import AuditAPI
|
||||||
from sshecret.crypto import encrypt_string, load_public_key
|
from sshecret.crypto import encrypt_string, load_public_key
|
||||||
|
|
||||||
@ -209,7 +215,14 @@ class AdminBackend:
|
|||||||
return await self._create_client(name, public_key, description, sources)
|
return await self._create_client(name, public_key, description, sources)
|
||||||
except ClientManagementError:
|
except ClientManagementError:
|
||||||
raise
|
raise
|
||||||
|
except BackendValidationError as e:
|
||||||
|
LOG.error("Validation error: %s", e, exc_info=True)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
except BackendError:
|
||||||
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
LOG.error("Exception: %s", e, exc_info=True)
|
||||||
raise BackendUnavailableError() from e
|
raise BackendUnavailableError() from e
|
||||||
|
|
||||||
async def _update_client_public_key(
|
async def _update_client_public_key(
|
||||||
@ -518,9 +531,9 @@ class AdminBackend:
|
|||||||
secret: str | None = None
|
secret: str | None = None
|
||||||
async with self.secrets_manager() as password_manager:
|
async with self.secrets_manager() as password_manager:
|
||||||
secret = await password_manager.get_secret(name)
|
secret = await password_manager.get_secret(name)
|
||||||
secret_group: str | None = None
|
secret_group: GroupReference | None = None
|
||||||
if secret:
|
if secret:
|
||||||
secret_group = await password_manager.get_entry_group(name)
|
secret_group = await password_manager.get_entry_group_info(name)
|
||||||
|
|
||||||
secret_view = SecretView(name=name, secret=secret, group=secret_group)
|
secret_view = SecretView(name=name, secret=secret, group=secret_group)
|
||||||
|
|
||||||
@ -535,6 +548,10 @@ class AdminBackend:
|
|||||||
for ref in secret_mapping.clients
|
for ref in secret_mapping.clients
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if not secret_mapping and not secret_group:
|
||||||
|
# This secret is effectively deleted.
|
||||||
|
return None
|
||||||
|
|
||||||
return secret_view
|
return secret_view
|
||||||
|
|
||||||
async def delete_secret(self, name: str) -> None:
|
async def delete_secret(self, name: str) -> None:
|
||||||
@ -653,6 +670,10 @@ class AdminBackend:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise BackendUnavailableError() from e
|
raise BackendUnavailableError() from e
|
||||||
|
|
||||||
|
async def get_system_stats(self) -> SystemStats:
|
||||||
|
"""Get system stats."""
|
||||||
|
return await self.backend.get_system_stats()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def audit(self) -> AuditAPI:
|
def audit(self) -> AuditAPI:
|
||||||
"""Resolve audit API."""
|
"""Resolve audit API."""
|
||||||
|
|||||||
@ -32,12 +32,23 @@ class SecretListView(BaseModel):
|
|||||||
clients: list[str] = Field(default_factory=list) # Clients that have access to it.
|
clients: list[str] = Field(default_factory=list) # Clients that have access to it.
|
||||||
|
|
||||||
|
|
||||||
|
class GroupReference(BaseModel):
|
||||||
|
"""Reference to a group.
|
||||||
|
|
||||||
|
This will be used for references to parent groups to avoid circular
|
||||||
|
references.
|
||||||
|
"""
|
||||||
|
|
||||||
|
group_name: str
|
||||||
|
path: str
|
||||||
|
|
||||||
|
|
||||||
class SecretView(BaseModel):
|
class SecretView(BaseModel):
|
||||||
"""Model containing a secret, including its clear-text value."""
|
"""Model containing a secret, including its clear-text value."""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
secret: str | None
|
secret: str | None
|
||||||
group: str | None = None
|
group: GroupReference | None = None
|
||||||
clients: list[ClientReference] = Field(
|
clients: list[ClientReference] = Field(
|
||||||
default_factory=list
|
default_factory=list
|
||||||
) # Clients that have access to it.
|
) # Clients that have access to it.
|
||||||
@ -147,17 +158,6 @@ class SecretClientMapping(BaseModel):
|
|||||||
clients: list[ClientReference] = Field(default_factory=list)
|
clients: list[ClientReference] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
class GroupReference(BaseModel):
|
|
||||||
"""Reference to a group.
|
|
||||||
|
|
||||||
This will be used for references to parent groups to avoid circular
|
|
||||||
references.
|
|
||||||
"""
|
|
||||||
|
|
||||||
group_name: str
|
|
||||||
path: str
|
|
||||||
|
|
||||||
|
|
||||||
class ClientSecretGroup(BaseModel):
|
class ClientSecretGroup(BaseModel):
|
||||||
"""Client secrets grouped."""
|
"""Client secrets grouped."""
|
||||||
|
|
||||||
|
|||||||
@ -32,7 +32,7 @@ from sshecret_admin.auth import PasswordDB
|
|||||||
from sshecret_admin.auth.models import Group, ManagedSecret
|
from sshecret_admin.auth.models import Group, ManagedSecret
|
||||||
from sshecret_admin.core.db import DatabaseSessionManager
|
from sshecret_admin.core.db import DatabaseSessionManager
|
||||||
from sshecret_admin.core.settings import AdminServerSettings
|
from sshecret_admin.core.settings import AdminServerSettings
|
||||||
from sshecret_admin.services.models import SecretGroup
|
from sshecret_admin.services.models import GroupReference, SecretGroup
|
||||||
|
|
||||||
|
|
||||||
KEY_FILENAME = "sshecret-admin-key"
|
KEY_FILENAME = "sshecret-admin-key"
|
||||||
@ -430,6 +430,17 @@ class AsyncSecretContext:
|
|||||||
return entry.group.name
|
return entry.group.name
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
async def get_entry_group_info(self, entry_name: str) -> GroupReference | None:
|
||||||
|
"""Get group of entry, with path."""
|
||||||
|
entry = await self._get_entry(entry_name)
|
||||||
|
if not entry:
|
||||||
|
raise InvalidSecretNameError("Invalid secret name or secret not found.")
|
||||||
|
if not entry.group:
|
||||||
|
return None
|
||||||
|
group = await self._get_group_by_id(entry.group.id)
|
||||||
|
group_tree = await self._build_group_tree(group)
|
||||||
|
return GroupReference(group_name=entry.group.name, path=group_tree.path)
|
||||||
|
|
||||||
async def _get_groups(
|
async def _get_groups(
|
||||||
self, pattern: str | None = None, regex: bool = True, root_groups: bool = False
|
self, pattern: str | None = None, regex: bool = True, root_groups: bool = False
|
||||||
) -> list[Group]:
|
) -> list[Group]:
|
||||||
|
|||||||
@ -124,7 +124,7 @@ class ClientOperations:
|
|||||||
existing_id = await self.get_client_id(FlexID.name(create_model.name))
|
existing_id = await self.get_client_id(FlexID.name(create_model.name))
|
||||||
if existing_id:
|
if existing_id:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400, detail="Error: A client already exists with this name."
|
status_code=400, detail="A client already exists with this name.", headers={"X-Model-Field": "name"}
|
||||||
)
|
)
|
||||||
deleted_id = await resolve_client_id(
|
deleted_id = await resolve_client_id(
|
||||||
self.session, create_model.name, include_deleted=True
|
self.session, create_model.name, include_deleted=True
|
||||||
|
|||||||
@ -8,6 +8,8 @@ from typing import Annotated
|
|||||||
from fastapi import APIRouter, Depends, Query, Request
|
from fastapi import APIRouter, Depends, Query, Request
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from sshecret_backend.api.common import get_system_stats
|
||||||
|
from sshecret_backend.api.schemas import SystemStats
|
||||||
from sshecret_backend.types import AsyncDBSessionDep
|
from sshecret_backend.types import AsyncDBSessionDep
|
||||||
from sshecret_backend.api.clients.schemas import (
|
from sshecret_backend.api.clients.schemas import (
|
||||||
ClientCreate,
|
ClientCreate,
|
||||||
@ -149,4 +151,11 @@ def create_client_router(get_db_session: AsyncDBSessionDep) -> APIRouter:
|
|||||||
client_op = ClientOperations(session, request)
|
client_op = ClientOperations(session, request)
|
||||||
return await client_op.update_client_policies(client_id, policy_update)
|
return await client_op.update_client_policies(client_id, policy_update)
|
||||||
|
|
||||||
|
@router.get("/stats")
|
||||||
|
async def get_stats(
|
||||||
|
session: Annotated[AsyncSession, Depends(get_db_session)],
|
||||||
|
) -> SystemStats:
|
||||||
|
"""Get system stats."""
|
||||||
|
return await get_system_stats(session)
|
||||||
|
|
||||||
return router
|
return router
|
||||||
|
|||||||
@ -6,11 +6,12 @@ import uuid
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
import bcrypt
|
import bcrypt
|
||||||
from sqlalchemy import Select
|
from sqlalchemy import Select, distinct, func
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.future import select
|
from sqlalchemy.future import select
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
from sshecret_backend.models import Client, ClientAccessPolicy
|
from sshecret_backend.api.schemas import SystemStats
|
||||||
|
from sshecret_backend.models import AuditLog, Client, ClientAccessPolicy, ClientSecret
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
RE_UUID = re.compile(
|
RE_UUID = re.compile(
|
||||||
@ -137,3 +138,24 @@ async def create_new_client_version(
|
|||||||
await session.flush()
|
await session.flush()
|
||||||
await refresh_client(session, new_client)
|
await refresh_client(session, new_client)
|
||||||
return new_client
|
return new_client
|
||||||
|
|
||||||
|
|
||||||
|
async def get_system_stats(session: AsyncSession) -> SystemStats:
|
||||||
|
"""Get system stats."""
|
||||||
|
client_count_expr = (
|
||||||
|
select(func.count("*"))
|
||||||
|
.select_from(Client)
|
||||||
|
.where(Client.is_deleted.is_not(True))
|
||||||
|
.where(Client.is_active.is_not(False))
|
||||||
|
.where(Client.is_system.is_not(True))
|
||||||
|
)
|
||||||
|
client_count = (await session.scalars(client_count_expr)).one()
|
||||||
|
secret_count_expr = select(func.count(distinct(ClientSecret.name))).where(
|
||||||
|
ClientSecret.deleted.is_not(True)
|
||||||
|
)
|
||||||
|
secret_count = (await session.scalars(secret_count_expr)).one()
|
||||||
|
audit_count_expr = select(func.count("*")).select_from(AuditLog)
|
||||||
|
audit_count = (await session.scalars(audit_count_expr)).one()
|
||||||
|
return SystemStats(
|
||||||
|
clients=client_count, secrets=secret_count, audit_events=audit_count
|
||||||
|
)
|
||||||
|
|||||||
@ -7,3 +7,11 @@ class BodyValue(BaseModel):
|
|||||||
"""A generic model with just a value parameter."""
|
"""A generic model with just a value parameter."""
|
||||||
|
|
||||||
value: str
|
value: str
|
||||||
|
|
||||||
|
|
||||||
|
class SystemStats(BaseModel):
|
||||||
|
"""Generic system stats."""
|
||||||
|
|
||||||
|
clients: int
|
||||||
|
secrets: int
|
||||||
|
audit_events: int
|
||||||
|
|||||||
@ -146,6 +146,7 @@ class ClientSecretOperations:
|
|||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail="Cannot add a secret. A different secret with the same name already exists.",
|
detail="Cannot add a secret. A different secret with the same name already exists.",
|
||||||
|
headers={"X-Model-Field": "name"},
|
||||||
)
|
)
|
||||||
secret = ClientSecret(
|
secret = ClientSecret(
|
||||||
name=name,
|
name=name,
|
||||||
|
|||||||
@ -24,8 +24,9 @@ from .models import (
|
|||||||
Operation,
|
Operation,
|
||||||
Secret,
|
Secret,
|
||||||
SubSystem,
|
SubSystem,
|
||||||
|
SystemStats,
|
||||||
)
|
)
|
||||||
from .exceptions import BackendValidationError, BackendConnectionError
|
from .exceptions import BackendError, BackendValidationError, BackendConnectionError, HttpErrorItem, get_error_message, handle_exception, handle_validation_error
|
||||||
from .identifiers import KeySpec
|
from .identifiers import KeySpec
|
||||||
from .utils import validate_public_key
|
from .utils import validate_public_key
|
||||||
|
|
||||||
@ -135,9 +136,10 @@ class BaseBackend:
|
|||||||
LOG.debug("Handling response with status_code %s", response.status_code)
|
LOG.debug("Handling response with status_code %s", response.status_code)
|
||||||
if response.status_code == 422:
|
if response.status_code == 422:
|
||||||
LOG.error("Validation error from backend:\n%s", response.text)
|
LOG.error("Validation error from backend:\n%s", response.text)
|
||||||
raise BackendValidationError(response.text)
|
error = handle_validation_error(response)
|
||||||
|
raise error
|
||||||
if response.status_code != 404 and str(response.status_code).startswith("4"):
|
if response.status_code != 404 and str(response.status_code).startswith("4"):
|
||||||
raise BackendConnectionError("Error from backend: %s", response.text)
|
handle_exception(response)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@ -315,7 +317,8 @@ class SshecretBackend(BaseBackend):
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Register a new client."""
|
"""Register a new client."""
|
||||||
if not validate_public_key(public_key):
|
if not validate_public_key(public_key):
|
||||||
raise BackendValidationError("Error: Invalid public key format.")
|
error: HttpErrorItem = { "loc": [ "body", "public_key" ], "msg": "Invalid public key format", "type": "None" }
|
||||||
|
raise BackendValidationError(errors=[error])
|
||||||
data = {
|
data = {
|
||||||
"name": name,
|
"name": name,
|
||||||
"public_key": public_key,
|
"public_key": public_key,
|
||||||
@ -328,7 +331,8 @@ class SshecretBackend(BaseBackend):
|
|||||||
async def create_system_client(self, name: str, public_key: str) -> Client:
|
async def create_system_client(self, name: str, public_key: str) -> Client:
|
||||||
"""Create system client."""
|
"""Create system client."""
|
||||||
if not validate_public_key(public_key):
|
if not validate_public_key(public_key):
|
||||||
raise BackendValidationError("Error: Invalid public key format.")
|
error: HttpErrorItem = { "loc": [ "body", "public_key" ], "msg": "Invalid public key format", "type": "None" }
|
||||||
|
raise BackendValidationError(errors=[error])
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"name": name,
|
"name": name,
|
||||||
@ -387,7 +391,7 @@ class SshecretBackend(BaseBackend):
|
|||||||
raise BackendConnectionError() from e
|
raise BackendConnectionError() from e
|
||||||
if results.status_code != 200:
|
if results.status_code != 200:
|
||||||
output = results.text
|
output = results.text
|
||||||
raise BackendConnectionError(f"Error from backend:\n{output}")
|
raise BackendError(f"Error from backend:\n{output}")
|
||||||
query_results = ClientQueryResult.model_validate(results.json())
|
query_results = ClientQueryResult.model_validate(results.json())
|
||||||
return query_results
|
return query_results
|
||||||
|
|
||||||
@ -522,6 +526,12 @@ class SshecretBackend(BaseBackend):
|
|||||||
|
|
||||||
return DetailedSecrets.model_validate(response.json())
|
return DetailedSecrets.model_validate(response.json())
|
||||||
|
|
||||||
|
async def get_system_stats(self) -> SystemStats:
|
||||||
|
"""Get system stats."""
|
||||||
|
path = "/api/v1/stats"
|
||||||
|
response = await self._get(path)
|
||||||
|
return SystemStats.model_validate(response.json())
|
||||||
|
|
||||||
def audit(self, subsystem: SubSystem) -> AuditAPI:
|
def audit(self, subsystem: SubSystem) -> AuditAPI:
|
||||||
"""Create the audit API."""
|
"""Create the audit API."""
|
||||||
audit = AuditAPI(self._backend_url, self._api_token, subsystem)
|
audit = AuditAPI(self._backend_url, self._api_token, subsystem)
|
||||||
|
|||||||
@ -1,8 +1,111 @@
|
|||||||
"""Exceptions."""
|
"""Exceptions."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from typing import Any, TypeGuard, TypedDict
|
||||||
|
|
||||||
|
from pydantic_core import ErrorDetails
|
||||||
|
|
||||||
|
from httpx import Response
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class BackendError(Exception):
|
||||||
|
"""Generic backend error."""
|
||||||
|
|
||||||
|
|
||||||
|
class HttpErrorItem(TypedDict):
|
||||||
|
"""Interface for the 'item' type of the http error class."""
|
||||||
|
|
||||||
|
loc: Sequence[str | int]
|
||||||
|
msg: str
|
||||||
|
type: str
|
||||||
|
|
||||||
|
|
||||||
|
class HttpError(TypedDict):
|
||||||
|
"""Interface of the http error class."""
|
||||||
|
|
||||||
|
detail: Sequence[HttpErrorItem]
|
||||||
|
|
||||||
|
|
||||||
class BackendValidationError(Exception):
|
class BackendValidationError(Exception):
|
||||||
"""Validation error."""
|
"""Validation error."""
|
||||||
|
|
||||||
|
def __init__(self, errors: Sequence[HttpErrorItem]) -> None:
|
||||||
|
"""Backend validation error.
|
||||||
|
|
||||||
|
Modelled after fastapi's RequestValidationException.
|
||||||
|
"""
|
||||||
|
self._errors: Sequence[HttpErrorItem] = errors
|
||||||
|
|
||||||
|
def errors(self) -> Sequence[HttpErrorItem]:
|
||||||
|
"""Get errors."""
|
||||||
|
return self._errors
|
||||||
|
|
||||||
|
|
||||||
class BackendConnectionError(Exception):
|
class BackendConnectionError(Exception):
|
||||||
"""Could not connect to backend server."""
|
"""Could not connect to backend server."""
|
||||||
|
|
||||||
|
|
||||||
|
def is_error_details(instance: Any) -> TypeGuard[ErrorDetails]:
|
||||||
|
"""Confirm that something is an error detail."""
|
||||||
|
if "type" in instance and "loc" in instance:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def is_http_error(instance: Any) -> TypeGuard[HttpError]:
|
||||||
|
"""Check if object is a http error."""
|
||||||
|
if not isinstance(instance, dict):
|
||||||
|
return False
|
||||||
|
details = instance.get("detail")
|
||||||
|
if not isinstance(details, list):
|
||||||
|
return False
|
||||||
|
if all([is_error_details(detail) for detail in details]):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def handle_validation_error(
|
||||||
|
response: Response,
|
||||||
|
) -> BackendValidationError | BackendError:
|
||||||
|
"""Convert a 422 response into a backend validation error."""
|
||||||
|
try:
|
||||||
|
body = response.json()
|
||||||
|
if not (is_http_error(body)):
|
||||||
|
LOG.error("Unknown validation error type: %s", response.text)
|
||||||
|
return BackendError("Unhandled validation error from backend.")
|
||||||
|
return BackendValidationError(body["detail"])
|
||||||
|
|
||||||
|
except:
|
||||||
|
LOG.error("Unknown validation error type: %s", response.text)
|
||||||
|
return BackendError("Unhandled validation error from backend.")
|
||||||
|
|
||||||
|
|
||||||
|
def get_error_message(response: Response) -> str:
|
||||||
|
"""Get error message."""
|
||||||
|
error_message = "Unknown error"
|
||||||
|
if (
|
||||||
|
content_type := response.headers.get("Content-Type")
|
||||||
|
) and content_type == "application/json":
|
||||||
|
body = response.json()
|
||||||
|
if detail := body.get("detail"):
|
||||||
|
error_message = str(detail)
|
||||||
|
elif response.text:
|
||||||
|
error_message = response.text
|
||||||
|
|
||||||
|
return error_message
|
||||||
|
|
||||||
|
|
||||||
|
def handle_exception(response: Response) -> None:
|
||||||
|
"""Handle exceptions."""
|
||||||
|
error_message = get_error_message(response)
|
||||||
|
if error_field := response.headers.get("X-Model-Field"):
|
||||||
|
field_error: HttpErrorItem = {
|
||||||
|
"loc": ["body", str(error_field)],
|
||||||
|
"msg": error_message,
|
||||||
|
"type": "None",
|
||||||
|
}
|
||||||
|
raise BackendValidationError(errors=[field_error])
|
||||||
|
raise BackendError(error_message)
|
||||||
|
|||||||
@ -181,3 +181,10 @@ class AuditListResult(BaseModel):
|
|||||||
results: list[AuditLog]
|
results: list[AuditLog]
|
||||||
total: int
|
total: int
|
||||||
remaining: int
|
remaining: int
|
||||||
|
|
||||||
|
class SystemStats(BaseModel):
|
||||||
|
"""Generic system stats."""
|
||||||
|
|
||||||
|
clients: int
|
||||||
|
secrets: int
|
||||||
|
audit_events: int
|
||||||
|
|||||||
Reference in New Issue
Block a user