Enforce partner ownership on partner dashboard
This commit is contained in:
@@ -7,6 +7,7 @@ from typing import Any
|
||||
from sqlalchemy import func, or_, select
|
||||
from sqlalchemy.orm import Session, selectinload
|
||||
|
||||
from app.modules.clients.association_models import ClientAssociation
|
||||
from app.modules.clients.models import Client
|
||||
from app.modules.alerts.workflow_escalations import list_workflow_escalations
|
||||
from app.modules.core.iam.models import User
|
||||
@@ -89,6 +90,41 @@ def _financial_year(request) -> str | None:
|
||||
return value or None
|
||||
|
||||
|
||||
def _is_scoped_partner(roles: set[str]) -> bool:
|
||||
"""Return True only for a normal Partner login.
|
||||
|
||||
System Admin and Firm Admin retain their existing administrative scope. A
|
||||
normal Partner is always ownership-scoped, even when a branch is selected.
|
||||
"""
|
||||
return "Partner" in roles and "System Admin" not in roles and "Firm Admin" not in roles
|
||||
|
||||
|
||||
def _partner_client_access_expression(*, partner_user_id: int, tenant_id: int | None):
|
||||
association_access = exists(
|
||||
select(ClientAssociation.id).where(
|
||||
ClientAssociation.client_id == Client.id,
|
||||
ClientAssociation.partner_user_id == int(partner_user_id),
|
||||
or_(
|
||||
ClientAssociation.firm_tenant_id == tenant_id,
|
||||
ClientAssociation.firm_tenant_id.is_(None),
|
||||
) if tenant_id is not None else ClientAssociation.id.is_not(None),
|
||||
)
|
||||
)
|
||||
review_access = exists(
|
||||
select(ClientServiceSubscription.id).where(
|
||||
ClientServiceSubscription.client_id == Client.id,
|
||||
ClientServiceSubscription.review_partner_user_id == int(partner_user_id),
|
||||
ClientServiceSubscription.is_active.is_(True),
|
||||
ClientServiceSubscription.tenant_id == tenant_id if tenant_id is not None else ClientServiceSubscription.id.is_not(None),
|
||||
)
|
||||
)
|
||||
return or_(
|
||||
Client.partner_id == int(partner_user_id),
|
||||
association_access,
|
||||
review_access,
|
||||
)
|
||||
|
||||
|
||||
def _task_scope(stmt, tenant_id: int | None, branch_id: int | None, current_user, roles: set[str], fy: str | None = None):
|
||||
if tenant_id:
|
||||
stmt = stmt.where(ClientServiceTaskInstance.tenant_id == tenant_id)
|
||||
@@ -97,7 +133,7 @@ def _task_scope(stmt, tenant_id: int | None, branch_id: int | None, current_user
|
||||
stmt = stmt.where(ClientServiceTaskInstance.branch_id == branch_id)
|
||||
if fy:
|
||||
stmt = stmt.where(ClientServiceTaskInstance.financial_year == fy)
|
||||
if "Partner" in roles and "System Admin" not in roles and branch_id is None:
|
||||
if _is_scoped_partner(roles):
|
||||
stmt = stmt.where(
|
||||
ClientServiceTaskInstance.subscription.has(
|
||||
or_(
|
||||
@@ -117,7 +153,7 @@ def _subscription_scope(stmt, tenant_id: int | None, branch_id: int | None, curr
|
||||
stmt = stmt.where(ClientServiceSubscription.branch_id == branch_id)
|
||||
if fy:
|
||||
stmt = stmt.where(ClientServiceSubscription.financial_year == fy)
|
||||
if "Partner" in roles and "System Admin" not in roles and branch_id is None:
|
||||
if _is_scoped_partner(roles):
|
||||
stmt = stmt.where(
|
||||
or_(
|
||||
ClientServiceSubscription.assigned_partner_user_id == current_user.id,
|
||||
@@ -133,11 +169,11 @@ def _client_scope(stmt, tenant_id: int | None, branch_id: int | None, current_us
|
||||
stmt = stmt.where(Client.is_active.is_(True))
|
||||
if branch_id is not None:
|
||||
stmt = stmt.where(Client.branch_id == branch_id)
|
||||
elif "Partner" in roles and "System Admin" not in roles:
|
||||
if _is_scoped_partner(roles):
|
||||
stmt = stmt.where(
|
||||
or_(
|
||||
Client.partner_id == current_user.id,
|
||||
Client.default_review_partner_user_id == current_user.id,
|
||||
_partner_client_access_expression(
|
||||
partner_user_id=int(current_user.id),
|
||||
tenant_id=tenant_id,
|
||||
)
|
||||
)
|
||||
return stmt
|
||||
@@ -213,7 +249,7 @@ def _load_clients(db: Session, tenant_id: int | None, branch_id: int | None, cur
|
||||
"service_count": service_count,
|
||||
"task_count": task_count,
|
||||
"overdue_count": overdue_count,
|
||||
"href": f"/clients/{client.id}/edit",
|
||||
"href": f"/clients/{client.id}",
|
||||
})
|
||||
return out
|
||||
|
||||
@@ -258,7 +294,14 @@ def _staff_rows(db: Session, tenant_id: int | None, branch_id: int | None, tasks
|
||||
return rows[:25]
|
||||
|
||||
|
||||
def _billing_rows(db: Session, tenant_id: int | None, branch_id: int | None, fy: str | None) -> dict[str, Any]:
|
||||
def _billing_rows(
|
||||
db: Session,
|
||||
tenant_id: int | None,
|
||||
branch_id: int | None,
|
||||
fy: str | None,
|
||||
current_user,
|
||||
roles: set[str],
|
||||
) -> dict[str, Any]:
|
||||
if BillingInvoice is None or not tenant_id:
|
||||
return {"available": False, "invoices": [], "invoice_count": 0, "draft_count": 0, "outstanding": Decimal("0")}
|
||||
stmt = select(BillingInvoice).where(BillingInvoice.tenant_id == tenant_id)
|
||||
@@ -266,6 +309,15 @@ def _billing_rows(db: Session, tenant_id: int | None, branch_id: int | None, fy:
|
||||
stmt = stmt.where(BillingInvoice.branch_id == branch_id)
|
||||
if fy:
|
||||
stmt = stmt.where(BillingInvoice.financial_year == fy)
|
||||
if _is_scoped_partner(roles):
|
||||
allowed_client_ids = select(Client.id).where(
|
||||
Client.tenant_id == tenant_id,
|
||||
_partner_client_access_expression(
|
||||
partner_user_id=int(current_user.id),
|
||||
tenant_id=tenant_id,
|
||||
),
|
||||
)
|
||||
stmt = stmt.where(BillingInvoice.client_id.in_(allowed_client_ids))
|
||||
invoices = list(db.execute(stmt.order_by(BillingInvoice.invoice_date.desc(), BillingInvoice.id.desc()).limit(25)).scalars().all())
|
||||
outstanding = sum((_money(getattr(inv, "balance_amount", 0)) for inv in invoices), Decimal("0"))
|
||||
return {
|
||||
@@ -297,7 +349,7 @@ def build_partner_dashboard_payload(db: Session, request, current_user) -> dict[
|
||||
|
||||
clients = _load_clients(db, tenant_id, branch_id, current_user, roles)
|
||||
staff = _staff_rows(db, tenant_id, branch_id, tasks)
|
||||
billing = _billing_rows(db, tenant_id, branch_id, fy)
|
||||
billing = _billing_rows(db, tenant_id, branch_id, fy, current_user, roles)
|
||||
unified_escalations = list_workflow_escalations(db, tenant_id=tenant_id, branch_id=branch_id, assigned_to_user_id=current_user.id)
|
||||
|
||||
subscriptions_count = _count(db, _subscription_scope(select(func.count(ClientServiceSubscription.id)), tenant_id, branch_id, current_user, roles, fy)) if tenant_id else 0
|
||||
|
||||
Reference in New Issue
Block a user