feat: add personal and group view ownership
This commit is contained in:
@@ -12,6 +12,10 @@ from govoplan_views.backend.manifest import (
|
||||
ASSIGNMENT_WRITE_SCOPE,
|
||||
DEFINITION_READ_SCOPE,
|
||||
DEFINITION_WRITE_SCOPE,
|
||||
GROUP_DEFINITION_READ_SCOPE,
|
||||
GROUP_DEFINITION_WRITE_SCOPE,
|
||||
PERSONAL_DEFINITION_READ_SCOPE,
|
||||
PERSONAL_DEFINITION_WRITE_SCOPE,
|
||||
SELECTION_READ_SCOPE,
|
||||
SELECTION_WRITE_SCOPE,
|
||||
SYSTEM_ASSIGNMENT_READ_SCOPE,
|
||||
@@ -82,7 +86,15 @@ def _require_any_scope(principal: ApiPrincipal, *scopes: str) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _require_definition_read(principal: ApiPrincipal, scope_type: str) -> None:
|
||||
def _has_any_scope(principal: ApiPrincipal, *scopes: str) -> bool:
|
||||
return any(has_scope(principal, scope) for scope in scopes)
|
||||
|
||||
|
||||
def _require_definition_read(
|
||||
principal: ApiPrincipal,
|
||||
scope_type: str,
|
||||
scope_id: str | None = None,
|
||||
) -> None:
|
||||
if scope_type == "system":
|
||||
_require_any_scope(
|
||||
principal,
|
||||
@@ -90,22 +102,98 @@ def _require_definition_read(principal: ApiPrincipal, scope_type: str) -> None:
|
||||
SYSTEM_DEFINITION_WRITE_SCOPE,
|
||||
)
|
||||
return
|
||||
_require_any_scope(
|
||||
principal,
|
||||
DEFINITION_READ_SCOPE,
|
||||
DEFINITION_WRITE_SCOPE,
|
||||
if scope_type == "tenant":
|
||||
_require_any_scope(
|
||||
principal,
|
||||
DEFINITION_READ_SCOPE,
|
||||
DEFINITION_WRITE_SCOPE,
|
||||
)
|
||||
return
|
||||
if _has_any_scope(principal, DEFINITION_READ_SCOPE, DEFINITION_WRITE_SCOPE):
|
||||
return
|
||||
if scope_type == "group":
|
||||
if not scope_id or scope_id not in principal.group_ids:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Group Views can only be managed by members of that group.",
|
||||
)
|
||||
_require_any_scope(
|
||||
principal,
|
||||
GROUP_DEFINITION_READ_SCOPE,
|
||||
GROUP_DEFINITION_WRITE_SCOPE,
|
||||
)
|
||||
return
|
||||
if scope_type == "user":
|
||||
if scope_id != principal.account_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Personal Views can only be managed by their owner.",
|
||||
)
|
||||
_require_any_scope(
|
||||
principal,
|
||||
PERSONAL_DEFINITION_READ_SCOPE,
|
||||
PERSONAL_DEFINITION_WRITE_SCOPE,
|
||||
)
|
||||
return
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||
detail=f"Unsupported View definition scope: {scope_type}",
|
||||
)
|
||||
|
||||
|
||||
def _require_definition_write(principal: ApiPrincipal, scope_type: str) -> None:
|
||||
_require_any_scope(
|
||||
principal,
|
||||
SYSTEM_DEFINITION_WRITE_SCOPE
|
||||
if scope_type == "system"
|
||||
else DEFINITION_WRITE_SCOPE,
|
||||
def _require_definition_write(
|
||||
principal: ApiPrincipal,
|
||||
scope_type: str,
|
||||
scope_id: str | None = None,
|
||||
) -> None:
|
||||
if scope_type == "system":
|
||||
_require_any_scope(principal, SYSTEM_DEFINITION_WRITE_SCOPE)
|
||||
return
|
||||
if scope_type == "tenant":
|
||||
_require_any_scope(principal, DEFINITION_WRITE_SCOPE)
|
||||
return
|
||||
if has_scope(principal, DEFINITION_WRITE_SCOPE):
|
||||
return
|
||||
if scope_type == "group":
|
||||
if not scope_id or scope_id not in principal.group_ids:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Group Views can only be managed by members of that group.",
|
||||
)
|
||||
_require_any_scope(principal, GROUP_DEFINITION_WRITE_SCOPE)
|
||||
return
|
||||
if scope_type == "user":
|
||||
if scope_id != principal.account_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Personal Views can only be managed by their owner.",
|
||||
)
|
||||
_require_any_scope(principal, PERSONAL_DEFINITION_WRITE_SCOPE)
|
||||
return
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||
detail=f"Unsupported View definition scope: {scope_type}",
|
||||
)
|
||||
|
||||
|
||||
def _definition_scope_id(
|
||||
principal: ApiPrincipal,
|
||||
scope_type: str,
|
||||
scope_id: str | None,
|
||||
) -> str | None:
|
||||
if scope_type in {"system", "tenant"}:
|
||||
return scope_id
|
||||
if scope_type == "user":
|
||||
return (scope_id or principal.account_id).strip()
|
||||
target_id = (scope_id or "").strip()
|
||||
if not target_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||
detail="Group View definitions require a target id.",
|
||||
)
|
||||
return target_id
|
||||
|
||||
|
||||
def _require_assignment_read(principal: ApiPrincipal, scope_type: str) -> None:
|
||||
if scope_type == "system":
|
||||
_require_any_scope(
|
||||
@@ -279,6 +367,10 @@ def api_view_surfaces(
|
||||
principal,
|
||||
DEFINITION_READ_SCOPE,
|
||||
DEFINITION_WRITE_SCOPE,
|
||||
GROUP_DEFINITION_READ_SCOPE,
|
||||
GROUP_DEFINITION_WRITE_SCOPE,
|
||||
PERSONAL_DEFINITION_READ_SCOPE,
|
||||
PERSONAL_DEFINITION_WRITE_SCOPE,
|
||||
SYSTEM_DEFINITION_READ_SCOPE,
|
||||
SYSTEM_DEFINITION_WRITE_SCOPE,
|
||||
)
|
||||
@@ -313,16 +405,22 @@ def api_view_surfaces(
|
||||
|
||||
@router.get("/definitions", response_model=ViewDefinitionListResponse)
|
||||
def api_list_definitions(
|
||||
scope_type: str = Query(default="tenant", pattern="^(system|tenant)$"),
|
||||
scope_type: str = Query(
|
||||
default="tenant",
|
||||
pattern="^(system|tenant|group|user)$",
|
||||
),
|
||||
scope_id: str | None = Query(default=None, max_length=255),
|
||||
include_inherited: bool = True,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> ViewDefinitionListResponse:
|
||||
_require_definition_read(principal, scope_type)
|
||||
target_id = _definition_scope_id(principal, scope_type, scope_id)
|
||||
_require_definition_read(principal, scope_type, target_id)
|
||||
definitions = list_definitions(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
scope_type=scope_type,
|
||||
scope_id=target_id,
|
||||
include_inherited=include_inherited,
|
||||
)
|
||||
return ViewDefinitionListResponse(
|
||||
@@ -330,7 +428,16 @@ def api_list_definitions(
|
||||
_definition_response(
|
||||
session,
|
||||
definition,
|
||||
readonly=(scope_type == "tenant" and definition.scope_type == "system"),
|
||||
readonly=(
|
||||
(scope_type == "tenant" and definition.scope_type == "system")
|
||||
or (
|
||||
scope_type in {"group", "user"}
|
||||
and (
|
||||
definition.scope_type != scope_type
|
||||
or definition.scope_id != target_id
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
for definition in definitions
|
||||
]
|
||||
@@ -347,12 +454,18 @@ def api_create_definition(
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> ViewDefinitionResponse:
|
||||
_require_definition_write(principal, payload.scope_type)
|
||||
target_id = _definition_scope_id(
|
||||
principal,
|
||||
payload.scope_type,
|
||||
payload.scope_id,
|
||||
)
|
||||
_require_definition_write(principal, payload.scope_type, target_id)
|
||||
try:
|
||||
definition = create_definition(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
scope_type=payload.scope_type,
|
||||
scope_id=target_id,
|
||||
definition_key=payload.definition_key,
|
||||
name=payload.name,
|
||||
description=payload.description,
|
||||
@@ -391,7 +504,11 @@ def api_update_definition(
|
||||
tenant_id=principal.tenant_id,
|
||||
definition_id=definition_id,
|
||||
)
|
||||
_require_definition_write(principal, definition.scope_type)
|
||||
_require_definition_write(
|
||||
principal,
|
||||
definition.scope_type,
|
||||
definition.scope_id,
|
||||
)
|
||||
update_definition(
|
||||
session,
|
||||
definition,
|
||||
@@ -430,7 +547,11 @@ def api_list_revisions(
|
||||
tenant_id=principal.tenant_id,
|
||||
definition_id=definition_id,
|
||||
)
|
||||
_require_definition_read(principal, definition.scope_type)
|
||||
_require_definition_read(
|
||||
principal,
|
||||
definition.scope_type,
|
||||
definition.scope_id,
|
||||
)
|
||||
return [
|
||||
ViewRevisionResponse.model_validate(revision)
|
||||
for revision in definition_revisions(
|
||||
@@ -458,7 +579,11 @@ def api_create_revision(
|
||||
tenant_id=principal.tenant_id,
|
||||
definition_id=definition_id,
|
||||
)
|
||||
_require_definition_write(principal, definition.scope_type)
|
||||
_require_definition_write(
|
||||
principal,
|
||||
definition.scope_type,
|
||||
definition.scope_id,
|
||||
)
|
||||
revision = create_revision(
|
||||
session,
|
||||
definition,
|
||||
@@ -500,7 +625,11 @@ def api_publish_revision(
|
||||
tenant_id=principal.tenant_id,
|
||||
definition_id=definition_id,
|
||||
)
|
||||
_require_definition_write(principal, definition.scope_type)
|
||||
_require_definition_write(
|
||||
principal,
|
||||
definition.scope_type,
|
||||
definition.scope_id,
|
||||
)
|
||||
revision = get_revision(
|
||||
session,
|
||||
definition_id=definition.id,
|
||||
@@ -543,7 +672,11 @@ def api_archive_definition(
|
||||
tenant_id=principal.tenant_id,
|
||||
definition_id=definition_id,
|
||||
)
|
||||
_require_definition_write(principal, definition.scope_type)
|
||||
_require_definition_write(
|
||||
principal,
|
||||
definition.scope_type,
|
||||
definition.scope_id,
|
||||
)
|
||||
archive_definition(
|
||||
session,
|
||||
definition,
|
||||
|
||||
Reference in New Issue
Block a user