Phase 3 harden CSRF invalid POST and work access handling

This commit is contained in:
A R R R Associates
2026-06-26 11:41:00 +05:30
parent 4ce5556e9d
commit fc31a5f4e9
7 changed files with 503 additions and 94 deletions
+213 -48
View File
@@ -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()
validate_csrf(request, form.get("csrf_token"))
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(...),
):
validate_csrf(request, csrf_token)
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(...),
):
validate_csrf(request, csrf_token)
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(...)):
validate_csrf(request, csrf_token)
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(...)):
validate_csrf(request, csrf_token)
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(...)):
validate_csrf(request, csrf_token)
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(...)):
validate_csrf(request, csrf_token)
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(...)):
validate_csrf(request, csrf_token)
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(...),
):
validate_csrf(request, csrf_token)
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(...),
):
validate_csrf(request, csrf_token)
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(...)):
validate_csrf(request, csrf_token)
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()
validate_csrf(request, form.get("csrf_token"))
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()
validate_csrf(request, form.get("csrf_token"))
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(...)):
validate_csrf(request, csrf_token)
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(...)):
validate_csrf(request, csrf_token)
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()
validate_csrf(request, form.get("csrf_token"))
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(...)):
validate_csrf(request, csrf_token)
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(...)):
validate_csrf(request, csrf_token)
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(...)):
validate_csrf(request, csrf_token)
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(...)):
validate_csrf(request, csrf_token)
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()
validate_csrf(request, form.get("csrf_token"))
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()
validate_csrf(request, form.get("csrf_token"))
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()
validate_csrf(request, form.get("csrf_token"))
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(...)):
validate_csrf(request, csrf_token)
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(...)):
validate_csrf(request, csrf_token)
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()
validate_csrf(request, form.get("csrf_token"))
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()
validate_csrf(request, form.get("csrf_token"))
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()
validate_csrf(request, form.get("csrf_token"))
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(...)):
validate_csrf(request, csrf_token)
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(...)):
validate_csrf(request, csrf_token)
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(...)):
validate_csrf(request, csrf_token)
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(...)):
validate_csrf(request, csrf_token)
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()
validate_csrf(request, csrf_token)
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()
validate_csrf(request, csrf_token)
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()
validate_csrf(request, csrf_token)
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()
validate_csrf(request, csrf_token)
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()
validate_csrf(request, form.get("csrf_token"))
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(...)):
validate_csrf(request, csrf_token)
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()
validate_csrf(request, form.get("csrf_token"))
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(...)):
validate_csrf(request, csrf_token)
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(...),
):
validate_csrf(request, csrf_token)
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(...),
):
validate_csrf(request, csrf_token)
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(...),
):
validate_csrf(request, csrf_token)
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(...)):
validate_csrf(request, csrf_token)
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(...)):
validate_csrf(request, csrf_token)
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()
validate_csrf(request, form.get("csrf_token"))
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()
validate_csrf(request, form.get("csrf_token"))
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()
validate_csrf(request, form.get("csrf_token"))
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)