bifroest/app/tools/routes.py
2026-10-27 08:34:17 +01:00

82 lines
4.2 KiB
Python

"""Tool-Einstufung (Plattform-Admin) und Verschärfung je Workspace (Workspace-Admin)."""
from typing import Annotated
from urllib.parse import urlencode
from fastapi import APIRouter, Form, Request
from fastapi.responses import HTMLResponse, Response
from sqlalchemy import select
from app import web
from app.core.errors import Invalid
from app.identity.admin import require as require_platform_admin
from app.mcp import service as mcp
from app.tools import service as tools
from app.tools.models import DECISIONS, RISK_CLASSES
from app.workspaces.models import Workspace
Text = Annotated[str, Form()]
def router() -> APIRouter:
r = APIRouter()
def page(request: Request, principal, error: str = "", notice: str = "", status: int = 200) -> HTMLResponse:
require_platform_admin(principal)
with web.db(request).session() as session:
origins = mcp.origins(session)
workspace_names = dict(session.execute(select(Workspace.id, Workspace.name)).all())
# Tools der Datenbanken stuft man am Treiber ein
definitions = [d for d in tools.sync(session)
if not (d.kind in origins and origins[d.kind].template_id)]
for server in origins.values():
session.expunge(server)
return web.render(request, "admin_tools.html", {"definitions": definitions, "classes": RISK_CLASSES,
"origins": origins, "workspace_names": workspace_names,
"decisions": DECISIONS, "defaults": tools.DEFAULT_DECISION,
"error": error, "notice": notice},
status)
def act(request: Request, principal, action, notice) -> Response:
try:
with web.db(request).session() as session:
result = action(session)
except Invalid as e:
return page(request, principal, error=str(e), status=400)
return page(request, principal, notice=notice(result) if callable(notice) else notice)
@r.get("/admin/tools")
def overview(request: Request, principal: web.User) -> HTMLResponse:
return page(request, principal)
@r.post("/admin/tools/suggested")
def suggested(request: Request, principal: web.Checked) -> Response:
return act(request, principal, lambda s: tools.classify_suggested(s, principal),
lambda count: f"{count} Tools nach Vorschlag eingestuft.")
@r.post("/admin/tools/{name}")
def classify(request: Request, principal: web.Checked, name: str, risk_class: Text, decision: Text,
approval_level: Text = "workspace_admin", four_eyes: Annotated[bool, Form()] = False) -> Response:
return act(request, principal, lambda s: tools.classify(s, principal, name, risk_class, decision,
approval_level, four_eyes),
f"{name} eingestuft.")
@r.post("/workspaces/{workspace_id}/tools/{name}")
def workspace_policy(request: Request, principal: web.Checked, workspace_id: int, name: str,
decision: Text = "", four_eyes: Annotated[bool, Form()] = False) -> Response:
with web.db(request).session() as session:
tools.set_workspace_policy(session, principal, workspace_id, name, decision, four_eyes)
return web.redirect(f"/workspaces/{workspace_id}/settings?{urlencode({'tab': 'tools', 'tool': name})}")
@r.post("/workspaces/{workspace_id}/source-rules")
def source_rule(request: Request, principal: web.Checked, workspace_id: int,
rule: Text, decision: Text = "", four_eyes: Annotated[bool, Form()] = False) -> Response:
"""rule = <source_id>:<tool>; leere Entscheidung ohne Vier-Augen-Prinzip entfernt die Regel."""
source_id, _, name = rule.partition(":")
if not source_id.isdigit() or not name:
raise Invalid("Bitte Quelle und Tool wählen.")
with web.db(request).session() as session:
tools.set_source_policy(session, principal, workspace_id, int(source_id), name, decision, four_eyes)
return web.redirect(f"/workspaces/{workspace_id}/settings?tab=tools&saved=1#source-rules")
return r