diff --git a/app/core/permission_registry.py b/app/core/permission_registry.py index d939b09..ec522a1 100644 --- a/app/core/permission_registry.py +++ b/app/core/permission_registry.py @@ -29,7 +29,9 @@ CORE_PERMISSIONS: list[dict[str, str]] = [ {"key": "groups:write", "label": "Groups: Write", "category": "core", "module": "groups"}, {"key": "groups:delete", "label": "Groups: Delete", "category": "core", "module": "groups"}, {"key": "audit:read", "label": "Audit Log: Read", "category": "core", "module": "audit"}, + {"key": "settings:read", "label": "Settings: Read", "category": "core", "module": "settings"}, {"key": "settings:write", "label": "Settings: Write", "category": "core", "module": "settings"}, + {"key": "plugins:read", "label": "Plugins: Read", "category": "core", "module": "plugins"}, {"key": "plugins:install", "label": "Plugins: Install", "category": "core", "module": "plugins"}, {"key": "plugins:configure", "label": "Plugins: Configure", "category": "core", "module": "plugins"}, {"key": "tenants:read", "label": "Tenants: Read", "category": "core", "module": "tenants"}, diff --git a/app/routes/addresses.py b/app/routes/addresses.py index de71811..d9bfb75 100644 --- a/app/routes/addresses.py +++ b/app/routes/addresses.py @@ -7,9 +7,8 @@ import uuid from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy.ext.asyncio import AsyncSession -from app.core.auth import check_permission from app.core.db import get_db -from app.deps import get_current_user +from app.deps import require_permission from app.schemas.address import AddressCreate, AddressUpdate from app.services import address_service @@ -21,7 +20,7 @@ async def list_addresses( entity_type: str = Query(..., pattern="^(company|contact)$"), entity_id: str = Query(...), db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("addresses:read")), ): """List all addresses for a given entity (company or contact).""" tenant_id = uuid.UUID(current_user["tenant_id"]) @@ -37,18 +36,12 @@ async def list_addresses( async def create_address( body: AddressCreate, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("addresses:write")), ): """Create a new address for a company or contact.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) - role = current_user.get("role", "viewer") - if not check_permission(role, "companies", "create") and not check_permission(role, "contacts", "create"): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail={"detail": "Insufficient permissions", "code": "forbidden"}, - ) data = body.model_dump() try: @@ -62,18 +55,12 @@ async def update_address( address_id: str, body: AddressUpdate, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("addresses:write")), ): """Update an address.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) - role = current_user.get("role", "viewer") - if not check_permission(role, "companies", "update") and not check_permission(role, "contacts", "update"): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail={"detail": "Insufficient permissions", "code": "forbidden"}, - ) try: aid = uuid.UUID(address_id) @@ -91,18 +78,12 @@ async def update_address( async def delete_address( address_id: str, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("addresses:write")), ): """Soft-delete an address.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) - role = current_user.get("role", "viewer") - if not check_permission(role, "companies", "delete") and not check_permission(role, "contacts", "delete"): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail={"detail": "Insufficient permissions", "code": "forbidden"}, - ) try: aid = uuid.UUID(address_id) diff --git a/app/routes/attachments.py b/app/routes/attachments.py index a713a8d..c805126 100644 --- a/app/routes/attachments.py +++ b/app/routes/attachments.py @@ -9,9 +9,8 @@ from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile, s from fastapi.responses import FileResponse from sqlalchemy.ext.asyncio import AsyncSession -from app.core.auth import check_permission from app.core.db import get_db -from app.deps import get_current_user +from app.deps import require_permission from app.services import attachment_service router = APIRouter(prefix="/api/v1/attachments", tags=["attachments"]) @@ -23,18 +22,12 @@ async def upload_attachment( entity_type: str = Form(...), entity_id: str = Form(...), db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("attachments:write")), ): """Upload a file attachment. Multipart form data.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) - role = current_user.get("role", "viewer") - if not check_permission(role, "companies", "update"): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail={"detail": "Insufficient permissions", "code": "forbidden"}, - ) try: eid = uuid.UUID(entity_id) @@ -55,7 +48,7 @@ async def list_attachments( entity_type: str, entity_id: str, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("attachments:read")), ): """List attachments for a specific entity.""" tenant_id = uuid.UUID(current_user["tenant_id"]) @@ -72,7 +65,7 @@ async def list_attachments( async def download_attachment( attachment_id: str, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("attachments:read")), ): """Download an attachment file.""" tenant_id = uuid.UUID(current_user["tenant_id"]) @@ -101,18 +94,12 @@ async def download_attachment( async def delete_attachment( attachment_id: str, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("attachments:write")), ): """Delete an attachment.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) - role = current_user.get("role", "viewer") - if not check_permission(role, "companies", "delete"): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail={"detail": "Insufficient permissions", "code": "forbidden"}, - ) try: aid = uuid.UUID(attachment_id) diff --git a/app/routes/audit.py b/app/routes/audit.py index 60e722e..c45f7c2 100644 --- a/app/routes/audit.py +++ b/app/routes/audit.py @@ -10,7 +10,7 @@ from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession from app.core.db import get_db -from app.deps import get_current_user +from app.deps import require_permission from app.models.audit import AuditLog router = APIRouter(prefix="/api/v1/audit-log", tags=["audit"]) @@ -40,14 +40,9 @@ async def list_audit_logs( page: int = Query(1, ge=1, description="Page number"), page_size: int = Query(50, ge=1, le=200, description="Items per page"), db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("audit:read")), ): """List audit log entries with filtering and pagination. Admin only.""" - if current_user.get("role") != "admin": - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail={"detail": "Admin access required", "code": "forbidden"}, - ) tenant_id = uuid.UUID(current_user["tenant_id"]) diff --git a/app/routes/companies.py b/app/routes/companies.py index 1b5ccbf..7fcf0e0 100644 --- a/app/routes/companies.py +++ b/app/routes/companies.py @@ -7,9 +7,8 @@ import uuid from fastapi import APIRouter, Depends, HTTPException, Query, Response, status from sqlalchemy.ext.asyncio import AsyncSession -from app.core.auth import check_permission from app.core.db import get_db -from app.deps import get_current_user +from app.deps import require_permission from app.schemas.company import CompanyCreate, CompanyUpdate from app.services import company_service @@ -25,7 +24,7 @@ async def list_companies( sort_by: str = Query("name"), sort_order: str = Query("asc", pattern="^(asc|desc)$"), db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("companies:read")), ): """List companies with pagination, FTS search, industry filter, and sorting.""" tenant_id = uuid.UUID(current_user["tenant_id"]) @@ -46,18 +45,12 @@ async def list_companies( async def create_company( body: CompanyCreate, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("companies:write")), ): """Create a company. Requires write permission.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) - role = current_user.get("role", "viewer") - if not check_permission(role, "companies", "create"): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail={"detail": "Insufficient permissions", "code": "forbidden"}, - ) data = body.model_dump() return await company_service.create_company(db, tenant_id, user_id, data) @@ -69,7 +62,7 @@ async def export_companies( industry: str | None = Query(None), search: str | None = Query(None), db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("companies:read")), ): """Export companies as CSV (streaming) or XLSX (buffered).""" tenant_id = uuid.UUID(current_user["tenant_id"]) @@ -155,7 +148,7 @@ async def export_companies( async def get_company( company_id: str, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("companies:read")), ): """Get a single company with contacts array. Cross-tenant returns 404.""" tenant_id = uuid.UUID(current_user["tenant_id"]) @@ -177,15 +170,12 @@ async def update_company( company_id: str, body: CompanyUpdate, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("companies:write")), ): """Update a company (full PUT).""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) - role = current_user.get("role", "viewer") - if not check_permission(role, "companies", "update"): - raise HTTPException(403, detail={"detail": "Insufficient permissions", "code": "forbidden"}) try: cid = uuid.UUID(company_id) @@ -206,15 +196,12 @@ async def delete_company( company_id: str, cascade: bool = Query(False), db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("companies:write")), ): """Soft-delete a company. Use cascade=true to also delete N:M links.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) - role = current_user.get("role", "viewer") - if not check_permission(role, "companies", "delete"): - raise HTTPException(403, detail={"detail": "Insufficient permissions", "code": "forbidden"}) try: cid = uuid.UUID(company_id) @@ -237,15 +224,12 @@ async def link_company_contact( company_id: str, contact_id: str, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("companies:write")), ): """Link a contact to a company (N:M).""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) - role = current_user.get("role", "viewer") - if not check_permission(role, "companies", "update"): - raise HTTPException(403, detail={"detail": "Insufficient permissions", "code": "forbidden"}) try: comp_id = uuid.UUID(company_id) @@ -266,15 +250,12 @@ async def unlink_company_contact( company_id: str, contact_id: str, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("companies:write")), ): """Unlink a contact from a company (N:M).""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) - role = current_user.get("role", "viewer") - if not check_permission(role, "companies", "update"): - raise HTTPException(403, detail={"detail": "Insufficient permissions", "code": "forbidden"}) try: comp_id = uuid.UUID(company_id) @@ -293,7 +274,7 @@ async def unlink_company_contact( async def get_company_emails( company_id: str, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("companies:read")), ): """Get emails for a company (mail plugin inactive — returns empty array).""" tenant_id = uuid.UUID(current_user["tenant_id"]) diff --git a/app/routes/contacts.py b/app/routes/contacts.py index f8b3bb4..3cdac69 100644 --- a/app/routes/contacts.py +++ b/app/routes/contacts.py @@ -11,9 +11,8 @@ from fastapi.responses import StreamingResponse from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from app.core.auth import check_permission from app.core.db import get_db -from app.deps import get_current_user +from app.deps import require_permission from app.models.contact import Contact from app.schemas.contact import ContactCreate, ContactUpdate from app.services import contact_service @@ -29,7 +28,7 @@ async def list_contacts( sort_by: str = Query("last_name"), sort_order: str = Query("asc", pattern="^(asc|desc)$"), db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("contacts:read")), ): """List contacts with pagination and optional search. @@ -53,7 +52,7 @@ async def export_contacts( format: str = Query("csv", pattern="^(csv)$"), search: str | None = Query(None), db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("contacts:read")), ): """Stream contacts as CSV (not buffered — uses StreamingResponse). @@ -148,18 +147,12 @@ async def export_contacts( async def create_contact( body: ContactCreate, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("contacts:write")), ): """Create a contact. Optionally link to companies via company_ids array.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) - role = current_user.get("role", "viewer") - if not check_permission(role, "contacts", "create"): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail={"detail": "Insufficient permissions", "code": "forbidden"}, - ) data = body.model_dump() return await contact_service.create_contact(db, tenant_id, user_id, data) @@ -169,7 +162,7 @@ async def create_contact( async def get_contact( contact_id: str, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("contacts:read")), ): """Get a single contact with companies array. Cross-tenant returns 404.""" tenant_id = uuid.UUID(current_user["tenant_id"]) @@ -191,15 +184,12 @@ async def update_contact( contact_id: str, body: ContactUpdate, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("contacts:write")), ): """Update a contact.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) - role = current_user.get("role", "viewer") - if not check_permission(role, "contacts", "update"): - raise HTTPException(403, detail={"detail": "Insufficient permissions", "code": "forbidden"}) try: cid = uuid.UUID(contact_id) @@ -220,15 +210,12 @@ async def delete_contact( contact_id: str, gdpr: bool = Query(False), db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("contacts:write")), ): """Delete a contact. Default: soft-delete. Use gdpr=true for hard-delete with deletion_log.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) - role = current_user.get("role", "viewer") - if not check_permission(role, "contacts", "delete"): - raise HTTPException(403, detail={"detail": "Insufficient permissions", "code": "forbidden"}) try: cid = uuid.UUID(contact_id) diff --git a/app/routes/currencies.py b/app/routes/currencies.py index dac9354..c49a348 100644 --- a/app/routes/currencies.py +++ b/app/routes/currencies.py @@ -7,9 +7,8 @@ import uuid from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession -from app.core.auth import check_permission from app.core.db import get_db -from app.deps import get_current_user +from app.deps import require_permission from app.schemas.currency import CurrencyCreate, CurrencyUpdate from app.services import currency_service @@ -19,7 +18,7 @@ router = APIRouter(prefix="/api/v1/currencies", tags=["currencies"]) @router.get("") async def list_currencies( db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("currencies:read")), ): """List all currencies for the current tenant.""" tenant_id = uuid.UUID(current_user["tenant_id"]) @@ -30,18 +29,12 @@ async def list_currencies( async def create_currency( body: CurrencyCreate, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("currencies:write")), ): """Create a currency. Requires admin permission.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) - role = current_user.get("role", "viewer") - if not check_permission(role, "settings", "create"): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail={"detail": "Insufficient permissions", "code": "forbidden"}, - ) data = body.model_dump() return await currency_service.create_currency(db, tenant_id, user_id, data) @@ -52,18 +45,12 @@ async def update_currency( currency_id: str, body: CurrencyUpdate, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("currencies:write")), ): """Update a currency. Requires admin permission.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) - role = current_user.get("role", "viewer") - if not check_permission(role, "settings", "update"): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail={"detail": "Insufficient permissions", "code": "forbidden"}, - ) try: cid = uuid.UUID(currency_id) @@ -81,18 +68,12 @@ async def update_currency( async def delete_currency( currency_id: str, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("currencies:write")), ): """Delete a currency. Requires admin permission.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) - role = current_user.get("role", "viewer") - if not check_permission(role, "settings", "delete"): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail={"detail": "Insufficient permissions", "code": "forbidden"}, - ) try: cid = uuid.UUID(currency_id) diff --git a/app/routes/import_export.py b/app/routes/import_export.py index 3005473..031ecd8 100644 --- a/app/routes/import_export.py +++ b/app/routes/import_export.py @@ -14,9 +14,8 @@ from fastapi import ( ) from sqlalchemy.ext.asyncio import AsyncSession -from app.core.auth import check_permission from app.core.db import get_db -from app.deps import get_current_user +from app.deps import require_permission from app.services import import_export_service router = APIRouter(prefix="/api/v1", tags=["import_export"]) @@ -27,17 +26,14 @@ async def import_csv( file: UploadFile = File(...), entity_type: str = Form("companies"), db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("import_export:write")), ): """Import companies or contacts from CSV file. entity_type: 'companies' or 'contacts'. """ tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) - role = current_user.get("role", "viewer") - if not check_permission(role, "companies", "create"): - raise HTTPException(403, detail={"detail": "Insufficient permissions", "code": "forbidden"}) content = await file.read() csv_content = content.decode("utf-8") @@ -58,15 +54,12 @@ async def import_csv_preview( file: UploadFile = File(...), entity_type: str = Form("companies"), db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("import_export:read")), ): """Preview CSV import (dry-run — no DB changes).""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) - role = current_user.get("role", "viewer") - if not check_permission(role, "companies", "read"): - raise HTTPException(403, detail={"detail": "Insufficient permissions", "code": "forbidden"}) content = await file.read() csv_content = content.decode("utf-8") diff --git a/app/routes/metrics.py b/app/routes/metrics.py index 9e8d635..9da72ed 100644 --- a/app/routes/metrics.py +++ b/app/routes/metrics.py @@ -6,7 +6,7 @@ from fastapi import APIRouter, Depends from fastapi.responses import PlainTextResponse from app.core.monitoring import generate_metrics -from app.deps import require_admin +from app.deps import get_current_user router = APIRouter(tags=["metrics"]) @@ -14,7 +14,7 @@ router = APIRouter(tags=["metrics"]) @router.get( "/api/v1/metrics", response_class=PlainTextResponse, - dependencies=[Depends(require_admin)], + dependencies=[Depends(get_current_user)], ) async def metrics(): """Prometheus metrics endpoint. diff --git a/app/routes/notifications.py b/app/routes/notifications.py index 4d2120b..8863be6 100644 --- a/app/routes/notifications.py +++ b/app/routes/notifications.py @@ -14,7 +14,7 @@ from app.core.notifications import ( list_notifications, mark_notification_read, ) -from app.deps import get_current_user +from app.deps import require_permission from app.models.notification import ( NotificationPreference, NotificationType, @@ -29,7 +29,7 @@ async def list_notifications_endpoint( page: int = Query(1, ge=1), page_size: int = Query(25, ge=1, le=100), db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("notifications:read")), ): """List notifications (unread first).""" tenant_id = uuid.UUID(current_user["tenant_id"]) @@ -41,7 +41,7 @@ async def list_notifications_endpoint( async def mark_notification_read_endpoint( notification_id: str, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("notifications:write")), ): """Mark a notification as read.""" tenant_id = uuid.UUID(current_user["tenant_id"]) @@ -70,7 +70,7 @@ async def mark_notification_read_endpoint( @router.get("/unread-count") async def unread_count_endpoint( db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("notifications:read")), ): """Get unread notification count.""" tenant_id = uuid.UUID(current_user["tenant_id"]) @@ -82,7 +82,7 @@ async def unread_count_endpoint( @router.get("/types") async def list_notification_types( db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("notifications:read")), ): """List all registered notification types with the user's preference.""" tenant_id = uuid.UUID(current_user["tenant_id"]) @@ -125,7 +125,7 @@ async def list_notification_types( @router.get("/preferences") async def list_preferences( db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("notifications:read")), ): """List user's notification preferences.""" tenant_id = uuid.UUID(current_user["tenant_id"]) @@ -154,7 +154,7 @@ async def update_preference( type_key: str, data: NotificationPreferenceUpdate, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("notifications:write")), ): """Update user's preference for a notification type (upsert).""" tenant_id = uuid.UUID(current_user["tenant_id"]) diff --git a/app/routes/plugins.py b/app/routes/plugins.py index 4633b10..4f84d88 100644 --- a/app/routes/plugins.py +++ b/app/routes/plugins.py @@ -7,7 +7,7 @@ from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession from app.core.db import get_db -from app.deps import require_admin +from app.deps import require_permission from app.plugins.migration_runner import MigrationValidationError from app.services.plugin_service import get_plugin_service @@ -22,7 +22,7 @@ class PluginConfigUpdate(BaseModel): @router.get("") async def list_plugins( db: AsyncSession = Depends(get_db), - current_user: dict = Depends(require_admin), + current_user: dict = Depends(require_permission("plugins:read")), ): """List all plugins with their current status (discovered, installed, active, inactive).""" service = get_plugin_service() @@ -32,7 +32,7 @@ async def list_plugins( @router.get("/manifest") async def get_manifest_schema( - current_user: dict = Depends(require_admin), + current_user: dict = Depends(require_permission("plugins:read")), ): """Get the plugin manifest schema documentation.""" service = get_plugin_service() @@ -43,7 +43,7 @@ async def get_manifest_schema( async def get_plugin_config( name: str, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(require_admin), + current_user: dict = Depends(require_permission("plugins:read")), ): """Get the configuration for a specific plugin. @@ -64,7 +64,7 @@ async def update_plugin_config( name: str, body: PluginConfigUpdate, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(require_admin), + current_user: dict = Depends(require_permission("plugins:configure")), ): """Update the configuration for a specific plugin. @@ -92,7 +92,7 @@ async def update_plugin_config( async def install_plugin( name: str, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(require_admin), + current_user: dict = Depends(require_permission("plugins:configure")), ): """Install a plugin by name. Runs migrations and creates DB record. @@ -125,7 +125,7 @@ async def install_plugin( async def activate_plugin( name: str, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(require_admin), + current_user: dict = Depends(require_permission("plugins:configure")), ): """Activate a plugin by name. Registers event listeners and routes. @@ -158,7 +158,7 @@ async def activate_plugin( async def deactivate_plugin( name: str, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(require_admin), + current_user: dict = Depends(require_permission("plugins:configure")), ): """Deactivate a plugin by name. Unregisters event listeners and routes. @@ -188,7 +188,7 @@ async def uninstall_plugin( name: str, remove_data: bool = Query(False), db: AsyncSession = Depends(get_db), - current_user: dict = Depends(require_admin), + current_user: dict = Depends(require_permission("plugins:configure")), ): """Uninstall a plugin. Optionally drop plugin-created tables with remove_data=true. diff --git a/app/routes/roles.py b/app/routes/roles.py index ed85bf9..6349681 100644 --- a/app/routes/roles.py +++ b/app/routes/roles.py @@ -13,7 +13,7 @@ from app.core.audit import log_audit from app.core.auth import get_redis from app.core.db import get_db from app.core.permissions import invalidate_all_user_permissions -from app.deps import get_current_user, require_admin +from app.deps import require_permission from app.models.plugin import Plugin as PluginModel from app.plugins.registry import get_registry from app.schemas.role import RoleCreate, RoleUpdate @@ -25,23 +25,53 @@ router = APIRouter(prefix="/api/v1/roles", tags=["roles"]) SYSTEM_PERMISSIONS: list[dict[str, str]] = [ {"key": "companies:read", "label": "Companies: Read", "category": "system"}, {"key": "companies:write", "label": "Companies: Write", "category": "system"}, + {"key": "companies:delete", "label": "Companies: Delete", "category": "system"}, {"key": "contacts:read", "label": "Contacts: Read", "category": "system"}, {"key": "contacts:write", "label": "Contacts: Write", "category": "system"}, + {"key": "contacts:delete", "label": "Contacts: Delete", "category": "system"}, {"key": "users:read", "label": "Users: Read", "category": "system"}, {"key": "users:write", "label": "Users: Write", "category": "system"}, - {"key": "audit:read", "label": "Audit Log: Read", "category": "system"}, - {"key": "settings:write", "label": "Settings: Write", "category": "system"}, - {"key": "plugins:install", "label": "Plugins: Install", "category": "system"}, - {"key": "plugins:configure", "label": "Plugins: Configure", "category": "system"}, + {"key": "users:delete", "label": "Users: Delete", "category": "system"}, {"key": "roles:read", "label": "Roles: Read", "category": "system"}, {"key": "roles:write", "label": "Roles: Write", "category": "system"}, + {"key": "roles:delete", "label": "Roles: Delete", "category": "system"}, + {"key": "groups:read", "label": "Groups: Read", "category": "system"}, + {"key": "groups:write", "label": "Groups: Write", "category": "system"}, + {"key": "groups:delete", "label": "Groups: Delete", "category": "system"}, + {"key": "audit:read", "label": "Audit Log: Read", "category": "system"}, + {"key": "settings:read", "label": "Settings: Read", "category": "system"}, + {"key": "settings:write", "label": "Settings: Write", "category": "system"}, + {"key": "plugins:read", "label": "Plugins: Read", "category": "system"}, + {"key": "plugins:install", "label": "Plugins: Install", "category": "system"}, + {"key": "plugins:configure", "label": "Plugins: Configure", "category": "system"}, + {"key": "tenants:read", "label": "Tenants: Read", "category": "system"}, + {"key": "tenants:write", "label": "Tenants: Write", "category": "system"}, + {"key": "tenants:delete", "label": "Tenants: Delete", "category": "system"}, + {"key": "notifications:read", "label": "Notifications: Read", "category": "system"}, + {"key": "notifications:write", "label": "Notifications: Write", "category": "system"}, + {"key": "attachments:read", "label": "Attachments: Read", "category": "system"}, + {"key": "attachments:write", "label": "Attachments: Write", "category": "system"}, + {"key": "attachments:delete", "label": "Attachments: Delete", "category": "system"}, + {"key": "workflows:read", "label": "Workflows: Read", "category": "system"}, + {"key": "workflows:write", "label": "Workflows: Write", "category": "system"}, + {"key": "sequences:read", "label": "Sequences: Read", "category": "system"}, + {"key": "sequences:write", "label": "Sequences: Write", "category": "system"}, + {"key": "addresses:read", "label": "Addresses: Read", "category": "system"}, + {"key": "addresses:write", "label": "Addresses: Write", "category": "system"}, + {"key": "addresses:delete", "label": "Addresses: Delete", "category": "system"}, + {"key": "taxes:read", "label": "Taxes: Read", "category": "system"}, + {"key": "taxes:write", "label": "Taxes: Write", "category": "system"}, + {"key": "currencies:read", "label": "Currencies: Read", "category": "system"}, + {"key": "currencies:write", "label": "Currencies: Write", "category": "system"}, + {"key": "import_export:read", "label": "Import/Export: Read", "category": "system"}, + {"key": "import_export:write", "label": "Import/Export: Write", "category": "system"}, ] @router.get("/permissions") async def list_permissions( db: AsyncSession = Depends(get_db), - current_user: dict = Depends(require_admin), + current_user: dict = Depends(require_permission("roles:read")), ): """List all available permissions (system + active plugin permissions). @@ -84,7 +114,7 @@ async def list_permissions( @router.get("") async def list_roles( db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("roles:read")), ): """List roles for the current tenant.""" tenant_id = uuid.UUID(current_user["tenant_id"]) @@ -96,7 +126,7 @@ async def list_roles( async def create_role( body: RoleCreate, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(require_admin), + current_user: dict = Depends(require_permission("roles:write")), ): """Create a custom role (admin only).""" tenant_id = uuid.UUID(current_user["tenant_id"]) @@ -123,7 +153,7 @@ async def update_role( role_id: str, body: RoleUpdate, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(require_admin), + current_user: dict = Depends(require_permission("roles:write")), ): """Update a role (admin only).""" tenant_id = uuid.UUID(current_user["tenant_id"]) @@ -168,7 +198,7 @@ async def update_role( async def delete_role( role_id: str, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(require_admin), + current_user: dict = Depends(require_permission("roles:write")), ): """Delete a role (admin only).""" tenant_id = uuid.UUID(current_user["tenant_id"]) diff --git a/app/routes/sequences.py b/app/routes/sequences.py index 1952ff7..02bb4d5 100644 --- a/app/routes/sequences.py +++ b/app/routes/sequences.py @@ -7,9 +7,8 @@ import uuid from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession -from app.core.auth import check_permission from app.core.db import get_db -from app.deps import get_current_user +from app.deps import require_permission from app.schemas.sequence import SequenceCreate, SequenceUpdate from app.services import sequence_service @@ -19,17 +18,11 @@ router = APIRouter(prefix="/api/v1/sequences", tags=["sequences"]) @router.get("") async def list_sequences( db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("sequences:read")), ): """List all sequences for the current tenant. Admin only.""" tenant_id = uuid.UUID(current_user["tenant_id"]) - role = current_user.get("role", "viewer") - if not check_permission(role, "settings", "read"): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail={"detail": "Insufficient permissions", "code": "forbidden"}, - ) return await sequence_service.list_sequences(db, tenant_id) @@ -38,18 +31,12 @@ async def list_sequences( async def create_sequence( body: SequenceCreate, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("sequences:write")), ): """Create a new sequence. Admin only.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) - role = current_user.get("role", "viewer") - if not check_permission(role, "settings", "create"): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail={"detail": "Insufficient permissions", "code": "forbidden"}, - ) data = body.model_dump() return await sequence_service.create_sequence(db, tenant_id, user_id, data) @@ -60,18 +47,12 @@ async def update_sequence( sequence_id: str, body: SequenceUpdate, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("sequences:write")), ): """Update a sequence (name, prefix, padding only). Admin only.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) - role = current_user.get("role", "viewer") - if not check_permission(role, "settings", "update"): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail={"detail": "Insufficient permissions", "code": "forbidden"}, - ) try: sid = uuid.UUID(sequence_id) @@ -89,18 +70,12 @@ async def update_sequence( async def delete_sequence( sequence_id: str, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("sequences:write")), ): """Delete a sequence (soft-delete). Admin only.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) - role = current_user.get("role", "viewer") - if not check_permission(role, "settings", "delete"): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail={"detail": "Insufficient permissions", "code": "forbidden"}, - ) try: sid = uuid.UUID(sequence_id) diff --git a/app/routes/system_settings.py b/app/routes/system_settings.py index 5d1feb4..356248b 100644 --- a/app/routes/system_settings.py +++ b/app/routes/system_settings.py @@ -7,9 +7,8 @@ import uuid from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession -from app.core.auth import check_permission from app.core.db import get_db -from app.deps import get_current_user +from app.deps import require_permission from app.schemas.system_settings import SystemSettingsUpsert from app.services import system_settings_service @@ -19,7 +18,7 @@ router = APIRouter(prefix="/api/v1/system-settings", tags=["system-settings"]) @router.get("") async def get_system_settings( db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("settings:read")), ): """Get system settings for the current tenant.""" tenant_id = uuid.UUID(current_user["tenant_id"]) @@ -33,18 +32,12 @@ async def get_system_settings( async def upsert_system_settings( body: SystemSettingsUpsert, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("settings:write")), ): """Upsert system settings. Requires admin permission.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) - role = current_user.get("role", "viewer") - if not check_permission(role, "settings", "update"): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail={"detail": "Insufficient permissions", "code": "forbidden"}, - ) data = body.model_dump() return await system_settings_service.upsert_system_settings(db, tenant_id, user_id, data) diff --git a/app/routes/taxes.py b/app/routes/taxes.py index 189f314..e110b3d 100644 --- a/app/routes/taxes.py +++ b/app/routes/taxes.py @@ -7,9 +7,8 @@ import uuid from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession -from app.core.auth import check_permission from app.core.db import get_db -from app.deps import get_current_user +from app.deps import require_permission from app.schemas.tax import TaxRateCreate, TaxRateUpdate from app.services import tax_service @@ -19,7 +18,7 @@ router = APIRouter(prefix="/api/v1/taxes", tags=["taxes"]) @router.get("") async def list_tax_rates( db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("taxes:read")), ): """List all tax rates for the current tenant.""" tenant_id = uuid.UUID(current_user["tenant_id"]) @@ -30,18 +29,12 @@ async def list_tax_rates( async def create_tax_rate( body: TaxRateCreate, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("taxes:write")), ): """Create a tax rate. Requires admin permission.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) - role = current_user.get("role", "viewer") - if not check_permission(role, "settings", "create"): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail={"detail": "Insufficient permissions", "code": "forbidden"}, - ) data = body.model_dump() return await tax_service.create_tax_rate(db, tenant_id, user_id, data) @@ -52,18 +45,12 @@ async def update_tax_rate( tax_id: str, body: TaxRateUpdate, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("taxes:write")), ): """Update a tax rate. Requires admin permission.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) - role = current_user.get("role", "viewer") - if not check_permission(role, "settings", "update"): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail={"detail": "Insufficient permissions", "code": "forbidden"}, - ) try: tid = uuid.UUID(tax_id) @@ -81,18 +68,12 @@ async def update_tax_rate( async def delete_tax_rate( tax_id: str, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("taxes:write")), ): """Delete a tax rate. Requires admin permission.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) - role = current_user.get("role", "viewer") - if not check_permission(role, "settings", "delete"): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail={"detail": "Insufficient permissions", "code": "forbidden"}, - ) try: tid = uuid.UUID(tax_id) diff --git a/app/routes/tenants.py b/app/routes/tenants.py index ade4c4b..87649dc 100644 --- a/app/routes/tenants.py +++ b/app/routes/tenants.py @@ -8,7 +8,7 @@ from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession from app.core.db import get_db -from app.deps import get_current_user, require_admin +from app.deps import require_permission from app.schemas.tenant import TenantCreate, TenantUserAssign from app.services.tenant_service import tenant_service @@ -18,7 +18,7 @@ router = APIRouter(prefix="/api/v1/tenants", tags=["tenants"]) @router.get("") async def list_tenants( db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("tenants:read")), ): """List tenants for the current user.""" user_id = uuid.UUID(current_user["user_id"]) @@ -30,7 +30,7 @@ async def list_tenants( async def create_tenant( body: TenantCreate, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(require_admin), + current_user: dict = Depends(require_permission("tenants:write")), ): """Create a new tenant (admin only).""" tenant = await tenant_service.create_tenant(db, body.name, body.slug) @@ -45,7 +45,7 @@ async def create_tenant( async def list_tenant_users( tenant_id: str, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(require_admin), + current_user: dict = Depends(require_permission("tenants:write")), ): """List users in a tenant (admin only).""" try: @@ -64,7 +64,7 @@ async def assign_user_to_tenant( tenant_id: str, body: TenantUserAssign, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(require_admin), + current_user: dict = Depends(require_permission("tenants:write")), ): """Assign a user to a tenant (admin only).""" try: diff --git a/app/routes/users.py b/app/routes/users.py index e5b4dd2..be24b2c 100644 --- a/app/routes/users.py +++ b/app/routes/users.py @@ -13,7 +13,7 @@ from app.core.auth import get_redis from app.core.db import get_db from app.core.notifications import create_notification from app.core.permissions import invalidate_permission_cache -from app.deps import get_current_user, require_admin +from app.deps import require_permission from app.schemas.user import UserCreate, UserUpdate from app.services.user_service import user_service, _UNSET @@ -42,7 +42,7 @@ async def list_users( page_size: int = Query(25, ge=1, le=100), search: str | None = Query(None), db: AsyncSession = Depends(get_db), - current_user: dict = Depends(require_admin), + current_user: dict = Depends(require_permission("users:read")), ): """List users (admin only, paginated).""" tenant_id = uuid.UUID(current_user["tenant_id"]) @@ -53,7 +53,7 @@ async def list_users( async def create_user( body: UserCreate, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(require_admin), + current_user: dict = Depends(require_permission("users:write")), ): """Create a new user (admin only).""" tenant_id = uuid.UUID(current_user["tenant_id"]) @@ -107,7 +107,7 @@ async def create_user( async def get_user( user_id: str, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("users:read")), ): """Get a single user.""" tenant_id = uuid.UUID(current_user["tenant_id"]) @@ -138,7 +138,7 @@ async def update_user( user_id: str, body: UserUpdate, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(require_admin), + current_user: dict = Depends(require_permission("users:write")), ): """Update a user (admin only). @@ -220,7 +220,7 @@ async def update_user( async def delete_user( user_id: str, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(require_admin), + current_user: dict = Depends(require_permission("users:write")), ): """Delete a user (admin only).""" tenant_id = uuid.UUID(current_user["tenant_id"]) diff --git a/app/routes/workflows.py b/app/routes/workflows.py index 9ad715b..f9bffaa 100644 --- a/app/routes/workflows.py +++ b/app/routes/workflows.py @@ -7,9 +7,8 @@ import uuid from fastapi import APIRouter, Depends, HTTPException, Query, Response, status from sqlalchemy.ext.asyncio import AsyncSession -from app.core.auth import check_permission from app.core.db import get_db -from app.deps import get_current_user +from app.deps import require_permission from app.schemas.workflow import AdvanceRequest, InstanceCreate, WorkflowCreate, WorkflowUpdate from app.services import workflow_service @@ -25,7 +24,7 @@ async def list_workflows( page_size: int = Query(20, ge=1, le=100), is_active: bool | None = Query(None), db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("workflows:read")), ): """List workflows with pagination.""" tenant_id = uuid.UUID(current_user["tenant_id"]) @@ -42,18 +41,12 @@ async def list_workflows( async def create_workflow( body: WorkflowCreate, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("workflows:write")), ): """Create a new workflow definition. Requires write permission.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) - role = current_user.get("role", "viewer") - if not check_permission(role, "workflows", "create"): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail={"detail": "Insufficient permissions", "code": "forbidden"}, - ) data = body.model_dump() return await workflow_service.create_workflow(db, tenant_id, user_id, data) @@ -65,7 +58,7 @@ async def list_instances( page_size: int = Query(20, ge=1, le=100), status: str | None = Query(None), db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("workflows:read")), ): """List workflow instances with optional status filter.""" tenant_id = uuid.UUID(current_user["tenant_id"]) @@ -82,7 +75,7 @@ async def list_instances( async def get_workflow( workflow_id: str, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("workflows:read")), ): """Get a single workflow by ID.""" tenant_id = uuid.UUID(current_user["tenant_id"]) @@ -100,18 +93,12 @@ async def update_workflow( workflow_id: str, body: WorkflowUpdate, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("workflows:write")), ): """Update a workflow definition.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) - role = current_user.get("role", "viewer") - if not check_permission(role, "workflows", "update"): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail={"detail": "Insufficient permissions", "code": "forbidden"}, - ) data = body.model_dump(exclude_unset=True) result = await workflow_service.update_workflow(db, tenant_id, user_id, workflow_id, data) @@ -127,18 +114,12 @@ async def update_workflow( async def delete_workflow( workflow_id: str, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("workflows:write")), ): """Delete a workflow definition.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) - role = current_user.get("role", "viewer") - if not check_permission(role, "workflows", "delete"): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail={"detail": "Insufficient permissions", "code": "forbidden"}, - ) deleted = await workflow_service.delete_workflow(db, tenant_id, user_id, workflow_id) if not deleted: @@ -157,7 +138,7 @@ async def create_instance( workflow_id: str, body: InstanceCreate, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("workflows:write")), ): """Create a new workflow instance.""" tenant_id = uuid.UUID(current_user["tenant_id"]) @@ -183,7 +164,7 @@ async def create_instance( async def get_instance( instance_id: str, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("workflows:read")), ): """Get a workflow instance with step history.""" tenant_id = uuid.UUID(current_user["tenant_id"]) @@ -201,7 +182,7 @@ async def advance_instance( instance_id: str, body: AdvanceRequest, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("workflows:write")), ): """Advance or reject a workflow instance step. @@ -236,7 +217,7 @@ async def advance_instance( async def cancel_instance( instance_id: str, db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user), + current_user: dict = Depends(require_permission("workflows:write")), ): """Cancel a workflow instance. Returns 200 with cancelled instance.""" tenant_id = uuid.UUID(current_user["tenant_id"])