Enforce partner client ownership and partner-scoped imports

This commit is contained in:
A R R R Associates
2026-07-29 06:05:10 +05:30
parent 83f4cd2d4f
commit a58ec7b807
9 changed files with 205 additions and 108 deletions
+48 -8
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
from math import ceil
from sqlalchemy import asc, case, desc, func, or_, select
from sqlalchemy import and_, asc, case, desc, exists, func, or_, select
from sqlalchemy.orm import Session
from app.core.security.passwords import hash_password
@@ -13,6 +13,7 @@ from app.modules.client_groups.models import ClientGroup
from app.modules.core.iam.models import User
from app.modules.core.rbac.models import Role, UserRole
from app.modules.core.tenancy.models import Branch, Tenant
from app.modules.services.models import ClientServiceSubscription
def _safe_sort(sort_by: str, sort_order: str):
@@ -28,6 +29,7 @@ def build_clients_query(
allow_cross_branch: bool = False,
allow_all_clients: bool = False,
partner_id: int | None = None,
viewer_partner_id: int | None = None,
q: str = "",
status: str = "",
client_type: str = "",
@@ -35,6 +37,14 @@ def build_clients_query(
include_archived: bool = False,
):
assoc = ClientAssociation
review_access = exists(
select(ClientServiceSubscription.id).where(
ClientServiceSubscription.tenant_id == tenant_id,
ClientServiceSubscription.client_id == Client.id,
ClientServiceSubscription.review_partner_user_id == viewer_partner_id,
ClientServiceSubscription.is_active.is_(True),
)
) if viewer_partner_id is not None else None
stmt = (
select(
Client,
@@ -48,6 +58,7 @@ def build_clients_query(
assoc.consultant_id.label("assoc_consultant_id"),
assoc.partner_user_id.label("assoc_partner_user_id"),
assoc.created_source.label("assoc_created_source"),
(review_access if review_access is not None else False).label("has_review_access"),
)
.outerjoin(assoc, assoc.client_id == Client.id)
.outerjoin(ClientGroup, ClientGroup.id == Client.client_group_id)
@@ -61,7 +72,11 @@ def build_clients_query(
stmt = stmt.where(Client.is_archived.is_(False))
if branch_id and not allow_all_clients and not allow_cross_branch:
stmt = stmt.where(Client.branch_id == branch_id)
if partner_id:
if viewer_partner_id is not None:
stmt = stmt.where(
or_(Client.partner_id == viewer_partner_id, assoc.partner_user_id == viewer_partner_id, review_access)
)
elif partner_id:
stmt = stmt.where((Client.partner_id == partner_id) | (assoc.partner_user_id == partner_id))
if status:
stmt = stmt.where(Client.status == status)
@@ -82,13 +97,13 @@ def build_clients_query(
def list_clients(
db: Session, *, tenant_id: int, branch_id: int | None = None,
allow_cross_branch: bool = False, allow_all_clients: bool = False,
partner_id: int | None = None, q: str = "", status: str = "", client_type: str = "",
partner_id: int | None = None, viewer_partner_id: int | None = None, q: str = "", status: str = "", client_type: str = "",
client_group_id: int | None = None, include_archived: bool = False,
page: int = 1, per_page: int = 25, sort_by: str = "client_name", sort_order: str = "asc",
) -> dict:
stmt = build_clients_query(
tenant_id=tenant_id, branch_id=branch_id, allow_cross_branch=allow_cross_branch,
allow_all_clients=allow_all_clients, partner_id=partner_id, q=q, status=status,
allow_all_clients=allow_all_clients, partner_id=partner_id, viewer_partner_id=viewer_partner_id, q=q, status=status,
client_type=client_type, client_group_id=client_group_id, include_archived=include_archived,
)
total = db.execute(select(func.count()).select_from(stmt.subquery())).scalar_one()
@@ -100,7 +115,7 @@ def list_clients(
stmt.order_by(_safe_sort(sort_by, sort_order)).offset(offset).limit(per_page)
).all()
rows=[]
for client, partner_name, branch_name, tenant_name, client_group_name, client_group_code, association_type, assoc_firm_tenant_id, assoc_consultant_id, assoc_partner_user_id, assoc_created_source in result:
for client, partner_name, branch_name, tenant_name, client_group_name, client_group_code, association_type, assoc_firm_tenant_id, assoc_consultant_id, assoc_partner_user_id, assoc_created_source, has_review_access in result:
row={**client.__dict__}; row.pop("_sa_instance_state",None)
row.update({
"partner_name":partner_name,"branch_name":branch_name,"tenant_name":tenant_name,
@@ -108,17 +123,39 @@ def list_clients(
"association_type":association_type,"assoc_firm_tenant_id":assoc_firm_tenant_id,
"assoc_consultant_id":assoc_consultant_id,"assoc_partner_user_id":assoc_partner_user_id,
"assoc_created_source":assoc_created_source,"effective_partner_id":assoc_partner_user_id or row.get("partner_id"),
"has_review_access": bool(has_review_access),
}); rows.append(row)
stats_stmt=select(func.count(Client.id),func.sum(case((Client.status=="active",1),else_=0)),func.sum(case((Client.status=="inactive",1),else_=0)),func.sum(case((Client.status=="archived",1),else_=0)))
if not allow_all_clients:
stats_stmt=stats_stmt.where(Client.tenant_id==tenant_id)
if branch_id and not allow_cross_branch: stats_stmt=stats_stmt.where(Client.branch_id==branch_id)
if partner_id: stats_stmt=stats_stmt.where(Client.partner_id==partner_id)
if viewer_partner_id is not None:
review_stats = exists(select(ClientServiceSubscription.id).where(
ClientServiceSubscription.tenant_id == tenant_id,
ClientServiceSubscription.client_id == Client.id,
ClientServiceSubscription.review_partner_user_id == viewer_partner_id,
ClientServiceSubscription.is_active.is_(True),
))
stats_stmt = stats_stmt.where(or_(Client.partner_id == viewer_partner_id, review_stats))
elif partner_id:
stats_stmt=stats_stmt.where(Client.partner_id==partner_id)
total_all,active,inactive,archived=db.execute(stats_stmt).one()
return {"rows":rows,"meta":{"total":total,"page":page,"per_page":per_page,"pages":pages},"stats":{"total":int(total_all or 0),"active":int(active or 0),"inactive":int(inactive or 0),"archived":int(archived or 0)}}
def get_client_detail_payload(db: Session, client_id: int):
def has_partner_review_access(db: Session, *, tenant_id: int, client_id: int, partner_user_id: int) -> bool:
return bool(db.execute(
select(ClientServiceSubscription.id).where(
ClientServiceSubscription.tenant_id == tenant_id,
ClientServiceSubscription.client_id == client_id,
ClientServiceSubscription.review_partner_user_id == partner_user_id,
ClientServiceSubscription.is_active.is_(True),
).limit(1)
).scalar_one_or_none())
def get_client_detail_payload(db: Session, client_id: int, *, viewer_partner_id: int | None = None):
assoc=ClientAssociation
stmt=(select(
Client, ClientGroup.group_name.label("client_group_name"), ClientGroup.group_code.label("client_group_code"),
@@ -133,7 +170,10 @@ def get_client_detail_payload(db: Session, client_id: int):
row.update({"client_group_name":client_group_name,"client_group_code":client_group_code,"association_type":association_type,
"assoc_firm_tenant_id":assoc_firm_tenant_id,"assoc_consultant_id":assoc_consultant_id,
"assoc_partner_user_id":assoc_partner_user_id,"assoc_created_source":assoc_created_source,
"effective_partner_id":assoc_partner_user_id or row.get("partner_id")})
"effective_partner_id":assoc_partner_user_id or row.get("partner_id"),
"has_review_access": bool(viewer_partner_id is not None and has_partner_review_access(
db, tenant_id=int(row["tenant_id"]), client_id=int(row["id"]), partner_user_id=int(viewer_partner_id)
))})
return row