293 lines
9.9 KiB
Python
293 lines
9.9 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Literal
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from sqlalchemy.orm import Session
|
|
|
|
from govoplan_core.auth import ApiPrincipal, require_scope
|
|
from govoplan_files.backend.change_tracking import (
|
|
FILES_CONNECTOR_SPACES_COLLECTION,
|
|
)
|
|
from govoplan_files.backend.schemas import (
|
|
FileConnectorSpaceCreateRequest,
|
|
FileConnectorSpaceResponse,
|
|
FileConnectorSpacesResponse,
|
|
FileConnectorSpaceUpdateRequest,
|
|
FileSpaceResponse,
|
|
FileSpacesResponse,
|
|
)
|
|
from govoplan_core.db.session import get_session
|
|
from govoplan_files.backend.storage.access import group_refs_for_ids, user_group_ids
|
|
from govoplan_files.backend.storage.common import FileStorageError
|
|
from govoplan_files.backend.storage.connector_spaces import (
|
|
connector_space_owner_id,
|
|
create_connector_space,
|
|
get_connector_space_for_user,
|
|
list_connector_spaces_for_user,
|
|
soft_delete_connector_space,
|
|
update_connector_space,
|
|
)
|
|
from govoplan_files.backend.storage.connector_policy import (
|
|
ConnectorPolicyDenied,
|
|
)
|
|
|
|
|
|
from govoplan_files.backend.route_support import (
|
|
FILES_CONNECTOR_SPACE_RESOURCE,
|
|
_connector_policy_error,
|
|
_connector_space_file_space_response,
|
|
_connector_space_policy_decision,
|
|
_connector_space_response,
|
|
_http_error,
|
|
_is_admin,
|
|
_record_connector_settings_change,
|
|
_visible_connector_profile,
|
|
)
|
|
|
|
router = APIRouter(prefix="/files", tags=["files"])
|
|
|
|
|
|
@router.get("/spaces", response_model=FileSpacesResponse)
|
|
def list_file_spaces(
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(require_scope("files:file:read")),
|
|
):
|
|
spaces = [
|
|
FileSpaceResponse(
|
|
id=f"user:{principal.user.id}",
|
|
label="My files",
|
|
owner_type="user",
|
|
owner_id=principal.user.id,
|
|
description="Files owned by your user account.",
|
|
)
|
|
]
|
|
group_ids = user_group_ids(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
user_id=principal.user.id,
|
|
include_admin_groups=_is_admin(principal),
|
|
)
|
|
if group_ids:
|
|
groups = group_refs_for_ids(tenant_id=principal.tenant_id, group_ids=group_ids)
|
|
spaces.extend(
|
|
FileSpaceResponse(
|
|
id=f"group:{group.id}",
|
|
label=f"{group.name} files",
|
|
owner_type="group",
|
|
owner_id=group.id,
|
|
description="Files owned by this group.",
|
|
)
|
|
for group in groups
|
|
)
|
|
connector_spaces = list_connector_spaces_for_user(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
user_id=principal.user.id,
|
|
is_admin=_is_admin(principal),
|
|
)
|
|
spaces.extend(
|
|
_connector_space_file_space_response(space) for space in connector_spaces
|
|
)
|
|
return FileSpacesResponse(spaces=spaces)
|
|
|
|
|
|
@router.get("/connector-spaces", response_model=FileConnectorSpacesResponse)
|
|
def list_file_connector_spaces(
|
|
owner_type: Literal["user", "group"] | None = None,
|
|
owner_id: str | None = None,
|
|
include_inactive: bool = False,
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(require_scope("files:file:read")),
|
|
):
|
|
try:
|
|
spaces = list_connector_spaces_for_user(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
user_id=principal.user.id,
|
|
owner_type=owner_type,
|
|
owner_id=owner_id,
|
|
include_inactive=include_inactive and _is_admin(principal),
|
|
is_admin=_is_admin(principal),
|
|
)
|
|
return FileConnectorSpacesResponse(
|
|
spaces=[_connector_space_response(space) for space in spaces]
|
|
)
|
|
except FileStorageError as exc:
|
|
raise _http_error(exc) from exc
|
|
|
|
|
|
@router.post(
|
|
"/connector-spaces",
|
|
response_model=FileConnectorSpaceResponse,
|
|
status_code=status.HTTP_201_CREATED,
|
|
)
|
|
def create_file_connector_space(
|
|
payload: FileConnectorSpaceCreateRequest,
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(require_scope("files:file:organize")),
|
|
):
|
|
if payload.owner_type == "group" and not payload.owner_id:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="owner_id is required for group connector spaces",
|
|
)
|
|
target_owner = payload.owner_id or principal.user.id
|
|
try:
|
|
profile = _visible_connector_profile(
|
|
session, principal, payload.connector_profile_id
|
|
)
|
|
decision = _connector_space_policy_decision(
|
|
profile,
|
|
library_id=payload.library_id,
|
|
remote_path=payload.remote_path,
|
|
operation="link",
|
|
)
|
|
if not decision.allowed:
|
|
raise ConnectorPolicyDenied(decision)
|
|
space = create_connector_space(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
owner_type=payload.owner_type,
|
|
owner_id=target_owner,
|
|
user_id=principal.user.id,
|
|
label=payload.label,
|
|
profile=profile,
|
|
library_id=payload.library_id,
|
|
remote_path=payload.remote_path,
|
|
sync_mode=payload.sync_mode,
|
|
metadata=payload.metadata,
|
|
is_admin=_is_admin(principal),
|
|
)
|
|
_record_connector_settings_change(
|
|
session,
|
|
collection=FILES_CONNECTOR_SPACES_COLLECTION,
|
|
resource_type=FILES_CONNECTOR_SPACE_RESOURCE,
|
|
resource_id=space.id,
|
|
operation="created",
|
|
principal=principal,
|
|
tenant_id=space.tenant_id,
|
|
payload={
|
|
"owner_type": space.owner_type,
|
|
"owner_id": connector_space_owner_id(space),
|
|
},
|
|
)
|
|
session.commit()
|
|
return _connector_space_response(space)
|
|
except ConnectorPolicyDenied as exc:
|
|
session.rollback()
|
|
raise _connector_policy_error(exc) from exc
|
|
except (FileStorageError, ValueError, json.JSONDecodeError) as exc:
|
|
session.rollback()
|
|
raise _http_error(exc) from exc
|
|
|
|
|
|
@router.patch("/connector-spaces/{space_id}", response_model=FileConnectorSpaceResponse)
|
|
def update_file_connector_space(
|
|
space_id: str,
|
|
payload: FileConnectorSpaceUpdateRequest,
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(require_scope("files:file:organize")),
|
|
):
|
|
try:
|
|
space = get_connector_space_for_user(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
user_id=principal.user.id,
|
|
space_id=space_id,
|
|
include_inactive=_is_admin(principal),
|
|
is_admin=_is_admin(principal),
|
|
)
|
|
except FileStorageError as exc:
|
|
raise _http_error(exc, not_found=True) from exc
|
|
try:
|
|
if payload.library_id is not None or payload.remote_path is not None:
|
|
profile = _visible_connector_profile(
|
|
session, principal, space.connector_profile_id
|
|
)
|
|
decision = _connector_space_policy_decision(
|
|
profile,
|
|
library_id=payload.library_id
|
|
if payload.library_id is not None
|
|
else space.library_id,
|
|
remote_path=payload.remote_path
|
|
if payload.remote_path is not None
|
|
else space.remote_path,
|
|
operation="link",
|
|
)
|
|
if not decision.allowed:
|
|
raise ConnectorPolicyDenied(decision)
|
|
update_connector_space(
|
|
session,
|
|
space,
|
|
user_id=principal.user.id,
|
|
label=payload.label,
|
|
library_id=payload.library_id,
|
|
remote_path=payload.remote_path,
|
|
sync_mode=payload.sync_mode,
|
|
is_active=payload.is_active,
|
|
metadata=payload.metadata,
|
|
is_admin=_is_admin(principal),
|
|
)
|
|
_record_connector_settings_change(
|
|
session,
|
|
collection=FILES_CONNECTOR_SPACES_COLLECTION,
|
|
resource_type=FILES_CONNECTOR_SPACE_RESOURCE,
|
|
resource_id=space.id,
|
|
operation="updated",
|
|
principal=principal,
|
|
tenant_id=space.tenant_id,
|
|
payload={
|
|
"owner_type": space.owner_type,
|
|
"owner_id": connector_space_owner_id(space),
|
|
},
|
|
)
|
|
session.commit()
|
|
return _connector_space_response(space)
|
|
except ConnectorPolicyDenied as exc:
|
|
session.rollback()
|
|
raise _connector_policy_error(exc) from exc
|
|
except (FileStorageError, ValueError, json.JSONDecodeError) as exc:
|
|
session.rollback()
|
|
raise _http_error(exc) from exc
|
|
|
|
|
|
@router.delete(
|
|
"/connector-spaces/{space_id}", response_model=FileConnectorSpaceResponse
|
|
)
|
|
def delete_file_connector_space(
|
|
space_id: str,
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(require_scope("files:file:organize")),
|
|
):
|
|
try:
|
|
space = get_connector_space_for_user(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
user_id=principal.user.id,
|
|
space_id=space_id,
|
|
include_inactive=True,
|
|
is_admin=_is_admin(principal),
|
|
)
|
|
soft_delete_connector_space(
|
|
session, space, user_id=principal.user.id, is_admin=_is_admin(principal)
|
|
)
|
|
_record_connector_settings_change(
|
|
session,
|
|
collection=FILES_CONNECTOR_SPACES_COLLECTION,
|
|
resource_type=FILES_CONNECTOR_SPACE_RESOURCE,
|
|
resource_id=space.id,
|
|
operation="deleted",
|
|
principal=principal,
|
|
tenant_id=space.tenant_id,
|
|
payload={
|
|
"owner_type": space.owner_type,
|
|
"owner_id": connector_space_owner_id(space),
|
|
},
|
|
)
|
|
session.commit()
|
|
return _connector_space_response(space)
|
|
except FileStorageError as exc:
|
|
session.rollback()
|
|
raise _http_error(exc, not_found=True) from exc
|