From b078bfe837b87851ea133db3ec719724144e3e64 Mon Sep 17 00:00:00 2001 From: velz Date: Fri, 9 Jan 2026 15:59:37 +0530 Subject: [PATCH 1/2] FEAT_RBAC --- .env.sample | 2 + app/Config/Acl.php | 157 +++++++++++++++++++++++++++++++++ app/Config/Filters.php | 3 + app/Filters/AclFilter.php | 156 ++++++++++++++++++++++++++++++++ app/Helpers/session_helper.php | 3 +- app/Libraries/AuthLogout.php | 2 +- app/Views/errors/404.php | 2 +- 7 files changed, 322 insertions(+), 3 deletions(-) create mode 100644 app/Config/Acl.php create mode 100644 app/Filters/AclFilter.php diff --git a/.env.sample b/.env.sample index ebc9ab1d..f9ab9b80 100755 --- a/.env.sample +++ b/.env.sample @@ -108,3 +108,5 @@ CORS_MAX_AGE=7200 CORS_DEBUG=true APP_SIGNATURE = +TOKENTIMEOUT = +JWT_SECRET = \ No newline at end of file diff --git a/app/Config/Acl.php b/app/Config/Acl.php new file mode 100644 index 00000000..f2f4ecca --- /dev/null +++ b/app/Config/Acl.php @@ -0,0 +1,157 @@ + ['public' => true], + '#^/logout#' => ['public' => true], + '#^/auth#' => ['public' => true], + '#^/oauth2callback#' => ['public' => true], + '#^/loginPos#' => ['public' => true], + '#^/getVerifyPosMobileNo#' => ['public' => true], + '#^/getVerifiedPosUserData#' => ['public' => true], + '#^/swagger#' => ['roles' => [ADMIN_ROLE_ID]], + '#^/fedeploy#' => ['roles' => [ADMIN_ROLE_ID]], + + // ===================== PUBLIC DOWNLOADS / FORMS ===================== + '#^/download-#' => ['public' => true], + '#^/claim-form-download#' => ['public' => true], + '#^/claims-feedback-form#' => ['public' => true], + '#^/autobookstackLogin#' => ['public' => true], + + // ===================== DASHBOARD ===================== + '#^/dashboard#' => [ + 'roles' => [ ADMIN_ROLE_ID,HEAD_ROLE_ID, MANAGER_ROLE_ID, STAFF_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID], + 'teams' => [] + ], + + // ===================== USER MANAGEMENT ===================== + '#^/user#' => [ + 'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID], + 'teams' => [] + ], + + // ===================== CLIENT ===================== + '#^/client#' => [ + 'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID], + 'teams' => [] + ], + + // ===================== EMPLOYEE / ENROLLMENT ===================== + '#^/employee#' => [ + 'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID], + 'teams' => [ENROLLMENT_TEAM_ID] + ], + + // ===================== MASTERS ===================== + '#^/master#' => [ + 'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID], + 'teams' => [] + ], + '#^/util#' => [ + 'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID,STAFF_ROLE_ID], + 'teams' => [] + ], + + // ===================== POLICY TRANSACTION / BDS ===================== + '#^/policy_tranction#' => [ + 'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID], + 'teams' => [FINANCE_TEAM_ID,POS_TEAM_ID] + ], + '#^/bds_upload#' => [ + 'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID], + 'teams' => [FINANCE_TEAM_ID,POS_TEAM_ID] + ], + + // ===================== REPORTS ===================== + '#^/bdsReport#' => [ + 'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID], + 'teams' => [FINANCE_TEAM_ID,POS_TEAM_ID] + ], + + // ===================== PAYOUT / COMMISSION ===================== + '#^/payout#' => [ + 'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID], + 'teams' => [FINANCE_TEAM_ID,POS_TEAM_ID] + ], + '#^/commission#' => [ + 'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID], + 'teams' => [FINANCE_TEAM_ID,POS_TEAM_ID] + ], + + // ===================== CLAIMS / TICKETS ===================== + '#^/ticket#' => [ + 'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID], + 'teams' => [CLAIMS_TEAM_ID] + ], + '#^/claim_mis#' => [ + 'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID], + 'teams' => [CLAIMS_TEAM_ID] + ], + + // ===================== LEADS / SALES ===================== + '#^/leads#' => [ + 'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID], + 'teams' => [BUSINESS_SUPPORT_TEAM_ID, SALES_TEAM_ID] + ], + '#^/rfq#' => [ + 'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID], + 'teams' => [SALES_TEAM_ID] + ], + '#^/sales#' => [ + 'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID], + 'teams' => [SALES_TEAM_ID] + ], + + // ===================== CMS / CONTENT ===================== + '#^/add_image_index#' => [ + 'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID,STAFF_ROLE_ID], + 'teams' => [] + ], + '#^/frontend_content#' => [ + 'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID,STAFF_ROLE_ID], + 'teams' => [] + ], + '#^/FAQ#' => [ + 'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID,STAFF_ROLE_ID], + 'teams' => [] + ], + + // ===================== LOGS ===================== + '#^/logs#' => [ + 'roles' => [ADMIN_ROLE_ID], + 'teams' => [] + ], + + // ===================== INTERNAL TEST ===================== + '#^/test#' => [ + 'roles' => [ADMIN_ROLE_ID], + 'teams' => [] + ], + + // ===================== API (JWT / SIGNED) ===================== + '#^/api#' => ['public' => true], + '#^/employeeRest#' => ['public' => true], + '#^/clientApi#' => ['public' => true], + + // ===================== WEBHOOKS / 3RD PARTY ===================== + '#^/dispatchWebhookData#' => ['public' => true], + '#^/retrieveWebhookData#' => ['public' => true], + // '#^/ICICI#' => ['public' => true], + // '#^/Vidal#' => ['public' => true], + // '#^/MediAssist#' => ['public' => true], + + // ===================== CLI ===================== + '#^/cli/#' => ['public' => true], + + // ===================== DEFAULT DENY (ZERO TRUST) ===================== + '#^/#' => [ + 'roles' => [ADMIN_ROLE_ID], + 'teams' => [] + ], + ]; +} diff --git a/app/Config/Filters.php b/app/Config/Filters.php index d552bb46..7476309d 100755 --- a/app/Config/Filters.php +++ b/app/Config/Filters.php @@ -18,6 +18,7 @@ use App\Filters\VerifyAppSignature; use App\Filters\Cors; use App\Filters\SecurityInputFilter; use App\Filters\GlobalPostFileUploadGuard; +use App\Filters\AclFilter; use App\Filters\AuthJWT; @@ -47,6 +48,7 @@ class Filters extends BaseConfig 'Cors' => Cors::class, 'SecurityInputFilter' => SecurityInputFilter::class, 'GlobalPostFileUploadGuard' => GlobalPostFileUploadGuard::class, + 'AclFilter' => AclFilter::class, ]; /** @@ -60,6 +62,7 @@ class Filters extends BaseConfig 'before' => [ 'HttpRequestLog' => ['except' => 'cli/*'], 'Cors', + 'AclFilter' => ['except' => 'login', 'logout', 'auth/*', 'oauth2callback', 'download-*', 'claim-form-download', 'claims-feedback-form', 'autobookstackLogin'], 'SecurityInputFilter', 'GlobalPostFileUploadGuard' // 'csrf', diff --git a/app/Filters/AclFilter.php b/app/Filters/AclFilter.php new file mode 100644 index 00000000..0f6e0c17 --- /dev/null +++ b/app/Filters/AclFilter.php @@ -0,0 +1,156 @@ +getPath(), '/'); + + // Base path: /PHP828APPS/ruc/nhance + $basePath = rtrim(parse_url(base_url(), PHP_URL_PATH), '/'); + + // Remove base path + if ($basePath && str_starts_with($fullPath, $basePath)) { + $path = substr($fullPath, strlen($basePath)); + } else { + $path = $fullPath; + } + + // Remove index.php if present + if (str_starts_with($path, '/index.php')) { + $path = substr($path, strlen('/index.php')); + } + + // Normalize + $path = '/' . ltrim($path, '/'); + + // Fallback + if ($path === '') { + $path = '/'; + } + + // echo'
BASH PATH: ' . base_url(); + // echo'
ACL RAW PATH: ' . $fullPath; + // echo'
ACL BASE PATH: ' . $basePath; + // echo'
ACL FINAL PATH: ' . $path; + // ===================== LOAD ACL ===================== + $acl = new Acl(); + $rules = $acl->rules; + // print_rr($rules);die; + // ===================== MATCH RULE ===================== + $matchedRule = null; + + foreach ($rules as $pattern => $rule) { + // echo "$pattern".'---------
'; + if (preg_match($pattern, $path)) { + // echo "matched - $pattern"; + $matchedRule = $rule; + break; // FIRST MATCH WINS + } + } + + // print_r($matchedRule);//die; + // ===================== NO RULE = DENY ===================== + if ($matchedRule === null) { + return $this->deny($path, 'No ACL rule matched'); + } + + // ===================== PUBLIC ROUTE ===================== + if (!empty($matchedRule['public'])) { + return; // ALLOW + } + + // ===================== AUTH CHECK ===================== + if (!check_session()) { + // For API requests return 401 JSON + if ($request->isAJAX() || str_starts_with($path, '/api') || str_starts_with($path, '/employeeRest')) { + return service('response') + ->setStatusCode(401) + ->setJSON(['error' => 'Unauthorized']); + } + + // For web redirect to login + return AuthLogout::logout(); + } + + // ===================== GET USER CONTEXT ===================== + + $userRole = check_role(); // + $userTeams = user_team(); // must return array of TEAM IDs + + $allowedRoles = $matchedRule['roles'] ?? []; + $allowedTeams = $matchedRule['teams'] ?? []; + // ===================== ROLE FIRST ===================== + if (!empty($allowedRoles) && in_array((int)$userRole, $allowedRoles, true)) { + return; // ALLOW + } + // ===================== TEAM FALLBACK ===================== + if (!empty($allowedTeams) && is_array($userTeams)) { + foreach ($userTeams as $teamId) { + if (in_array($teamId, $allowedTeams, true)) { + return; // ALLOW + } + } + } + // ===================== DENY ===================== + return $this->deny($path, 'Role/Team not permitted'); + } + + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) + { + // nothing + } + + // ===================== DENY HANDLER ===================== + protected function deny(string $path, string $reason) + { + log_message('error', 'ACL BLOCKED: {user} {path} - {reason}', [ + 'user' => session()->get('userid') ?? 'guest', + 'path' => $path, + 'reason' => $reason, + ]); + + // API / AJAX → JSON + $request = service('request'); + if ($request->isAJAX() || str_starts_with($path, '/api') || str_starts_with($path, '/employeeRest')) { + return service('response') + ->setStatusCode(403) + ->setJSON([ + 'error' => 'Forbidden', + 'message' => 'You do not have permission to access this resource' + ]); + } + + $response = service('response'); + $response->setStatusCode(403); + $response->setBody(view('errors/404', [ + 'message' => '403 Access denied - You do not have permission to access this resource' + ])); + + return $response; + // Web → nice 403 page or simple text + return service('response') + ->setStatusCode(403) + ->setBody('403 Forbidden - Access denied - You do not have permission to access this resource'); + } +} diff --git a/app/Helpers/session_helper.php b/app/Helpers/session_helper.php index 640de0e7..bd54744c 100755 --- a/app/Helpers/session_helper.php +++ b/app/Helpers/session_helper.php @@ -118,7 +118,8 @@ if (!function_exists('check_role')) { { // $ci =& get_instance(); $session = \Config\Services::session(); - return $session->get('role'); + return $role_id = isset(get_session_userdata()->role) ? get_session_userdata()->role : null; + } } diff --git a/app/Libraries/AuthLogout.php b/app/Libraries/AuthLogout.php index cfb10051..ac7786cb 100644 --- a/app/Libraries/AuthLogout.php +++ b/app/Libraries/AuthLogout.php @@ -22,7 +22,7 @@ class AuthLogout setcookie( session_name(), // DO NOT hardcode cookie name - '', + null, time() - 42000, $params['path'], $params['domain'], diff --git a/app/Views/errors/404.php b/app/Views/errors/404.php index f271c312..69c6d42e 100755 --- a/app/Views/errors/404.php +++ b/app/Views/errors/404.php @@ -162,7 +162,7 @@
-
+

