From 375d0b132e0d180877aec665e77087aca0359bd9 Mon Sep 17 00:00:00 2001 From: velz Date: Fri, 9 Jan 2026 16:02:46 +0530 Subject: [PATCH] FEAT_RBAC --- .env.sample | 2 + .htaccess | 2 +- app/Config/Acl.php | 85 +++++++++++++++ app/Config/Filters.php | 5 + app/Controllers/LoginController.php | 8 +- app/Filters/AclFilter.php | 156 ++++++++++++++++++++++++++++ app/Libraries/AuthLogout.php | 2 +- 7 files changed, 255 insertions(+), 5 deletions(-) create mode 100644 app/Config/Acl.php create mode 100644 app/Filters/AclFilter.php diff --git a/.env.sample b/.env.sample index 36880c7..c302d13 100644 --- a/.env.sample +++ b/.env.sample @@ -183,3 +183,5 @@ CORS_MAX_AGE=7200 CORS_DEBUG=true APP_SIGNATURE = +TOKENTIMEOUT = +JWT_SECRET = \ No newline at end of file diff --git a/.htaccess b/.htaccess index 6782611..c9db48e 100755 --- a/.htaccess +++ b/.htaccess @@ -7,7 +7,7 @@ Options -Indexes ## ADDED for - block any script execution inside folder of public - + Deny from all # Disable PHP engine diff --git a/app/Config/Acl.php b/app/Config/Acl.php new file mode 100644 index 0000000..1fbea05 --- /dev/null +++ b/app/Config/Acl.php @@ -0,0 +1,85 @@ + ['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' => [ HEAD_ROLE_ID,ADMIN_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' => [] + ], + + // ===================== 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], + // ===================== 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 969dadf..e22e4af 100755 --- a/app/Config/Filters.php +++ b/app/Config/Filters.php @@ -18,6 +18,9 @@ use App\Filters\AuthJWT; use App\Filters\Cors; use App\Filters\GlobalPostFileUploadGuard; use App\Filters\SecurityInputFilter; +use App\Filters\AclFilter; + + class Filters extends BaseConfig { @@ -43,6 +46,7 @@ class Filters extends BaseConfig 'appSignature' => VerifyAppSignature::class, 'GlobalPostFileUploadGuard' => GlobalPostFileUploadGuard::class, 'SecurityInputFilter' => SecurityInputFilter::class, + 'AclFilter' => AclFilter::class, ]; @@ -56,6 +60,7 @@ class Filters extends BaseConfig public array $globals = [ 'before' => [ 'HttpRequestLog' => ['except' => 'cli/*'], + 'AclFilter' => ['except' => 'login', 'logout', 'auth/*', 'oauth2callback', 'download-*', 'claim-form-download', 'claims-feedback-form', 'autobookstackLogin'], 'Cors', 'SecurityInputFilter', 'GlobalPostFileUploadGuard', diff --git a/app/Controllers/LoginController.php b/app/Controllers/LoginController.php index eb3d98e..aa5d9a8 100755 --- a/app/Controllers/LoginController.php +++ b/app/Controllers/LoginController.php @@ -31,20 +31,22 @@ class LoginController extends BaseController public function receiveGoogleOAuthResponse() { $UserModel = new UserModel(); - //echo 'DONE';die(); + // echo 'DONE';die(); if ($this->request->getGet('code')) { $code = (string) $this->request->getGet('code'); log_message('error', 'Get OAuth Responce Code Sucessfully'); log_message('error', 'OAuthResponceCode : `'.$code.'`'); $value = googleOAuthLogin($this->request->getGet('code')); + // log_message('error', "OAuthResponceCode :" . json_encode($value)); if($value){ + // print_r($value);//die; $user = $UserModel->getUserByEmail($value->email); if($user){ if($user->is_active !== '0'){ $user_team = $UserModel->getUserTeamsByUserID($user->id); // dd($user_team); - + session()->regenerate(true); $session_data = [ 'isLoggedIn' => True , 'userid' => $user->id, @@ -66,7 +68,7 @@ class LoginController extends BaseController log_message('error', 'User Login Sucessfully'); $this->getUserDeviceInfo($user->id, 'NhanceUser'); - return AuthLogout::logout(); + return redirect()->to(base_url('/dashboard/view')); }else{ log_message('error', 'User Not Active'); diff --git a/app/Filters/AclFilter.php b/app/Filters/AclFilter.php new file mode 100644 index 0000000..0f6e0c1 --- /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/Libraries/AuthLogout.php b/app/Libraries/AuthLogout.php index cfb1005..ac7786c 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'],