feat(N1): Scope-Registry via Contract — workspace_scopes() Deklarationen + /scope-definitions Endpoint (#365)
Check Cross-Plugin Imports / check (push) Has been cancelled
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
This commit is contained in:
@@ -2,7 +2,9 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator, model_validator
|
||||
|
||||
|
||||
class WorkspaceBase(BaseModel):
|
||||
@@ -34,6 +36,72 @@ class WorkspaceResponse(WorkspaceBase):
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user