From 20d63f09cd8d5e609942a75f1c41b1a5e2c234bb Mon Sep 17 00:00:00 2001 From: velz Date: Fri, 9 Jan 2026 18:38:12 +0530 Subject: [PATCH 2/2] FIX_VALIDATE_BUSI_INPUT_VALUES --- app/Config/Autoload.php | 2 +- app/Controllers/ClientController.php | 110 ++++++++++++++++-- .../sanitizeInputArrayAdvanced_helper.php | 84 +++++++++++++ 3 files changed, 184 insertions(+), 12 deletions(-) create mode 100644 app/Helpers/sanitizeInputArrayAdvanced_helper.php diff --git a/app/Config/Autoload.php b/app/Config/Autoload.php index 62b24b8d..58edbca1 100755 --- a/app/Config/Autoload.php +++ b/app/Config/Autoload.php @@ -101,6 +101,6 @@ class Autoload extends AutoloadConfig * @phpstan-var list */ public $helpers = ['uuid','session','utility', 'form', 'url', 'oauth', 'fileupload', - 'excel_import_export', 'file', 'drive','ExcelSanitizeHelper', 'api_helper','exception','sms_helper' + 'excel_import_export', 'file', 'drive','ExcelSanitizeHelper', 'api_helper','exception','sms_helper','sanitizeInputArrayAdvanced' ]; } diff --git a/app/Controllers/ClientController.php b/app/Controllers/ClientController.php index 0b6470bc..c5900ca0 100755 --- a/app/Controllers/ClientController.php +++ b/app/Controllers/ClientController.php @@ -736,14 +736,102 @@ class ClientController extends AdminController public function saveDeposit() { + + $rules = [ + + 'amount' => [ + 'rules' => 'required|numeric|greater_than_equal_to[0]', + 'errors' => [ + 'required' => 'Amount is required', + 'numeric' => 'Amount must be a valid number', + 'greater_than_equal_to' => 'Amount cannot be negative', + ] + ], + + 'client_id' => [ + 'rules' => 'required|is_natural_no_zero', + 'errors' => [ + 'required' => 'Client ID is required', + 'is_natural_no_zero' => 'Client ID must be a positive integer', + ] + ], + + 'insurer_id' => [ + 'rules' => 'required|is_natural_no_zero', + 'errors' => [ + 'required' => 'Insurer ID is required', + 'is_natural_no_zero' => 'Insurer ID must be a positive integer', + ] + ], + + 'cd_ac_pk' => [ + 'rules' => 'required|is_natural_no_zero', + 'errors' => [ + 'required' => 'Account PK is required', + 'is_natural_no_zero' => 'Account PK must be a positive integer', + ] + ], + + 'cd_ac_no' => [ + 'rules' => 'required|is_natural_no_zero', + 'errors' => [ + 'required' => 'Account number is required', + 'is_natural_no_zero' => 'must be a positive integer', + ] + ], + + 'sub_type_id' => [ + 'rules' => 'required|is_natural_no_zero', + 'errors' => [ + 'required' => 'Sub type ID is required', + 'is_natural_no_zero' => 'Sub type ID must be a positive integer', + ] + ], + + 'description' => [ + 'rules' => 'required|string|min_length[3]|max_length[255]', + 'errors' => [ + 'required' => 'Description is required', + 'string' => 'Description must be text', + 'min_length' => 'Description must be at least 3 characters', + 'max_length' => 'Description must not exceed 255 characters', + ] + ], + + 'transaction_type' => [ + 'rules' => 'required|in_list[Credit,Debit]', + 'errors' => [ + 'required' => 'Transaction type is required', + 'in_list' => 'Transaction type must be either credit or debit', + ] + ], + + ]; + + + if (! $this->validate($rules)) { + return $this->response + ->setStatusCode(400) + ->setJSON([ + 'status' => 'error', + 'message' => 'Input validation failed', + 'errors' => $this->validator->getErrors() + ]); + } + + //sanitize the post params + $post_data = $this->request->getPost(); + $sanitized_post_data = sanitizeInputArrayAdvanced($post_data); // Retrieve form data from POST request $loggedInUserID = get_session_userid(); + // print_rr($sanitized_post_data);die(); + $client_id = $sanitized_post_data['client_id']; + $insurer_id = $sanitized_post_data['insurer_id']; + $record_date = $sanitized_post_data['record_date']; + $cd_ac_pk = $sanitized_post_data['cd_ac_pk']; + $cd_ac_no = $sanitized_post_data['cd_ac_no']; - $client_id = $this->request->getPost('client_id'); - $insurer_id = $this->request->getPost('insurer_id'); - $record_date = $this->request->getPost('record_date'); - $cd_ac_pk = $this->request->getPost('cd_ac_pk'); - $cd_ac_no = $this->request->getPost('cd_ac_no'); + // $CD_Account_Number = $this->CDMasterModel // ->where('client_id', $client_id) @@ -761,16 +849,16 @@ class ClientController extends AdminController } $data = [ - 'amount' => $this->request->getPost('amount'), - 'sub_type_id' => $this->request->getPost('sub_type_id'), - 'client_id' => $this->request->getPost('client_id'), + 'amount' => $sanitized_post_data['amount'], + 'sub_type_id' => $sanitized_post_data['sub_type_id'], + 'client_id' => $sanitized_post_data['client_id'], 'client_policy_id' => null, 'cd_ac_no' => $cd_ac_no ?? null, 'cd_ac_pk' => $cd_ac_pk ?? null, 'endorsement_no' => null, - 'insurer_id' => $this->request->getPost('insurer_id'), - 'description' => $this->request->getPost('description'), - 'transaction_type' => $this->request->getPost('transaction_type') ?: 'Credit', + 'insurer_id' => $sanitized_post_data['insurer_id'], + 'description' => $sanitized_post_data['description'], + 'transaction_type' => $sanitized_post_data['transaction_type'] ?: 'Credit', 'updated_by' => 1, 'record_date' => $record_date ]; diff --git a/app/Helpers/sanitizeInputArrayAdvanced_helper.php b/app/Helpers/sanitizeInputArrayAdvanced_helper.php new file mode 100644 index 00000000..79d7df9b --- /dev/null +++ b/app/Helpers/sanitizeInputArrayAdvanced_helper.php @@ -0,0 +1,84 @@ + $v) { + + if (is_array($v)) { + $data[$k] = sanitizeInputArrayAdvanced($v, $htmlAllowedFields); + continue; + } + + if (!is_string($v)) { + continue; + } + + // 1. Unicode normalization (prevents homoglyph attacks) + if (class_exists('Normalizer')) { + $v = \Normalizer::normalize($v, \Normalizer::FORM_C); + } + + // 2. Remove NULL bytes & control chars + $v = preg_replace('/[\x00-\x1F\x7F]/u', '', $v); + + // 3. Remove invisible unicode chars (zero width, etc) + $v = preg_replace('/[\x{200B}-\x{200F}\x{202A}-\x{202E}\x{2060}-\x{206F}]/u', '', $v); + + // 4. Decode HTML entities (so hidden payloads are exposed) + $v = html_entity_decode($v, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + + // 5. Trim + $v = trim($v); + + // 6. If this field is NOT allowed to contain HTML → strip aggressively + if (!in_array($k, $htmlAllowedFields, true)) { + + // Remove all tags + $v = strip_tags($v); + + // Kill any leftover JS protocol + $v = preg_replace('/(javascript:|data:|vbscript:)/i', '', $v); + + } else { + // This is HTML-allowed field → run HTML sanitizer + $v = sanitizeTrustedHtml($v); + } + + $data[$k] = $v; + } + + return $data; +} + +function sanitizeTrustedHtml(string $html): string +{ + // Allowed tags for email templates + $allowedTags = '