Phase 3 harden CSRF invalid POST and work access handling
This commit is contained in:
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Form, Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
from app.core.http_responses import not_found_response
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.core.db.common import CommonSessionLocal
|
||||
@@ -17,6 +18,12 @@ from app.modules.core.rbac.services import build_permissions_payload, build_role
|
||||
router = APIRouter(prefix="/system-settings/rbac", tags=["rbac-ui"])
|
||||
|
||||
|
||||
def _csrf_rejected(request: Request):
|
||||
from app.core.http_responses import forbidden_response
|
||||
return forbidden_response(request, "CSRF validation failed")
|
||||
|
||||
|
||||
|
||||
def _redirect_login():
|
||||
return RedirectResponse(url="/login", status_code=303)
|
||||
|
||||
@@ -47,6 +54,15 @@ def rbac_dashboard(request: Request):
|
||||
return RedirectResponse(url="/system-settings/rbac/roles", status_code=303)
|
||||
|
||||
|
||||
|
||||
|
||||
# Phase 3 security hardening: the roles list endpoint must not accept direct unsafe POSTs.
|
||||
# Existing role creation continues to use /roles/new.
|
||||
@router.post("/roles")
|
||||
def roles_list_post_rejected(request: Request):
|
||||
return _csrf_rejected(request)
|
||||
|
||||
|
||||
@router.get("/roles")
|
||||
def roles_list(request: Request, q: str = "", page: int = 1, per_page: int = 10):
|
||||
db = CommonSessionLocal()
|
||||
@@ -76,7 +92,10 @@ def role_create_submit(
|
||||
is_active: str | None = Form(None),
|
||||
csrf_token: str = Form(...),
|
||||
):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -124,7 +143,7 @@ def role_permissions_edit(request: Request, role_id: int):
|
||||
|
||||
role = db.execute(select(Role).where(Role.id == role_id)).scalar_one_or_none()
|
||||
if not role:
|
||||
return RedirectResponse(url="/system-settings/rbac/roles", status_code=303)
|
||||
return not_found_response(request, "Role not found")
|
||||
|
||||
scope = build_scope(db, current_user)
|
||||
try:
|
||||
@@ -160,7 +179,10 @@ def role_permissions_submit(
|
||||
permission_ids: list[int] = Form([]),
|
||||
csrf_token: str = Form(...),
|
||||
):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -172,7 +194,7 @@ def role_permissions_submit(
|
||||
|
||||
role = db.execute(select(Role).where(Role.id == role_id)).scalar_one_or_none()
|
||||
if not role:
|
||||
return RedirectResponse(url="/system-settings/rbac/roles", status_code=303)
|
||||
return not_found_response(request, "Role not found")
|
||||
|
||||
old_permission_ids = db.execute(
|
||||
select(RolePermission.permission_id).where(RolePermission.role_id == role.id)
|
||||
@@ -233,7 +255,10 @@ def permission_create_submit(
|
||||
is_active: str | None = Form(None),
|
||||
csrf_token: str = Form(...),
|
||||
):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
|
||||
@@ -135,6 +135,12 @@ from app.modules.employees.service import (
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/employees", tags=["employees-ui"])
|
||||
|
||||
|
||||
def _csrf_rejected(request: Request):
|
||||
from app.core.http_responses import forbidden_response
|
||||
return forbidden_response(request, "CSRF validation failed")
|
||||
|
||||
portal_router = APIRouter(prefix="/employee", tags=["employee-portal-ui"])
|
||||
|
||||
|
||||
@@ -416,7 +422,10 @@ def employee_new(request: Request):
|
||||
@router.post("/new")
|
||||
async def employee_create_submit(request: Request):
|
||||
form = await request.form()
|
||||
try:
|
||||
validate_csrf(request, form.get("csrf_token"))
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -447,6 +456,21 @@ async def employee_create_submit(request: Request):
|
||||
db.close()
|
||||
|
||||
|
||||
|
||||
|
||||
# Phase 3 security hardening: list pages must not accept direct unsafe POSTs.
|
||||
# Existing attendance and leave actions continue to use their dedicated action routes.
|
||||
@router.post("/attendance")
|
||||
def employee_attendance_list_post_rejected(request: Request):
|
||||
return _csrf_rejected(request)
|
||||
|
||||
|
||||
@router.post("/leaves")
|
||||
@router.post("/leave")
|
||||
def employee_leave_list_post_rejected(request: Request):
|
||||
return _csrf_rejected(request)
|
||||
|
||||
|
||||
@router.get("/attendance")
|
||||
def employees_attendance_list(
|
||||
request: Request,
|
||||
@@ -506,7 +530,10 @@ def employees_attendance_manual_submit(
|
||||
remarks: str | None = Form(None),
|
||||
csrf_token: str = Form(...),
|
||||
):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -541,7 +568,10 @@ def employees_attendance_review_submit(
|
||||
review_notes: str | None = Form(None),
|
||||
csrf_token: str = Form(...),
|
||||
):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -583,7 +613,10 @@ def employee_leave_types(request: Request, include_inactive: str | None = None):
|
||||
|
||||
@router.post("/leave-types/defaults")
|
||||
def employee_leave_type_defaults(request: Request, csrf_token: str = Form(...)):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -604,7 +637,10 @@ def employee_leave_type_defaults(request: Request, csrf_token: str = Form(...)):
|
||||
|
||||
@router.post("/leave-types/new")
|
||||
def employee_leave_type_create(request: Request, code: str = Form(...), name: str = Form(...), description: str | None = Form(None), annual_quota_days: int = Form(0), carry_forward_allowed: str | None = Form(None), allow_negative_balance: str | None = Form(None), requires_approval: str | None = Form(None), is_paid: str | None = Form(None), is_active: str | None = Form(None), csrf_token: str = Form(...)):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -625,7 +661,10 @@ def employee_leave_type_create(request: Request, code: str = Form(...), name: st
|
||||
|
||||
@router.post("/leave-types/{leave_type_id}/edit")
|
||||
def employee_leave_type_edit(request: Request, leave_type_id: int, name: str = Form(...), description: str | None = Form(None), annual_quota_days: int = Form(0), carry_forward_allowed: str | None = Form(None), allow_negative_balance: str | None = Form(None), requires_approval: str | None = Form(None), is_paid: str | None = Form(None), is_active: str | None = Form(None), csrf_token: str = Form(...)):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -669,7 +708,10 @@ def employee_leave_balances(request: Request, employee_id: int | None = None):
|
||||
|
||||
@router.post("/leave-balances/adjust")
|
||||
def employee_leave_balance_adjust(request: Request, employee_id: int = Form(...), leave_type_id: int = Form(...), adjusted_days: int = Form(0), notes: str | None = Form(None), csrf_token: str = Form(...)):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -712,7 +754,10 @@ def employee_leave_requests(request: Request, employee_id: int | None = None, st
|
||||
|
||||
@router.post("/leave/{request_id}/review")
|
||||
def employee_leave_review_submit(request: Request, request_id: int, status: str = Form(...), review_notes: str | None = Form(None), csrf_token: str = Form(...)):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -798,7 +843,10 @@ def employee_registration_request_approve(
|
||||
review_notes: str | None = Form(None),
|
||||
csrf_token: str = Form(...),
|
||||
):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -825,7 +873,10 @@ def employee_registration_request_reject(
|
||||
review_notes: str | None = Form(None),
|
||||
csrf_token: str = Form(...),
|
||||
):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -869,7 +920,10 @@ def employee_onboarding_checklist(request: Request):
|
||||
|
||||
@router.post("/onboarding-checklist/defaults")
|
||||
def employee_onboarding_checklist_defaults(request: Request, csrf_token: str = Form(...)):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -891,7 +945,10 @@ def employee_onboarding_checklist_defaults(request: Request, csrf_token: str = F
|
||||
@router.post("/onboarding-checklist")
|
||||
async def employee_onboarding_checklist_create(request: Request):
|
||||
form = await request.form()
|
||||
try:
|
||||
validate_csrf(request, form.get("csrf_token"))
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -913,7 +970,10 @@ async def employee_onboarding_checklist_create(request: Request):
|
||||
@router.post("/onboarding-checklist/{item_id}/edit")
|
||||
async def employee_onboarding_checklist_edit(request: Request, item_id: int):
|
||||
form = await request.form()
|
||||
try:
|
||||
validate_csrf(request, form.get("csrf_token"))
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -954,7 +1014,10 @@ def employee_onboarding_tasks(request: Request, employee_id: int | None = None):
|
||||
|
||||
@router.post("/{employee_id}/onboarding/generate")
|
||||
def employee_generate_onboarding(request: Request, employee_id: int, csrf_token: str = Form(...)):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -976,7 +1039,10 @@ def employee_generate_onboarding(request: Request, employee_id: int, csrf_token:
|
||||
|
||||
@router.post("/onboarding/{task_id}/status")
|
||||
def employee_onboarding_task_status(request: Request, task_id: int, status: str = Form(...), review_notes: str | None = Form(None), csrf_token: str = Form(...)):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -1019,7 +1085,10 @@ def employee_offboarding_requests(request: Request, status: str | None = None):
|
||||
@router.post("/{employee_id}/offboarding/initiate")
|
||||
async def employee_offboarding_initiate(request: Request, employee_id: int):
|
||||
form = await request.form()
|
||||
try:
|
||||
validate_csrf(request, form.get("csrf_token"))
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -1041,7 +1110,10 @@ async def employee_offboarding_initiate(request: Request, employee_id: int):
|
||||
|
||||
@router.post("/offboarding/{request_id}/review")
|
||||
def employee_offboarding_review(request: Request, request_id: int, status: str = Form(...), approved_relieving_date: str | None = Form(None), review_notes: str | None = Form(None), csrf_token: str = Form(...)):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -1063,7 +1135,10 @@ def employee_offboarding_review(request: Request, request_id: int, status: str =
|
||||
|
||||
@router.post("/offboarding/tasks/{task_id}/status")
|
||||
def employee_offboarding_task_status(request: Request, task_id: int, status: str = Form(...), review_notes: str | None = Form(None), csrf_token: str = Form(...)):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -1085,7 +1160,10 @@ def employee_offboarding_task_status(request: Request, task_id: int, status: str
|
||||
|
||||
@router.post("/offboarding/{request_id}/complete")
|
||||
def employee_offboarding_complete(request: Request, request_id: int, csrf_token: str = Form(...)):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -1127,7 +1205,10 @@ def employee_document_types(request: Request, include_inactive: str | None = Non
|
||||
|
||||
@router.post("/document-types/defaults")
|
||||
def employee_document_types_defaults(request: Request, csrf_token: str = Form(...)):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -1149,7 +1230,10 @@ def employee_document_types_defaults(request: Request, csrf_token: str = Form(..
|
||||
@router.post("/document-types")
|
||||
async def employee_document_type_create(request: Request):
|
||||
form = await request.form()
|
||||
try:
|
||||
validate_csrf(request, form.get("csrf_token"))
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -1174,7 +1258,10 @@ async def employee_document_type_create(request: Request):
|
||||
@router.post("/document-types/{type_id}/edit")
|
||||
async def employee_document_type_update(request: Request, type_id: int):
|
||||
form = await request.form()
|
||||
try:
|
||||
validate_csrf(request, form.get("csrf_token"))
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -1216,7 +1303,10 @@ def employee_documents(request: Request, employee_id: int | None = None, status:
|
||||
@router.post("/{employee_id}/documents/upload")
|
||||
async def employee_document_upload(request: Request, employee_id: int, document_file: UploadFile = File(...)):
|
||||
form = await request.form()
|
||||
try:
|
||||
validate_csrf(request, form.get("csrf_token"))
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -1258,7 +1348,10 @@ async def employee_document_upload(request: Request, employee_id: int, document_
|
||||
|
||||
@router.post("/documents/{document_id}/review")
|
||||
def employee_document_review(request: Request, document_id: int, status: str = Form(...), verification_notes: str | None = Form(None), csrf_token: str = Form(...)):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -1280,7 +1373,10 @@ def employee_document_review(request: Request, document_id: int, status: str = F
|
||||
|
||||
@router.post("/documents/{document_id}/archive")
|
||||
def employee_document_archive(request: Request, document_id: int, notes: str | None = Form(None), csrf_token: str = Form(...)):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -1324,7 +1420,10 @@ def employee_self_documents(request: Request):
|
||||
@portal_router.post("/documents/upload")
|
||||
async def employee_self_document_upload(request: Request, document_file: UploadFile = File(...)):
|
||||
form = await request.form()
|
||||
try:
|
||||
validate_csrf(request, form.get("csrf_token"))
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -1398,7 +1497,10 @@ def employee_salary_structures(request: Request, employee_id: int | None = None)
|
||||
@router.post("/payroll/structures")
|
||||
async def employee_salary_structure_submit(request: Request):
|
||||
form = await request.form()
|
||||
try:
|
||||
validate_csrf(request, form.get("csrf_token"))
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -1425,7 +1527,10 @@ async def employee_salary_structure_submit(request: Request):
|
||||
@router.post("/payroll/structures/{structure_id}/update")
|
||||
async def employee_salary_structure_update(request: Request, structure_id: int):
|
||||
form = await request.form()
|
||||
try:
|
||||
validate_csrf(request, form.get("csrf_token"))
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -1468,7 +1573,10 @@ def employee_payroll_runs(request: Request):
|
||||
|
||||
@router.post("/payroll/runs")
|
||||
def employee_payroll_run_create(request: Request, pay_year: int = Form(...), pay_month: int = Form(...), notes: str | None = Form(None), csrf_token: str = Form(...)):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -1489,7 +1597,10 @@ def employee_payroll_run_create(request: Request, pay_year: int = Form(...), pay
|
||||
|
||||
@router.post("/payroll/runs/{run_id}/generate")
|
||||
def employee_payroll_run_generate(request: Request, run_id: int, csrf_token: str = Form(...)):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -1511,7 +1622,10 @@ def employee_payroll_run_generate(request: Request, run_id: int, csrf_token: str
|
||||
|
||||
@router.post("/payroll/runs/{run_id}/approve")
|
||||
def employee_payroll_run_approve(request: Request, run_id: int, csrf_token: str = Form(...)):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -1533,7 +1647,10 @@ def employee_payroll_run_approve(request: Request, run_id: int, csrf_token: str
|
||||
|
||||
@router.post("/payroll/runs/{run_id}/paid")
|
||||
def employee_payroll_run_paid(request: Request, run_id: int, csrf_token: str = Form(...)):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -1680,7 +1797,10 @@ def employee_work_task_assign(
|
||||
require_permission(db, current_user, "employees.work.manage")
|
||||
except Exception:
|
||||
return _redirect_denied()
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
|
||||
branch_id = request.session.get("active_branch_id")
|
||||
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
|
||||
@@ -1720,7 +1840,10 @@ def employee_my_work_task_status(
|
||||
require_permission(db, current_user, "employees.work.view_self")
|
||||
except Exception:
|
||||
return _redirect_denied()
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
|
||||
branch_id = request.session.get("active_branch_id")
|
||||
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
|
||||
@@ -1860,7 +1983,10 @@ def employee_work_task_add_communication(
|
||||
require_permission(db, current_user, "employees.work.manage")
|
||||
except Exception:
|
||||
return _redirect_denied()
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
|
||||
branch_id = request.session.get("active_branch_id")
|
||||
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
|
||||
@@ -1935,7 +2061,10 @@ def employee_my_work_task_add_communication(
|
||||
require_permission(db, current_user, "employees.work.view_self")
|
||||
except Exception:
|
||||
return _redirect_denied()
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
|
||||
branch_id = request.session.get("active_branch_id")
|
||||
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
|
||||
@@ -2026,7 +2155,10 @@ def employee_import_template(request: Request, import_type: str):
|
||||
@router.post("/imports/preview")
|
||||
async def employee_import_preview(request: Request, import_type: str = Form(...), import_file: UploadFile = File(...)):
|
||||
form = await request.form()
|
||||
try:
|
||||
validate_csrf(request, form.get("csrf_token"))
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -2069,7 +2201,10 @@ async def employee_import_preview(request: Request, import_type: str = Form(...)
|
||||
|
||||
@router.post("/imports/commit")
|
||||
def employee_import_commit(request: Request, csrf_token: str = Form(...)):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -2162,7 +2297,10 @@ def employee_edit(request: Request, employee_id: int):
|
||||
@router.post("/{employee_id}/edit")
|
||||
async def employee_edit_submit(request: Request, employee_id: int):
|
||||
form = await request.form()
|
||||
try:
|
||||
validate_csrf(request, form.get("csrf_token"))
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -2199,7 +2337,10 @@ async def employee_edit_submit(request: Request, employee_id: int):
|
||||
|
||||
@router.post("/{employee_id}/link-user")
|
||||
def employee_link_user_submit(request: Request, employee_id: int, user_id: str | None = Form(None), csrf_token: str = Form(...)):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -2231,7 +2372,10 @@ def employee_status_submit(
|
||||
date_of_leaving: str | None = Form(None),
|
||||
csrf_token: str = Form(...),
|
||||
):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -2324,7 +2468,10 @@ def employee_punch_in_submit(
|
||||
accuracy_meters: str | None = Form(None),
|
||||
csrf_token: str = Form(...),
|
||||
):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -2357,7 +2504,10 @@ def employee_punch_out_submit(
|
||||
accuracy_meters: str | None = Form(None),
|
||||
csrf_token: str = Form(...),
|
||||
):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -2405,7 +2555,10 @@ def employee_self_leave(request: Request):
|
||||
|
||||
@portal_router.post("/leave/apply")
|
||||
def employee_self_leave_apply(request: Request, leave_type_id: int = Form(...), from_date: str = Form(...), to_date: str = Form(...), reason: str | None = Form(None), csrf_token: str = Form(...)):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -2423,7 +2576,10 @@ def employee_self_leave_apply(request: Request, leave_type_id: int = Form(...),
|
||||
|
||||
@portal_router.post("/leave/{request_id}/cancel")
|
||||
def employee_self_leave_cancel(request: Request, request_id: int, csrf_token: str = Form(...)):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -2461,7 +2617,10 @@ def employee_self_offboarding(request: Request):
|
||||
@portal_router.post("/offboarding/request")
|
||||
async def employee_self_offboarding_request(request: Request):
|
||||
form = await request.form()
|
||||
try:
|
||||
validate_csrf(request, form.get("csrf_token"))
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -2513,7 +2672,10 @@ def employee_self_profile(request: Request):
|
||||
@portal_router.post("/profile")
|
||||
async def employee_self_profile_submit(request: Request):
|
||||
form = await request.form()
|
||||
try:
|
||||
validate_csrf(request, form.get("csrf_token"))
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -2605,7 +2767,10 @@ def employee_registration_form(request: Request):
|
||||
@portal_router.post("/register")
|
||||
async def employee_registration_submit(request: Request):
|
||||
form = await request.form()
|
||||
try:
|
||||
validate_csrf(request, form.get("csrf_token"))
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
|
||||
@@ -34,6 +34,12 @@ router = APIRouter(prefix="/marketplace", tags=["marketplace-ui"])
|
||||
public_router = APIRouter(tags=["marketplace-public-ui"])
|
||||
|
||||
|
||||
def _csrf_rejected(request: Request):
|
||||
from app.core.http_responses import forbidden_response
|
||||
return forbidden_response(request, "CSRF validation failed")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def _public_marketplace_ctx(request: Request, db, **ctx):
|
||||
@@ -134,8 +140,11 @@ def marketplace_public_request_service_submit(request: Request, csrf_token: str
|
||||
if not is_marketplace_domain_request(request):
|
||||
return RedirectResponse(url="/marketplace/public-lead", status_code=303)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
lead = create_marketplace_lead(
|
||||
db,
|
||||
lead_name=lead_name,
|
||||
@@ -254,7 +263,10 @@ def lead_create(request: Request, csrf_token: str = Form(...), lead_name: str =
|
||||
user, response = _require_user(request, db, "marketplace_leads.create")
|
||||
if response:
|
||||
return response
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
lead = create_marketplace_lead(db, lead_name=lead_name, business_name=business_name, email=email, mobile=mobile, city=city, state=state, service_category=service_category, service_requested=service_requested, message=message, source=source, priority=priority, estimated_value=estimated_value, created_by_user_id=user.id)
|
||||
return RedirectResponse(url=f"/marketplace/leads/{lead.id}", status_code=303)
|
||||
finally:
|
||||
@@ -273,8 +285,11 @@ def public_lead_form(request: Request):
|
||||
@router.post("/public-lead")
|
||||
def public_lead_submit(request: Request, csrf_token: str = Form(...), lead_name: str = Form(...), business_name: str = Form(""), email: str = Form(""), mobile: str = Form(""), city: str = Form(""), state: str = Form(""), service_category: str = Form(""), service_requested: str = Form(...), message: str = Form("")):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
lead = create_marketplace_lead(db, lead_name=lead_name, business_name=business_name, email=email, mobile=mobile, city=city, state=state, service_category=service_category, service_requested=service_requested, message=message, source="public_website", priority="NORMAL")
|
||||
return _render(request, "modules/marketplace/templates/marketplace/public_thank_you.html", db, None, title="Thank You", lead=lead)
|
||||
finally:
|
||||
@@ -303,7 +318,10 @@ def lead_assign(request: Request, lead_id: int, csrf_token: str = Form(...), ten
|
||||
user, response = _require_user(request, db, "marketplace_leads.assign")
|
||||
if response:
|
||||
return response
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
lead = get_marketplace_lead(db, lead_id)
|
||||
if lead:
|
||||
assign_marketplace_lead(db, lead=lead, tenant_id=tenant_id, branch_id=branch_id or None, partner_user_id=partner_user_id or None, notes=notes, assigned_by_user_id=user.id)
|
||||
@@ -319,7 +337,10 @@ def lead_status_update(request: Request, lead_id: int, csrf_token: str = Form(..
|
||||
user, response = _require_user(request, db, "marketplace_leads.update")
|
||||
if response:
|
||||
return response
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
lead = get_marketplace_lead(db, lead_id)
|
||||
if lead:
|
||||
update_lead_status(db, lead=lead, status=status, user_id=user.id)
|
||||
@@ -335,7 +356,10 @@ def lead_convert_client(request: Request, lead_id: int, csrf_token: str = Form(.
|
||||
user, response = _require_user(request, db, "marketplace_leads.convert")
|
||||
if response:
|
||||
return response
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
lead = get_marketplace_lead(db, lead_id)
|
||||
if lead:
|
||||
client = convert_lead_to_client(db, lead=lead, tenant_id=tenant_id, branch_id=branch_id, partner_user_id=partner_user_id or None, client_code=client_code, user_id=user.id)
|
||||
|
||||
@@ -46,6 +46,12 @@ from app.modules.core.tenancy.year_control import redirect_if_financial_year_loc
|
||||
router = APIRouter(prefix="/notice-cases", tags=["notice-cases-ui"])
|
||||
|
||||
|
||||
def _csrf_rejected(request: Request):
|
||||
from app.core.http_responses import forbidden_response
|
||||
return forbidden_response(request, "CSRF validation failed")
|
||||
|
||||
|
||||
|
||||
def _base_ctx(request: Request, user, db, **ctx):
|
||||
base = {
|
||||
"request": request,
|
||||
@@ -249,7 +255,10 @@ def case_create_submit(
|
||||
remarks: str = Form(""),
|
||||
csrf_token: str = Form(...),
|
||||
):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "notice_cases.create")
|
||||
@@ -316,7 +325,10 @@ def case_edit_page(request: Request, case_id: int):
|
||||
|
||||
@router.post("/{case_id}/edit")
|
||||
def case_edit_submit(request: Request, case_id: int, department: str = Form("GST"), case_type: str = Form("Notice"), title: str = Form(""), reference_no: str = Form(""), din_ack_no: str = Form(""), notice_date: str = Form(""), due_date: str = Form(""), financial_year: str = Form(""), assessment_year: str = Form(""), period_label: str = Form(""), status: str = Form("open"), priority: str = Form("normal"), assigned_partner_user_id: str = Form(""), assigned_manager_user_id: str = Form(""), assigned_staff_user_id: str = Form(""), issue_summary: str = Form(""), remarks: str = Form(""), csrf_token: str = Form(...)):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "notice_cases.edit")
|
||||
@@ -348,7 +360,10 @@ def case_edit_submit(request: Request, case_id: int, department: str = Form("GST
|
||||
|
||||
@router.post("/{case_id}/events")
|
||||
def case_add_event(request: Request, case_id: int, event_type: str = Form("Internal Note"), event_date: str = Form(""), description: str = Form(""), next_due_date: str = Form(""), csrf_token: str = Form(...)):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "notice_cases.events.manage")
|
||||
@@ -368,7 +383,10 @@ def case_add_event(request: Request, case_id: int, event_type: str = Form("Inter
|
||||
|
||||
@router.post("/{case_id}/hearings")
|
||||
def case_add_hearing(request: Request, case_id: int, hearing_date: str = Form(""), hearing_time: str = Form(""), venue_or_mode: str = Form(""), officer_name: str = Form(""), agenda: str = Form(""), outcome: str = Form(""), status: str = Form("scheduled"), csrf_token: str = Form(...)):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "notice_cases.hearings.manage")
|
||||
@@ -388,7 +406,10 @@ def case_add_hearing(request: Request, case_id: int, hearing_date: str = Form(""
|
||||
|
||||
@router.post("/{case_id}/orders")
|
||||
def case_add_order(request: Request, case_id: int, order_type: str = Form("Other"), order_no: str = Form(""), order_date: str = Form(""), demand_amount: str = Form("0"), tax_amount: str = Form("0"), interest_amount: str = Form("0"), penalty_amount: str = Form("0"), summary: str = Form(""), appeal_due_date: str = Form(""), appeal_filed: str | None = Form(None), csrf_token: str = Form(...)):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "notice_cases.orders.manage")
|
||||
@@ -408,7 +429,10 @@ def case_add_order(request: Request, case_id: int, order_type: str = Form("Other
|
||||
|
||||
@router.post("/{case_id}/documents/upload")
|
||||
def case_document_upload(request: Request, case_id: int, title: str = Form(""), document_type: str = Form("OTHER"), description: str = Form(""), event_id: str = Form(""), file: UploadFile = File(...), csrf_token: str = Form(...)):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "notice_cases.documents.upload")
|
||||
@@ -451,7 +475,10 @@ def case_document_download(request: Request, document_id: int):
|
||||
|
||||
@router.post("/documents/{document_id}/delete")
|
||||
def case_document_delete(request: Request, document_id: int, csrf_token: str = Form(...)):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "notice_cases.documents.delete")
|
||||
|
||||
@@ -48,6 +48,12 @@ from app.modules.platform_billing.services import (
|
||||
router = APIRouter(prefix="/platform-billing", tags=["platform-billing-ui"])
|
||||
|
||||
|
||||
def _csrf_rejected(request: Request):
|
||||
from app.core.http_responses import forbidden_response
|
||||
return forbidden_response(request, "CSRF validation failed")
|
||||
|
||||
|
||||
|
||||
def _redirect_denied():
|
||||
from app.core.http_responses import ui_access_denied
|
||||
return ui_access_denied()
|
||||
@@ -121,6 +127,45 @@ def dashboard(request: Request):
|
||||
db.close()
|
||||
|
||||
|
||||
|
||||
|
||||
# Phase 3 security hardening: list pages must not accept direct unsafe POSTs.
|
||||
# Existing create/update features continue to use their dedicated /new or action routes.
|
||||
@router.post("/plans")
|
||||
def plans_list_post_rejected(request: Request):
|
||||
return _csrf_rejected(request)
|
||||
|
||||
|
||||
@router.post("/accounts")
|
||||
def accounts_list_post_rejected(request: Request):
|
||||
return _csrf_rejected(request)
|
||||
|
||||
|
||||
@router.post("/audit-firm-subscriptions")
|
||||
def audit_firm_subscriptions_list_post_rejected(request: Request):
|
||||
return _csrf_rejected(request)
|
||||
|
||||
|
||||
@router.post("/client-dashboard-subscriptions")
|
||||
def client_dashboard_subscriptions_list_post_rejected(request: Request):
|
||||
return _csrf_rejected(request)
|
||||
|
||||
|
||||
@router.post("/consultant-subscriptions")
|
||||
def consultant_subscriptions_list_post_rejected(request: Request):
|
||||
return _csrf_rejected(request)
|
||||
|
||||
|
||||
@router.post("/subscriptions")
|
||||
def subscriptions_list_post_rejected(request: Request):
|
||||
return _csrf_rejected(request)
|
||||
|
||||
|
||||
@router.post("/invoices")
|
||||
def invoices_list_post_rejected(request: Request):
|
||||
return _csrf_rejected(request)
|
||||
|
||||
|
||||
@router.get("/plans")
|
||||
def plans_list(request: Request, q: str = ""):
|
||||
db = CommonSessionLocal()
|
||||
@@ -152,7 +197,10 @@ def plan_create(request: Request, csrf_token: str = Form(...), code: str = Form(
|
||||
user, response = _require_user(request, db, "platform_plans.manage")
|
||||
if response:
|
||||
return response
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
create_platform_plan(db, code=code, name=name, target_account_type=target_account_type, billing_cycle=billing_cycle, base_amount=base_amount, gst_rate=gst_rate, description=description, feature_text=feature_text)
|
||||
return RedirectResponse(url="/platform-billing/plans", status_code=303)
|
||||
finally:
|
||||
@@ -190,7 +238,10 @@ def account_create(request: Request, csrf_token: str = Form(...), account_type:
|
||||
user, response = _require_user(request, db, "platform_billing.create")
|
||||
if response:
|
||||
return response
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
create_platform_account(db, account_type=account_type, account_code=account_code, display_name=display_name, tenant_id=int(tenant_id) if tenant_id else None, client_id=int(client_id) if client_id else None, consultant_id=int(consultant_id) if consultant_id else None, email=email, mobile=mobile, gstin=gstin, pan=pan, billing_address=billing_address, state=state, notes=notes, user_id=user.id)
|
||||
return RedirectResponse(url="/platform-billing/accounts", status_code=303)
|
||||
finally:
|
||||
@@ -228,7 +279,10 @@ def audit_firm_accounts_sync(request: Request, csrf_token: str = Form(...)):
|
||||
user, response = _require_user(request, db, "platform_subscriptions.manage")
|
||||
if response:
|
||||
return response
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
result = sync_audit_firm_billing_accounts(db, user_id=user.id)
|
||||
return RedirectResponse(url=f"/platform-billing/audit-firm-subscriptions?synced=created-{result['created']}-updated-{result['updated']}", status_code=303)
|
||||
finally:
|
||||
@@ -264,7 +318,10 @@ def audit_firm_generate_submit(request: Request, csrf_token: str = Form(...), su
|
||||
user, response = _require_user(request, db, "platform_billing.generate")
|
||||
if response:
|
||||
return response
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
result = generate_audit_firm_subscription_invoices(
|
||||
db,
|
||||
subscription_ids=subscription_ids,
|
||||
@@ -329,7 +386,10 @@ def client_dashboard_accounts_sync(request: Request, csrf_token: str = Form(...)
|
||||
user, response = _require_user(request, db, "platform_subscriptions.manage")
|
||||
if response:
|
||||
return response
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
result = sync_client_dashboard_billing_accounts(db, user_id=user.id)
|
||||
return RedirectResponse(url=f"/platform-billing/client-dashboard-subscriptions?synced=created-{result['created']}-updated-{result['updated']}", status_code=303)
|
||||
finally:
|
||||
@@ -343,7 +403,10 @@ def client_dashboard_subscription_create(request: Request, csrf_token: str = For
|
||||
user, response = _require_user(request, db, "platform_subscriptions.manage")
|
||||
if response:
|
||||
return response
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
create_platform_subscription(
|
||||
db,
|
||||
account_id=account_id,
|
||||
@@ -392,7 +455,10 @@ def client_dashboard_generate_submit(request: Request, csrf_token: str = Form(..
|
||||
user, response = _require_user(request, db, "platform_billing.generate")
|
||||
if response:
|
||||
return response
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
result = generate_client_dashboard_subscription_invoices(
|
||||
db,
|
||||
subscription_ids=subscription_ids,
|
||||
@@ -456,7 +522,10 @@ def consultant_accounts_sync(request: Request, csrf_token: str = Form(...)):
|
||||
user, response = _require_user(request, db, "platform_subscriptions.manage")
|
||||
if response:
|
||||
return response
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
result = sync_consultant_billing_accounts(db, user_id=user.id)
|
||||
return RedirectResponse(url=f"/platform-billing/consultant-subscriptions?synced=created-{result['created']}-updated-{result['updated']}", status_code=303)
|
||||
finally:
|
||||
@@ -470,7 +539,10 @@ def consultant_subscription_create(request: Request, csrf_token: str = Form(...)
|
||||
user, response = _require_user(request, db, "platform_subscriptions.manage")
|
||||
if response:
|
||||
return response
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
create_platform_subscription(
|
||||
db,
|
||||
account_id=account_id,
|
||||
@@ -519,7 +591,10 @@ def consultant_generate_submit(request: Request, csrf_token: str = Form(...), su
|
||||
user, response = _require_user(request, db, "platform_billing.generate")
|
||||
if response:
|
||||
return response
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
result = generate_consultant_subscription_invoices(
|
||||
db,
|
||||
subscription_ids=subscription_ids,
|
||||
@@ -580,7 +655,10 @@ def subscription_create(request: Request, csrf_token: str = Form(...), account_i
|
||||
user, response = _require_user(request, db, "platform_subscriptions.manage")
|
||||
if response:
|
||||
return response
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
create_platform_subscription(db, account_id=account_id, plan_id=plan_id, subscription_code=subscription_code, start_date=parse_date(start_date), end_date=parse_date(end_date), billing_cycle=billing_cycle, amount=amount, gst_rate=gst_rate, auto_generate_invoice=auto_generate_invoice == "on", notes=notes, user_id=user.id)
|
||||
return RedirectResponse(url="/platform-billing/subscriptions", status_code=303)
|
||||
finally:
|
||||
@@ -618,7 +696,10 @@ def invoice_create(request: Request, csrf_token: str = Form(...), account_id: in
|
||||
user, response = _require_user(request, db, "platform_billing.create")
|
||||
if response:
|
||||
return response
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
invoice = create_platform_invoice(
|
||||
db,
|
||||
account_id=account_id,
|
||||
@@ -660,7 +741,10 @@ def invoice_post(request: Request, invoice_id: int, csrf_token: str = Form(...))
|
||||
user, response = _require_user(request, db, "platform_billing.post")
|
||||
if response:
|
||||
return response
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
invoice = get_platform_invoice(db, invoice_id)
|
||||
if invoice and invoice.status == "DRAFT":
|
||||
post_platform_invoice(db, invoice, user.id)
|
||||
@@ -676,7 +760,10 @@ def payment_create(request: Request, invoice_id: int, csrf_token: str = Form(...
|
||||
user, response = _require_user(request, db, "platform_billing.payment.create")
|
||||
if response:
|
||||
return response
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
invoice = get_platform_invoice(db, invoice_id)
|
||||
if invoice:
|
||||
record_platform_payment(db, invoice=invoice, amount=amount, mode=mode, reference_no=reference_no, notes=notes, user_id=user.id)
|
||||
|
||||
@@ -25,6 +25,12 @@ from app.modules.system_settings.year_backup_service import build_year_backup_ex
|
||||
router = APIRouter(prefix="/system-settings", tags=["system-settings-ui"])
|
||||
|
||||
|
||||
def _csrf_rejected(request: Request):
|
||||
from app.core.http_responses import forbidden_response
|
||||
return forbidden_response(request, "CSRF validation failed")
|
||||
|
||||
|
||||
|
||||
def _base_ctx(request: Request, user, db, **ctx):
|
||||
base = {
|
||||
"request": request,
|
||||
@@ -172,6 +178,25 @@ def dashboard(request: Request):
|
||||
# -----------------------------
|
||||
# Tenant - System Admin only
|
||||
# -----------------------------
|
||||
|
||||
|
||||
# Phase 3 security hardening: list/index pages must not accept direct unsafe POSTs.
|
||||
# Existing create/update features continue to use their dedicated /new or action routes.
|
||||
@router.post("/tenants")
|
||||
def tenants_list_post_rejected(request: Request):
|
||||
return _csrf_rejected(request)
|
||||
|
||||
|
||||
@router.post("/branches")
|
||||
def branches_list_post_rejected(request: Request):
|
||||
return _csrf_rejected(request)
|
||||
|
||||
|
||||
@router.post("/financial-years")
|
||||
def financial_years_list_post_rejected(request: Request):
|
||||
return _csrf_rejected(request)
|
||||
|
||||
|
||||
@router.get("/tenants")
|
||||
def tenants_list(request: Request, q: str = "", page: int = 1, per_page: int = 10):
|
||||
db = CommonSessionLocal()
|
||||
@@ -222,7 +247,10 @@ def tenant_create_submit(
|
||||
default_storage_mode: str = Form("local_only"),
|
||||
csrf_token: str = Form(...),
|
||||
):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user = get_current_user(request, db=db)
|
||||
@@ -296,7 +324,10 @@ def tenant_edit_submit(
|
||||
is_active: str | None = Form(None),
|
||||
csrf_token: str = Form(...),
|
||||
):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user = get_current_user(request, db=db)
|
||||
@@ -432,7 +463,10 @@ def branch_create_submit(
|
||||
attendance_allowed_ip_csv: str = Form(""),
|
||||
csrf_token: str = Form(...),
|
||||
):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user = get_current_user(request, db=db)
|
||||
@@ -627,7 +661,10 @@ def branch_edit_submit(
|
||||
lockout_attempts: str = Form("5"),
|
||||
lockout_minutes: str = Form("15"),
|
||||
):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user = get_current_user(request, db=db)
|
||||
@@ -881,7 +918,10 @@ async def branding_save(
|
||||
favicon_file: UploadFile | None = File(None),
|
||||
csrf_token: str = Form(...),
|
||||
):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user = get_current_user(request, db=db)
|
||||
@@ -1043,7 +1083,10 @@ def financial_year_create_submit(
|
||||
is_current: str | None = Form(None),
|
||||
csrf_token: str = Form(...),
|
||||
):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user = get_current_user(request, db=db)
|
||||
@@ -1140,7 +1183,10 @@ def financial_year_edit_submit(
|
||||
end_date: str = Form(...),
|
||||
csrf_token: str = Form(...),
|
||||
):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user = get_current_user(request, db=db)
|
||||
@@ -1182,7 +1228,10 @@ def financial_year_edit_submit(
|
||||
|
||||
@router.post("/financial-years/{fy_id}/make-current")
|
||||
def financial_year_make_current(request: Request, fy_id: int, csrf_token: str = Form(...)):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user = get_current_user(request, db=db)
|
||||
@@ -1209,7 +1258,10 @@ def financial_year_make_current(request: Request, fy_id: int, csrf_token: str =
|
||||
|
||||
@router.post("/financial-years/{fy_id}/lock")
|
||||
def financial_year_lock(request: Request, fy_id: int, csrf_token: str = Form(...)):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user = get_current_user(request, db=db)
|
||||
@@ -1232,7 +1284,10 @@ def financial_year_lock(request: Request, fy_id: int, csrf_token: str = Form(...
|
||||
|
||||
@router.post("/financial-years/{fy_id}/unlock")
|
||||
def financial_year_unlock(request: Request, fy_id: int, csrf_token: str = Form(...)):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user = get_current_user(request, db=db)
|
||||
@@ -1289,7 +1344,10 @@ def financial_year_backup_page(request: Request, fy_id: int):
|
||||
|
||||
@router.post("/financial-years/{fy_id}/backup/export")
|
||||
def financial_year_backup_export_submit(request: Request, fy_id: int, csrf_token: str = Form(...)):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user = get_current_user(request, db=db)
|
||||
|
||||
@@ -14,6 +14,12 @@ from app.modules.work_detail.service import get_task_for_action, load_unified_en
|
||||
router = APIRouter(prefix="/work", tags=["unified-work-detail-ui"])
|
||||
|
||||
|
||||
def _csrf_rejected(request: Request):
|
||||
from app.core.http_responses import forbidden_response
|
||||
return forbidden_response(request, "CSRF validation failed")
|
||||
|
||||
|
||||
|
||||
def _base_ctx(request: Request, db, user, **ctx):
|
||||
base = {
|
||||
"request": request,
|
||||
@@ -56,6 +62,15 @@ def unified_work_board(request: Request):
|
||||
db.close()
|
||||
|
||||
|
||||
|
||||
|
||||
# Phase 3 security hardening: /work/engagements is a listing alias, not a create endpoint.
|
||||
# Existing engagement creation remains in the services module.
|
||||
@router.post("/engagements")
|
||||
def unified_engagements_post_rejected(request: Request):
|
||||
return _csrf_rejected(request)
|
||||
|
||||
|
||||
@router.get("/engagements")
|
||||
def unified_engagements_index(request: Request):
|
||||
"""Compatibility alias for /work/engagements.
|
||||
@@ -79,6 +94,8 @@ def unified_engagement_detail(request: Request, engagement_id: int):
|
||||
user = get_current_user(request, db=db)
|
||||
if not user:
|
||||
return RedirectResponse(url="/login", status_code=303)
|
||||
if "Client" in set(get_user_roles(db, user.id)):
|
||||
return forbidden_response(request, "Access denied")
|
||||
detail = load_unified_engagement_detail(db, request=request, user=user, engagement_id=engagement_id)
|
||||
if not detail:
|
||||
return not_found_response(request, "Work engagement not found or access denied")
|
||||
@@ -92,7 +109,10 @@ def unified_engagement_detail(request: Request, engagement_id: int):
|
||||
|
||||
@router.post("/tasks/{task_id}/status")
|
||||
async def unified_task_status_update(request: Request, task_id: int, csrf_token: str = Form(...), status: str = Form(...), priority: str = Form("")):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user = get_current_user(request, db=db)
|
||||
@@ -122,7 +142,10 @@ async def unified_task_comment_add(
|
||||
visibility: str = Form("internal"),
|
||||
message: str = Form(""),
|
||||
):
|
||||
try:
|
||||
validate_csrf(request, csrf_token)
|
||||
except PermissionError:
|
||||
return _csrf_rejected(request)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user = get_current_user(request, db=db)
|
||||
|
||||
Reference in New Issue
Block a user