Add subuser permission evaluation system and extend subuser-related route handling
- Introduce `hasPermission` method in `subusers_o` for permission checks tied to customer context. - Update `/subusers/me` route to return subuser grants with normalized permissions and metadata. - Add `get_subuser_customer_number_target` in `authentication` to resolve customer context from request headers. - Refactor route-level permission checks to handle subuser grants dynamically. - Introduce CLI test scripts for subuser grants and permission node mappings. - Add test coverage for subuser grants and permission nodes in new test classes.
This commit is contained in:
@@ -15,6 +15,11 @@ trait route_t
|
||||
{
|
||||
protected array $permissions = [];
|
||||
private string $route;
|
||||
/**
|
||||
* Lightweight per-request caches to avoid repeated DB/auth checks during a single request lifecycle.
|
||||
*/
|
||||
protected static array $__perm_user_cache = [];
|
||||
protected static array $__perm_subuser_grant_cache = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -282,63 +287,102 @@ trait route_t
|
||||
}
|
||||
|
||||
/**
|
||||
* Require permission
|
||||
* @param string|permission_node $permission
|
||||
* @return bool
|
||||
* Resolve customer number for a subuser permission context.
|
||||
* Order of precedence:
|
||||
* - Explicit `$customer_number` argument if provided
|
||||
* - Main authenticated user context (if available)
|
||||
* - Request header X-Customer-Number
|
||||
* - Query/post parameter `customer_number`
|
||||
*/
|
||||
public function requirePermission(string|permission_node $permission): bool
|
||||
private function resolveCustomerNumberForSubuser(authentication $auth, ?int $customer_number = null): ?int
|
||||
{
|
||||
if ($customer_number !== null) {
|
||||
return (int)$customer_number;
|
||||
}
|
||||
$user_ctx = $auth->get_user();
|
||||
if ($user_ctx !== false && isset($user_ctx->customer_number)) {
|
||||
return (int)$user_ctx->customer_number->value();
|
||||
}
|
||||
$headers = function_exists('getallheaders') ? getallheaders() : [];
|
||||
if (isset($headers['X-Customer-Number'])) {
|
||||
return (int)$headers['X-Customer-Number'];
|
||||
}
|
||||
if (isset($_GET['customer_number'])) {
|
||||
return (int)$_GET['customer_number'];
|
||||
}
|
||||
if (isset($_POST['customer_number'])) {
|
||||
return (int)$_POST['customer_number'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Centralized permission evaluation used by both requirePermission and hasPermission.
|
||||
* - Honors subusers permission nodes without falling back to classic user permissions when a node is defined.
|
||||
* - Supports explicit customer_number overrides and auto-detection fallback.
|
||||
* - Uses simple per-request caches for efficiency.
|
||||
*/
|
||||
private function evaluatePermission(string|permission_node $permission, ?int $customer_number, bool $throwOnDeny): bool
|
||||
{
|
||||
global $response;
|
||||
// Check if the users authorization token has the required permission
|
||||
try {
|
||||
// If permission is a node and a subuser is authenticated, evaluate node-based grant first
|
||||
$auth = new authentication();
|
||||
|
||||
// If permission is a node and a subuser is authenticated, evaluate node-based grant first
|
||||
$subuser = $auth->get_subuser();
|
||||
if ($subuser !== false && $permission instanceof permission_node && $permission->subusers_node_key !== null) {
|
||||
// Determine customer number context
|
||||
$customer_number = null;
|
||||
$user_ctx = $auth->get_user();
|
||||
if ($user_ctx !== false && isset($user_ctx->customer_number)) {
|
||||
$customer_number = (int)$user_ctx->customer_number->value();
|
||||
}
|
||||
if ($customer_number === null) {
|
||||
// Try to resolve from headers or request parameters as fallback
|
||||
$headers = getallheaders();
|
||||
if (isset($headers['X-Customer-Number'])) {
|
||||
$customer_number = (int)$headers['X-Customer-Number'];
|
||||
} elseif (isset($_GET['customer_number'])) {
|
||||
$customer_number = (int)$_GET['customer_number'];
|
||||
} elseif (isset($_POST['customer_number'])) {
|
||||
$customer_number = (int)$_POST['customer_number'];
|
||||
}
|
||||
}
|
||||
if ($customer_number === null) {
|
||||
$resolvedCustomer = $this->resolveCustomerNumberForSubuser($auth, $customer_number);
|
||||
if ($resolvedCustomer === null) {
|
||||
(new logs_o())->add('global', 'global', 1, $subuser->id ?? 0, 'PERMISSION_DENIED', 'Missing customer context for subuser permission evaluation: ' . $permission->permission);
|
||||
$response->error('Permission denied. Missing customer context for subuser.', 403);
|
||||
if ($throwOnDeny) {
|
||||
$response->error('Permission denied. Missing customer context for subuser.', 403);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
$grant = new subuser_user_grant((int)$subuser->id, (int)$customer_number);
|
||||
if ($grant->hasNode($permission->subusers_node_key)) {
|
||||
return true;
|
||||
// Expose resolved target customer in response meta for subuser requests
|
||||
$response->add_meta('target_customer_number', (int)$resolvedCustomer);
|
||||
$subuser_has_permission = $subuser->hasPermission($permission->subusers_node_key);
|
||||
if (!$subuser_has_permission && $throwOnDeny) {
|
||||
(new logs_o())->add('global', 'global', 1, $subuser->id ?? 0, 'PERMISSION_DENIED', 'Permission denied via subuser node: ' . $permission->permission . ' (Node: ' . $permission->subusers_node_key->name . ', Customer: ' . $resolvedCustomer . ')');
|
||||
$response->error('Permission denied for subuser. Missing permission: ' . $permission->permission . ' (Customer context: ' . $resolvedCustomer . ')', 403);
|
||||
}
|
||||
(new logs_o())->add('global', 'global', 1, $subuser->id ?? 0, 'PERMISSION_DENIED', 'Permission denied for subuser. Missing node: ' . $permission->subusers_node_key->name);
|
||||
$response->error('Permission denied for subuser. Missing permission node: ' . $permission->subusers_node_key->name, 403);
|
||||
return $subuser_has_permission;
|
||||
}
|
||||
|
||||
// Fallback to classic user permission check (or if permission is plain string)
|
||||
$user = $auth->get_user();
|
||||
if (!$user) {
|
||||
(new logs_o())->add('global', 'global', 1, 0, 'AUTHENTICATION_FAILED', 'Authentication failed. Invalid, or missing token');
|
||||
$response->error('Authentication failed. Invalid or missing token.', 401);
|
||||
if ($throwOnDeny) {
|
||||
$response->error('Authentication failed. Invalid or missing token.', 401);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
$perm_string = $permission instanceof permission_node ? $permission->permission : $permission;
|
||||
if (!$user->hasPermission($perm_string)) {
|
||||
$userCacheKey = (string)$user->id . ':' . $perm_string;
|
||||
if (!isset(self::$__perm_user_cache[$userCacheKey])) {
|
||||
self::$__perm_user_cache[$userCacheKey] = (bool)$user->hasPermission($perm_string);
|
||||
}
|
||||
$allowed = (bool)self::$__perm_user_cache[$userCacheKey];
|
||||
if (!$allowed && $throwOnDeny) {
|
||||
(new logs_o())->add('global', 'global', 1, $user->id, 'PERMISSION_DENIED', 'Permission denied. Missing permission: ' . $perm_string);
|
||||
$response->error('Permission denied. Missing permission: ' . $perm_string . ' for user: ' . $user->id . ' In group: ' . $user->group_id->value(), 403);
|
||||
}
|
||||
return $allowed;
|
||||
} catch (Exception $e) {
|
||||
$response->error($e->getMessage(), 400);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Require permission
|
||||
* @param string|permission_node $permission
|
||||
* @return bool
|
||||
*/
|
||||
public function requirePermission(string|permission_node $permission): bool
|
||||
{
|
||||
return $this->evaluatePermission($permission, null, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -346,50 +390,9 @@ trait route_t
|
||||
* @param string|permission_node $permission
|
||||
* @return bool
|
||||
*/
|
||||
public function hasPermission(string|permission_node $permission): bool
|
||||
public function hasPermission(string|permission_node $permission, int $customer_number = null): bool
|
||||
{
|
||||
global $response;
|
||||
// Check if the users authorization token has the required permission
|
||||
try {
|
||||
$auth = new authentication();
|
||||
$subuser = $auth->get_subuser();
|
||||
if ($subuser !== false && $permission instanceof permission_node && $permission->subusers_node_key !== null) {
|
||||
$customer_number = null;
|
||||
$user_ctx = $auth->get_user();
|
||||
if ($user_ctx !== false && isset($user_ctx->customer_number)) {
|
||||
$customer_number = (int)$user_ctx->customer_number->value();
|
||||
}
|
||||
if ($customer_number === null) {
|
||||
$headers = getallheaders();
|
||||
if (isset($headers['X-Customer-Number'])) {
|
||||
$customer_number = (int)$headers['X-Customer-Number'];
|
||||
} elseif (isset($_GET['customer_number'])) {
|
||||
$customer_number = (int)$_GET['customer_number'];
|
||||
} elseif (isset($_POST['customer_number'])) {
|
||||
$customer_number = (int)$_POST['customer_number'];
|
||||
}
|
||||
}
|
||||
if ($customer_number !== null) {
|
||||
$grant = new subuser_user_grant((int)$subuser->id, (int)$customer_number);
|
||||
if ($grant->hasNode($permission->subusers_node_key)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// IMPORTANT: When a subuser is authenticated and a subusers node key is defined
|
||||
// the permission must ONLY pass if the explicit subuser node is granted.
|
||||
// Do NOT fall back to classic user permissions in this branch.
|
||||
return false;
|
||||
}
|
||||
$user = $auth->get_user();
|
||||
if (!$user) {
|
||||
(new logs_o())->add('global', 'global', 1, 0, 'AUTHENTICATION_FAILED', 'Authentication failed. Invalid, or missing token');
|
||||
$response->error('Authentication failed. Invalid or missing token.', 401);
|
||||
}
|
||||
$perm_string = $permission instanceof permission_node ? $permission->permission : $permission;
|
||||
return $user->hasPermission($perm_string);
|
||||
} catch (Exception $e) {
|
||||
$response->error($e->getMessage(), 400);
|
||||
}
|
||||
return $this->evaluatePermission($permission, $customer_number, false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user