Enforce partner client ownership and partner-scoped imports
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy import exists, func, or_, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.modules.client_groups.models import ClientGroup
|
||||
from app.modules.clients.models import Client
|
||||
from app.modules.services.models import ClientServiceSubscription
|
||||
|
||||
GROUP_TYPES = ("Family", "Business Group", "Promoter Group", "Trust Group", "Common Management", "Other")
|
||||
|
||||
@@ -13,7 +14,7 @@ def normalise_group_code(value: str | None) -> str:
|
||||
return (value or "").strip().upper()
|
||||
|
||||
|
||||
def list_groups(db: Session, *, tenant_id: int, include_inactive: bool = False):
|
||||
def list_groups(db: Session, *, tenant_id: int, include_inactive: bool = False, viewer_partner_id: int | None = None):
|
||||
stmt = (
|
||||
select(ClientGroup, func.count(Client.id).label("client_count"))
|
||||
.outerjoin(Client, Client.client_group_id == ClientGroup.id)
|
||||
@@ -21,6 +22,14 @@ def list_groups(db: Session, *, tenant_id: int, include_inactive: bool = False):
|
||||
.group_by(ClientGroup.id)
|
||||
.order_by(ClientGroup.group_name.asc())
|
||||
)
|
||||
if viewer_partner_id is not None:
|
||||
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),
|
||||
))
|
||||
stmt = stmt.where(or_(Client.partner_id == viewer_partner_id, review_access))
|
||||
if not include_inactive:
|
||||
stmt = stmt.where(ClientGroup.is_active.is_(True))
|
||||
return [{"group": group, "client_count": int(count or 0)} for group, count in db.execute(stmt).all()]
|
||||
@@ -37,8 +46,17 @@ def get_group_by_code(db: Session, *, tenant_id: int, group_code: str):
|
||||
return db.execute(select(ClientGroup).where(ClientGroup.tenant_id == tenant_id, ClientGroup.group_code == code)).scalar_one_or_none()
|
||||
|
||||
|
||||
def list_group_clients(db: Session, *, tenant_id: int, group_id: int):
|
||||
return db.execute(select(Client).where(Client.tenant_id == tenant_id, Client.client_group_id == group_id, Client.is_archived.is_(False)).order_by(Client.is_group_head.desc(), Client.client_name.asc())).scalars().all()
|
||||
def list_group_clients(db: Session, *, tenant_id: int, group_id: int, viewer_partner_id: int | None = None):
|
||||
stmt = select(Client).where(Client.tenant_id == tenant_id, Client.client_group_id == group_id, Client.is_archived.is_(False))
|
||||
if viewer_partner_id is not None:
|
||||
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),
|
||||
))
|
||||
stmt = stmt.where(or_(Client.partner_id == viewer_partner_id, review_access))
|
||||
return db.execute(stmt.order_by(Client.is_group_head.desc(), Client.client_name.asc())).scalars().all()
|
||||
|
||||
|
||||
def create_group(db: Session, *, tenant_id: int, actor_user_id: int, payload: dict):
|
||||
|
||||
@@ -10,12 +10,20 @@ from app.core.templating import templates
|
||||
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles
|
||||
from app.modules.core.rbac.permission_guard import require_permission
|
||||
from app.modules.client_groups.service import GROUP_TYPES, create_group, get_group, list_group_clients, list_groups, update_group
|
||||
from app.modules.clients.access import build_scope
|
||||
from app.modules.clients.access import build_scope, enforce_partner_scope, is_partner_role
|
||||
from app.modules.clients import repository
|
||||
from app.modules.consultants.service import list_consultants
|
||||
|
||||
router = APIRouter(prefix="/client-groups", tags=["client-groups-ui"])
|
||||
|
||||
def _roles(db, user):
|
||||
return {str(name).strip().lower() for name in get_user_roles(db, user.id)}
|
||||
|
||||
def _scope(request, user, db):
|
||||
roles = _roles(db, user)
|
||||
scope = build_scope(request, user, lambda code: code in set(get_user_permissions(db, user.id)))
|
||||
return enforce_partner_scope(scope, user=user, role_names=roles), roles
|
||||
|
||||
|
||||
def _ctx(request, user, db, **extra):
|
||||
data={"request":request,"current_user":user,"current_user_roles":get_user_roles(db,user.id),"current_user_permissions":get_user_permissions(db,user.id),"csrf_token":get_or_create_csrf_token(request)}; data.update(extra); return data
|
||||
@@ -32,8 +40,8 @@ def groups_page(request: Request):
|
||||
try:
|
||||
user=get_current_user(request,db=db)
|
||||
if not user: return RedirectResponse('/login',303)
|
||||
require_permission(db,user,'clients.view'); scope=build_scope(request,user,lambda code: True)
|
||||
return templates.TemplateResponse('modules/client_groups/templates/client_groups/list.html',_ctx(request,user,db,title='Client Groups',groups=list_groups(db,tenant_id=scope.tenant_id,include_inactive=True)))
|
||||
require_permission(db,user,'clients.view'); scope, roles=_scope(request,user,db)
|
||||
return templates.TemplateResponse('modules/client_groups/templates/client_groups/list.html',_ctx(request,user,db,title='Client Groups',groups=list_groups(db,tenant_id=scope.tenant_id,include_inactive=True,viewer_partner_id=user.id if is_partner_role(roles) else None)))
|
||||
finally: db.close()
|
||||
|
||||
@router.get("/new")
|
||||
@@ -42,8 +50,8 @@ def group_new(request: Request):
|
||||
try:
|
||||
user=get_current_user(request,db=db)
|
||||
if not user: return RedirectResponse('/login',303)
|
||||
require_permission(db,user,'clients.create'); scope=build_scope(request,user,lambda code: True)
|
||||
return templates.TemplateResponse('modules/client_groups/templates/client_groups/form.html',_ctx(request,user,db,title='Add Client Group',row=None,group_types=GROUP_TYPES,partners=repository.list_partners_for_scope(db,tenant_id=scope.tenant_id,branch_id=None),consultants=list_consultants(db,tenant_id=scope.tenant_id,include_inactive=False),errors=[]))
|
||||
require_permission(db,user,'clients.create'); scope, roles=_scope(request,user,db)
|
||||
return templates.TemplateResponse('modules/client_groups/templates/client_groups/form.html',_ctx(request,user,db,title='Add Client Group',row=None,group_types=GROUP_TYPES,partners=([repository.get_partner(db,user.id)] if is_partner_role(roles) else repository.list_partners_for_scope(db,tenant_id=scope.tenant_id,branch_id=None)),consultants=list_consultants(db,tenant_id=scope.tenant_id,include_inactive=False),errors=[]))
|
||||
finally: db.close()
|
||||
|
||||
@router.post("")
|
||||
@@ -52,9 +60,12 @@ async def group_create(request: Request):
|
||||
try:
|
||||
user=get_current_user(request,db=db); form=await request.form(); validate_csrf(request,form.get('csrf_token'))
|
||||
if not user: return RedirectResponse('/login',303)
|
||||
require_permission(db,user,'clients.create'); scope=build_scope(request,user,lambda code: True)
|
||||
try: row=create_group(db,tenant_id=scope.tenant_id,actor_user_id=user.id,payload=_payload(form)); return RedirectResponse(f'/client-groups/{row.id}',303)
|
||||
except Exception as exc: return templates.TemplateResponse('modules/client_groups/templates/client_groups/form.html',_ctx(request,user,db,title='Add Client Group',row=_payload(form),group_types=GROUP_TYPES,partners=repository.list_partners_for_scope(db,tenant_id=scope.tenant_id,branch_id=None),consultants=list_consultants(db,tenant_id=scope.tenant_id,include_inactive=False),errors=[str(exc)]),status_code=400)
|
||||
require_permission(db,user,'clients.create'); scope, roles=_scope(request,user,db)
|
||||
try:
|
||||
payload=_payload(form)
|
||||
if is_partner_role(roles): payload["assigned_partner_user_id"] = user.id
|
||||
row=create_group(db,tenant_id=scope.tenant_id,actor_user_id=user.id,payload=payload); return RedirectResponse(f'/client-groups/{row.id}',303)
|
||||
except Exception as exc: return templates.TemplateResponse('modules/client_groups/templates/client_groups/form.html',_ctx(request,user,db,title='Add Client Group',row=_payload(form),group_types=GROUP_TYPES,partners=([repository.get_partner(db,user.id)] if is_partner_role(roles) else repository.list_partners_for_scope(db,tenant_id=scope.tenant_id,branch_id=None)),consultants=list_consultants(db,tenant_id=scope.tenant_id,include_inactive=False),errors=[str(exc)]),status_code=400)
|
||||
finally: db.close()
|
||||
|
||||
@router.get("/{group_id}")
|
||||
@@ -63,9 +74,9 @@ def group_detail(request: Request, group_id:int):
|
||||
try:
|
||||
user=get_current_user(request,db=db)
|
||||
if not user: return RedirectResponse('/login',303)
|
||||
require_permission(db,user,'clients.view'); scope=build_scope(request,user,lambda code: True); row=get_group(db,tenant_id=scope.tenant_id,group_id=group_id)
|
||||
require_permission(db,user,'clients.view'); scope, roles=_scope(request,user,db); row=get_group(db,tenant_id=scope.tenant_id,group_id=group_id)
|
||||
if not row: return RedirectResponse('/client-groups',303)
|
||||
return templates.TemplateResponse('modules/client_groups/templates/client_groups/detail.html',_ctx(request,user,db,title=row.group_name,row=row,clients=list_group_clients(db,tenant_id=scope.tenant_id,group_id=row.id)))
|
||||
return templates.TemplateResponse('modules/client_groups/templates/client_groups/detail.html',_ctx(request,user,db,title=row.group_name,row=row,clients=list_group_clients(db,tenant_id=scope.tenant_id,group_id=row.id,viewer_partner_id=user.id if is_partner_role(roles) else None)))
|
||||
finally: db.close()
|
||||
|
||||
@router.get("/{group_id}/edit")
|
||||
@@ -74,9 +85,9 @@ def group_edit(request: Request, group_id:int):
|
||||
try:
|
||||
user=get_current_user(request,db=db)
|
||||
if not user: return RedirectResponse('/login',303)
|
||||
require_permission(db,user,'clients.edit'); scope=build_scope(request,user,lambda code: True); row=get_group(db,tenant_id=scope.tenant_id,group_id=group_id)
|
||||
require_permission(db,user,'clients.edit'); scope, roles=_scope(request,user,db); row=get_group(db,tenant_id=scope.tenant_id,group_id=group_id)
|
||||
if not row: return RedirectResponse('/client-groups',303)
|
||||
return templates.TemplateResponse('modules/client_groups/templates/client_groups/form.html',_ctx(request,user,db,title='Edit Client Group',row=row,group_types=GROUP_TYPES,partners=repository.list_partners_for_scope(db,tenant_id=scope.tenant_id,branch_id=None),consultants=list_consultants(db,tenant_id=scope.tenant_id,include_inactive=False),errors=[]))
|
||||
return templates.TemplateResponse('modules/client_groups/templates/client_groups/form.html',_ctx(request,user,db,title='Edit Client Group',row=row,group_types=GROUP_TYPES,partners=([repository.get_partner(db,user.id)] if is_partner_role(roles) else repository.list_partners_for_scope(db,tenant_id=scope.tenant_id,branch_id=None)),consultants=list_consultants(db,tenant_id=scope.tenant_id,include_inactive=False),errors=[]))
|
||||
finally: db.close()
|
||||
|
||||
@router.post("/{group_id}/edit")
|
||||
@@ -85,8 +96,11 @@ async def group_update(request: Request, group_id:int):
|
||||
try:
|
||||
user=get_current_user(request,db=db); form=await request.form(); validate_csrf(request,form.get('csrf_token'))
|
||||
if not user: return RedirectResponse('/login',303)
|
||||
require_permission(db,user,'clients.edit'); scope=build_scope(request,user,lambda code: True); row=get_group(db,tenant_id=scope.tenant_id,group_id=group_id)
|
||||
require_permission(db,user,'clients.edit'); scope, roles=_scope(request,user,db); row=get_group(db,tenant_id=scope.tenant_id,group_id=group_id)
|
||||
if not row: return RedirectResponse('/client-groups',303)
|
||||
try: update_group(db,row=row,actor_user_id=user.id,payload=_payload(form)); return RedirectResponse(f'/client-groups/{row.id}',303)
|
||||
except Exception as exc: return templates.TemplateResponse('modules/client_groups/templates/client_groups/form.html',_ctx(request,user,db,title='Edit Client Group',row=row,group_types=GROUP_TYPES,partners=repository.list_partners_for_scope(db,tenant_id=scope.tenant_id,branch_id=None),consultants=list_consultants(db,tenant_id=scope.tenant_id,include_inactive=False),errors=[str(exc)]),status_code=400)
|
||||
try:
|
||||
payload=_payload(form)
|
||||
if is_partner_role(roles): payload["assigned_partner_user_id"] = user.id
|
||||
update_group(db,row=row,actor_user_id=user.id,payload=payload); return RedirectResponse(f'/client-groups/{row.id}',303)
|
||||
except Exception as exc: return templates.TemplateResponse('modules/client_groups/templates/client_groups/form.html',_ctx(request,user,db,title='Edit Client Group',row=row,group_types=GROUP_TYPES,partners=([repository.get_partner(db,user.id)] if is_partner_role(roles) else repository.list_partners_for_scope(db,tenant_id=scope.tenant_id,branch_id=None)),consultants=list_consultants(db,tenant_id=scope.tenant_id,include_inactive=False),errors=[str(exc)]),status_code=400)
|
||||
finally: db.close()
|
||||
|
||||
Reference in New Issue
Block a user