163 lines
4.4 KiB
Python
163 lines
4.4 KiB
Python
from collections.abc import Awaitable, Callable
|
|
from typing import Any
|
|
|
|
from fastapi import Depends, HTTPException, Request, status
|
|
|
|
from app.auth.project_dependencies import ProjectContext, get_project_context
|
|
|
|
WEBGIS_VIEW = "webgis.view"
|
|
WEBGIS_EDIT = "webgis.edit"
|
|
SCADA_VIEW = "scada.view"
|
|
SCADA_CLEAN = "scada.clean"
|
|
SIMULATION_VIEW = "simulation.view"
|
|
SIMULATION_RUN = "simulation.run"
|
|
BURST_VIEW = "burst.view"
|
|
BURST_RUN = "burst.run"
|
|
RISK_VIEW = "risk.view"
|
|
RISK_RUN = "risk.run"
|
|
OPTIMIZATION_VIEW = "optimization.view"
|
|
OPTIMIZATION_RUN = "optimization.run"
|
|
MODEL_IMPORT = "model.import"
|
|
AUDIT_VIEW = "audit.view"
|
|
ENVIRONMENT_MANAGE = "environment.manage"
|
|
MEMBERSHIP_MANAGE = "membership.manage"
|
|
|
|
PROJECT_MEMBER_PERMISSIONS = frozenset(
|
|
{
|
|
WEBGIS_VIEW,
|
|
WEBGIS_EDIT,
|
|
SCADA_VIEW,
|
|
SCADA_CLEAN,
|
|
SIMULATION_VIEW,
|
|
SIMULATION_RUN,
|
|
BURST_VIEW,
|
|
BURST_RUN,
|
|
RISK_VIEW,
|
|
RISK_RUN,
|
|
OPTIMIZATION_VIEW,
|
|
OPTIMIZATION_RUN,
|
|
}
|
|
)
|
|
|
|
PROJECT_VIEWER_PERMISSIONS = frozenset(
|
|
{
|
|
WEBGIS_VIEW,
|
|
SCADA_VIEW,
|
|
SIMULATION_VIEW,
|
|
}
|
|
)
|
|
|
|
SYSTEM_ADMIN_PERMISSIONS = frozenset(
|
|
{
|
|
MODEL_IMPORT,
|
|
AUDIT_VIEW,
|
|
ENVIRONMENT_MANAGE,
|
|
MEMBERSHIP_MANAGE,
|
|
}
|
|
)
|
|
|
|
PROJECT_ROLE_PERMISSIONS: dict[str, frozenset[str]] = {
|
|
"member": PROJECT_MEMBER_PERMISSIONS,
|
|
"viewer": PROJECT_VIEWER_PERMISSIONS,
|
|
}
|
|
|
|
|
|
def resolve_permissions(
|
|
*,
|
|
project_role: str | None,
|
|
system_role: str,
|
|
is_superuser: bool,
|
|
) -> frozenset[str]:
|
|
permissions = set(PROJECT_ROLE_PERMISSIONS.get(project_role or "", frozenset()))
|
|
if is_superuser or system_role == "admin":
|
|
permissions.update(SYSTEM_ADMIN_PERMISSIONS)
|
|
return frozenset(permissions)
|
|
|
|
|
|
def permissions_for_context(ctx: ProjectContext) -> frozenset[str]:
|
|
return resolve_permissions(
|
|
project_role=ctx.project_role,
|
|
system_role=ctx.system_role,
|
|
is_superuser=ctx.is_superuser,
|
|
)
|
|
|
|
|
|
def _permission_denied(permission: str) -> HTTPException:
|
|
return HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail={
|
|
"code": "permission_denied",
|
|
"permission": permission,
|
|
},
|
|
)
|
|
|
|
|
|
async def _enforce_project_scope(request: Request, ctx: ProjectContext) -> None:
|
|
requested_network = (
|
|
request.path_params.get("network")
|
|
or request.query_params.get("network")
|
|
)
|
|
if not requested_network:
|
|
content_type = request.headers.get("content-type", "")
|
|
if content_type.startswith("application/json"):
|
|
try:
|
|
payload = await request.json()
|
|
except (ValueError, RuntimeError):
|
|
payload = None
|
|
if isinstance(payload, dict):
|
|
requested_network = payload.get("network")
|
|
|
|
if requested_network and str(requested_network) != ctx.project_code:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail={
|
|
"code": "project_scope_denied",
|
|
"project_id": str(ctx.project_id),
|
|
},
|
|
)
|
|
|
|
|
|
def require_permission(
|
|
permission: str,
|
|
) -> Callable[..., Awaitable[ProjectContext]]:
|
|
async def dependency(
|
|
request: Request,
|
|
ctx: ProjectContext = Depends(get_project_context),
|
|
) -> ProjectContext:
|
|
if permission not in permissions_for_context(ctx):
|
|
raise _permission_denied(permission)
|
|
await _enforce_project_scope(request, ctx)
|
|
return ctx
|
|
|
|
return dependency
|
|
|
|
|
|
def require_method_permission(
|
|
*,
|
|
read_permission: str,
|
|
write_permission: str,
|
|
) -> Callable[..., Awaitable[ProjectContext]]:
|
|
async def dependency(
|
|
request: Request,
|
|
ctx: ProjectContext = Depends(get_project_context),
|
|
) -> ProjectContext:
|
|
permission = (
|
|
read_permission
|
|
if request.method.upper() in {"GET", "HEAD", "OPTIONS"}
|
|
else write_permission
|
|
)
|
|
if permission not in permissions_for_context(ctx):
|
|
raise _permission_denied(permission)
|
|
await _enforce_project_scope(request, ctx)
|
|
return ctx
|
|
|
|
return dependency
|
|
|
|
|
|
def has_permission(user: Any, project_role: str | None, permission: str) -> bool:
|
|
return permission in resolve_permissions(
|
|
project_role=project_role,
|
|
system_role=str(getattr(user, "role", "user")),
|
|
is_superuser=bool(getattr(user, "is_superuser", False)),
|
|
)
|