43 lines
1.5 KiB
Python
43 lines
1.5 KiB
Python
"""Authorization-related logging.
|
|
|
|
HTTP/WebSocket access logging is handled by fastapi_vue's ASGI middleware
|
|
(installed via fastapi_vue.server.run); request handlers can pass extra
|
|
details to the access log line via request.state.log_extra.
|
|
"""
|
|
|
|
import logging
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from paskia.db.structs import SessionContext
|
|
|
|
logger = logging.getLogger("paskia.access")
|
|
|
|
_RESET = "\033[0m"
|
|
_AUTHZ_DENIED = "\033[0;31m" # Permission denied (red)
|
|
_AUTHZ_USER = "\033[1;34m" # User info (light blue)
|
|
_AUTHZ_ORG = "\033[34m" # User info (blue)
|
|
_AUTHZ_NEEDS = "\033[1;38;5;231m" # Needs (brightest white)
|
|
_AUTHZ_MISSING = "\033[1;31m" # Missing scope (bold red)
|
|
_AUTHZ_GRANTED = "\033[0;32m" # Granted scope (green)
|
|
|
|
|
|
def log_permission_denied(
|
|
ctx: SessionContext, required: list[str], missing: list[str], *, require_all: bool
|
|
) -> None:
|
|
"""Log permission denied with org, role, user and highlighted missing scopes."""
|
|
missing_set = set(missing)
|
|
scopes = " ".join(
|
|
f"{_AUTHZ_MISSING}{s}✗{_RESET}"
|
|
if s in missing_set
|
|
else f"{_AUTHZ_GRANTED}{s}✓{_RESET}"
|
|
for s in required
|
|
)
|
|
n = "" if len(required) == 1 else " all" if require_all else " any"
|
|
logger.warning(
|
|
f"{_AUTHZ_DENIED}Permission denied{_RESET} "
|
|
f"{_AUTHZ_USER}{ctx.user.display_name}{_RESET} "
|
|
f"{_AUTHZ_ORG}({ctx.org.display_name} {ctx.role.display_name}){_RESET} "
|
|
f"{_AUTHZ_NEEDS}needs{n}:{_RESET} {scopes}"
|
|
)
|