Release v0.1.5
This commit is contained in:
@@ -9,11 +9,10 @@ from urllib.parse import quote
|
||||
from fastapi import APIRouter, Depends, File as FastAPIFile, Form, HTTPException, UploadFile, status
|
||||
from fastapi.responses import FileResponse, StreamingResponse
|
||||
from starlette.background import BackgroundTask
|
||||
from sqlalchemy import or_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_core.auth.dependencies import ApiPrincipal, has_scope, require_scope
|
||||
from govoplan_core.core.optional import reraise_unless_missing_package
|
||||
from govoplan_access.backend.auth.dependencies import ApiPrincipal, has_scope, require_scope
|
||||
from govoplan_core.core.campaigns import CAPABILITY_CAMPAIGNS_ACCESS, CampaignAccessProvider
|
||||
from govoplan_files.backend.schemas import (
|
||||
ArchiveRequest,
|
||||
BulkFileShareRequest,
|
||||
@@ -43,12 +42,11 @@ from govoplan_files.backend.schemas import (
|
||||
TransferResponse,
|
||||
_conflict_resolutions,
|
||||
)
|
||||
from govoplan_core.db.models import Group, UserGroupMembership
|
||||
from govoplan_files.backend.db.models import CampaignAttachmentUse, FileAsset, FileBlob, FileFolder, FileShare, FileVersion
|
||||
from govoplan_core.db.session import get_session
|
||||
from govoplan_files.backend.runtime import settings
|
||||
from govoplan_files.backend.runtime import get_registry, settings
|
||||
from govoplan_files.backend.storage.paths import UnsafeFilePathError, filename_from_path, normalize_logical_path
|
||||
from govoplan_files.backend.storage.access import ensure_group_access, user_group_ids
|
||||
from govoplan_files.backend.storage.access import ensure_group_access, group_refs_for_ids, user_group_ids
|
||||
from govoplan_files.backend.storage.archives import create_zip_file, extract_zip_upload
|
||||
from govoplan_files.backend.storage.common import FileStorageError
|
||||
from govoplan_files.backend.storage.files import (
|
||||
@@ -69,13 +67,14 @@ from govoplan_files.backend.storage.transfers import rename_selection, transfer_
|
||||
router = APIRouter(prefix="/files", tags=["files"])
|
||||
|
||||
|
||||
def _campaign_models():
|
||||
try:
|
||||
from govoplan_campaign.backend.db.models import Campaign, CampaignShare
|
||||
except ModuleNotFoundError as exc:
|
||||
reraise_unless_missing_package(exc, "govoplan_campaign")
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Campaign module is not installed") from exc
|
||||
return Campaign, CampaignShare
|
||||
def _campaign_access_provider() -> CampaignAccessProvider:
|
||||
registry = get_registry()
|
||||
if registry is None or not hasattr(registry, "has_capability") or not registry.has_capability(CAPABILITY_CAMPAIGNS_ACCESS):
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Campaign module is not installed")
|
||||
capability = registry.require_capability(CAPABILITY_CAMPAIGNS_ACCESS)
|
||||
if not isinstance(capability, CampaignAccessProvider):
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Campaign access capability is invalid")
|
||||
return capability
|
||||
|
||||
|
||||
def _is_admin(principal: ApiPrincipal) -> bool:
|
||||
@@ -116,37 +115,20 @@ def _ensure_campaign_file_access(session: Session, principal: ApiPrincipal, camp
|
||||
return
|
||||
if not has_scope(principal, "campaigns:campaign:read"):
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Missing scope: campaign:read")
|
||||
Campaign, CampaignShare = _campaign_models()
|
||||
campaign = session.get(Campaign, campaign_id)
|
||||
if not campaign or campaign.tenant_id != principal.tenant_id:
|
||||
provider = _campaign_access_provider()
|
||||
if not provider.campaign_exists(session, tenant_id=principal.tenant_id, campaign_id=campaign_id):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Campaign not found")
|
||||
if has_scope(principal, "tenant:*"):
|
||||
group_ids = set(user_group_ids(session, tenant_id=principal.tenant_id, user_id=principal.user.id))
|
||||
if provider.can_read_campaign(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
campaign_id=campaign_id,
|
||||
user_id=principal.user.id,
|
||||
group_ids=group_ids,
|
||||
tenant_admin=has_scope(principal, "tenant:*"),
|
||||
):
|
||||
return
|
||||
if campaign.owner_user_id == principal.user.id:
|
||||
return
|
||||
group_ids = {
|
||||
row[0]
|
||||
for row in session.query(UserGroupMembership.group_id)
|
||||
.filter(UserGroupMembership.tenant_id == principal.tenant_id, UserGroupMembership.user_id == principal.user.id)
|
||||
.all()
|
||||
}
|
||||
if campaign.owner_group_id and campaign.owner_group_id in group_ids:
|
||||
return
|
||||
share = (
|
||||
session.query(CampaignShare)
|
||||
.filter(
|
||||
CampaignShare.tenant_id == principal.tenant_id,
|
||||
CampaignShare.campaign_id == campaign.id,
|
||||
CampaignShare.revoked_at.is_(None),
|
||||
or_(
|
||||
(CampaignShare.target_type == "user") & (CampaignShare.target_id == principal.user.id),
|
||||
(CampaignShare.target_type == "group") & (CampaignShare.target_id.in_(group_ids)),
|
||||
),
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if share is None:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="No access to this campaign")
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="No access to this campaign")
|
||||
|
||||
|
||||
def _http_error(exc: Exception, *, not_found: bool = False) -> HTTPException:
|
||||
@@ -332,7 +314,7 @@ def list_file_spaces(
|
||||
]
|
||||
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 = session.query(Group).filter(Group.tenant_id == principal.tenant_id, Group.id.in_(group_ids)).order_by(Group.name.asc()).all()
|
||||
groups = group_refs_for_ids(tenant_id=principal.tenant_id, group_ids=group_ids)
|
||||
spaces.extend(
|
||||
FileSpaceResponse(
|
||||
id=f"group:{group.id}",
|
||||
|
||||
Reference in New Issue
Block a user