Add support for perm=foo+bar to specify multiple scopes that are required. Multiple perm args are acceptable too. Pretty logging of permission denied errors.
This commit is contained in:
@@ -78,7 +78,7 @@ async def validate_token(
|
||||
try:
|
||||
ctx = await authz.verify(
|
||||
auth,
|
||||
perm,
|
||||
" ".join(perm).split(),
|
||||
host=request.headers.get("host"),
|
||||
max_age=max_age,
|
||||
)
|
||||
@@ -131,7 +131,10 @@ async def forward_authentication(
|
||||
"""
|
||||
try:
|
||||
ctx = await authz.verify(
|
||||
auth, perm, host=request.headers.get("host"), max_age=max_age
|
||||
auth,
|
||||
" ".join(perm).split(),
|
||||
host=request.headers.get("host"),
|
||||
max_age=max_age,
|
||||
)
|
||||
# Build permission scopes for Remote-Groups header
|
||||
role_permissions = (
|
||||
|
||||
@@ -2,6 +2,7 @@ import logging
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
from paskia.fastapi.logging import log_permission_denied
|
||||
from paskia.util import permutil, sessionutil
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -93,20 +94,14 @@ async def verify(
|
||||
logger.warning(f"Invalid max_age format '{max_age}': {e}")
|
||||
|
||||
if not match(ctx, perm):
|
||||
# Determine which permissions are missing for clearer diagnostics
|
||||
effective_scopes = (
|
||||
{p.scope for p in (ctx.permissions or [])}
|
||||
if ctx.permissions
|
||||
else set(ctx.role.permissions or [])
|
||||
)
|
||||
missing = sorted(set(perm) - effective_scopes)
|
||||
logger.warning(
|
||||
"Permission denied: user=%s role=%s missing=%s required=%s granted=%s", # noqa: E501
|
||||
getattr(ctx.user, "uuid", "?"),
|
||||
getattr(ctx.role, "display_name", "?"),
|
||||
missing,
|
||||
perm,
|
||||
list(effective_scopes),
|
||||
log_permission_denied(
|
||||
ctx, perm, missing, require_all=(match == permutil.has_all)
|
||||
)
|
||||
raise AuthException(
|
||||
status_code=403, mode="forbidden", detail="Permission required"
|
||||
|
||||
@@ -4,8 +4,12 @@ import logging
|
||||
import sys
|
||||
import time
|
||||
from ipaddress import IPv6Address
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from paskia.db.structs import SessionContext
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
|
||||
@@ -25,6 +29,12 @@ _TIMING = "\033[38;5;242m" # timing/devmode (dark grey)
|
||||
_WS_OPEN = "\033[1;93m" # WebSocket connect (bold bright yellow)
|
||||
_WS_CLOSE = "\033[33m" # WebSocket disconnect (yellow)
|
||||
_WS_STATUS = "\033[38;5;242m" # WebSocket close status (dark grey)
|
||||
_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 format_ipv6_network(ip: str) -> str:
|
||||
@@ -198,6 +208,26 @@ def log_ws_close(ws_id: int, close_code: int | None, duration: float) -> None:
|
||||
logger.info(f"{' ' * 15} {prefix} {status_str} {timing_str}")
|
||||
|
||||
|
||||
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}"
|
||||
)
|
||||
|
||||
|
||||
class AccessLogMiddleware(BaseHTTPMiddleware):
|
||||
"""Middleware that logs HTTP requests with custom format."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user