c25356c257
Check Cross-Plugin Imports / check (push) Has been cancelled
- workspace_scopes() Contract-Hook (document_placeholders-Muster): Plugins deklarieren Scope-Dimensionen inkl. Wertequellen
- Deklarationen: contacts (Ordner/Typen/Saved-View), dms (Ordner/Datei-Typen), mail (Postfächer), calendar (Kalender/Standard-Ansicht)
- Pydantic fail-closed (schemas/workspace.py): ScopeOption, ScopeValueSource (nur interne /api/v1-Pfade, SSRF-sicher), WorkspaceScopeDimension, WorkspaceModuleScopes
- Aggregator workspace_scope_service.py: discovered-Plugins, ARCH-014-safe, Crash-sicher, ungültige Deklarationen verworfen
- GET /api/v1/workspaces/scope-definitions (workspaces:configure_modules) vor /{workspace_id} registriert
- Security-Invariante: Scope = reine UND-Einschränkung (Workspace ∧ RLS ∧ ABAC ∧ Permissions)
- Tests: 18/18 neu (TDD rot→grün), Regression 17/17, Checker 0 Verstöße, Ruff clean
- Doku: api-documentation.md Workspaces-Sektion, PROGRESS.md Phase N1
114 lines
3.1 KiB
Python
114 lines
3.1 KiB
Python
"""Schemas for Workspace API endpoints."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Literal
|
|
|
|
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
|
|
|
|
class WorkspaceBase(BaseModel):
|
|
name: str
|
|
icon: str = "LayoutGrid"
|
|
description: str | None = None
|
|
is_default: bool = False
|
|
is_active: bool = True
|
|
|
|
|
|
class WorkspaceCreate(WorkspaceBase):
|
|
pass
|
|
|
|
|
|
class WorkspaceUpdate(BaseModel):
|
|
name: str | None = None
|
|
icon: str | None = None
|
|
description: str | None = None
|
|
is_default: bool | None = None
|
|
is_active: bool | None = None
|
|
|
|
|
|
class WorkspaceResponse(WorkspaceBase):
|
|
id: str
|
|
created_by: str | None = None
|
|
created_at: str | None = None
|
|
updated_at: str | None = None
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
# ─── N1: Workspace-Scope-Registry (Phase N) ───────────────────
|
|
|
|
|
|
class ScopeOption(BaseModel):
|
|
"""Ein auswählbarer Wert einer Scope-Dimension."""
|
|
|
|
value: str
|
|
label: str
|
|
|
|
|
|
class ScopeValueSource(BaseModel):
|
|
"""Deklariert, woher der Editor wählbare Werte lädt.
|
|
|
|
Security (fail-closed): nur interne ``/api/v1/...``-Pfade sind
|
|
erlaubt — keine absoluten URLs, keine protokoll-relativen Pfade,
|
|
keine externen Hosts (SSRF-sicher per Konstruktion).
|
|
"""
|
|
|
|
endpoint: str
|
|
items_path: str = "items"
|
|
value_key: str = "id"
|
|
label_key: str = "name"
|
|
|
|
@field_validator("endpoint")
|
|
@classmethod
|
|
def _must_be_internal_api_path(cls, v: str) -> str:
|
|
if not v.startswith("/api/v1/"):
|
|
raise ValueError(
|
|
"value_source endpoint must be an internal /api/v1/ path"
|
|
)
|
|
return v
|
|
|
|
|
|
class WorkspaceScopeDimension(BaseModel):
|
|
"""Eine filterbare Dimension eines Moduls im Workspace (N1).
|
|
|
|
``multiselect``/``select`` brauchen zwingend ``options`` oder
|
|
``value_source`` (fail-closed gegen leere Filter-UI); ``toggle``
|
|
ist ein reiner Schalter mit ``default``.
|
|
"""
|
|
|
|
key: str = Field(..., min_length=1, max_length=100)
|
|
label: str = Field(..., min_length=1, max_length=200)
|
|
control: Literal["multiselect", "select", "toggle"]
|
|
options: list[ScopeOption] = Field(default_factory=list)
|
|
value_source: ScopeValueSource | None = None
|
|
default: bool | str | None = None
|
|
|
|
@model_validator(mode="after")
|
|
def _options_or_source_required(self) -> WorkspaceScopeDimension:
|
|
if self.control in ("multiselect", "select"):
|
|
if not self.options and self.value_source is None:
|
|
raise ValueError(
|
|
f"scope dimension '{self.key}': control '{self.control}' "
|
|
"requires options or value_source"
|
|
)
|
|
return self
|
|
|
|
|
|
class WorkspaceModuleScopes(BaseModel):
|
|
"""Scope-Contribution eines Plugins für ein Modul."""
|
|
|
|
module_key: str = Field(..., min_length=1, max_length=100)
|
|
dimensions: list[WorkspaceScopeDimension] = Field(..., min_length=1)
|
|
|
|
|
|
class WorkspaceModuleResponse(BaseModel):
|
|
id: str
|
|
workspace_id: str
|
|
module_key: str
|
|
is_visible: bool
|
|
menu_order: int
|
|
config: dict = {}
|
|
|
|
model_config = {"from_attributes": True}
|