Integrate subuser permission node system and refactor route-level permissions

- Add `permission_node` DTO to link classic permissions with subuser-specific nodes.
- Extend `authentication` to support subuser resolution via tokens.
- Introduce route traits for permission evaluation with subuser context.
- Update `requirePermission` and `hasPermission` to handle subuser grants dynamically.
- Implement fallback mechanisms for customer number context in subuser permissions.
This commit is contained in:
Jeppe Bundgaard
2026-02-12 13:54:28 +01:00
parent 2159cd293c
commit d26b94de3b
4 changed files with 189 additions and 18 deletions
+38 -6
View File
@@ -7,6 +7,7 @@ use interfaces\authentication_i;
use objects\plate_scanners_o;
use objects\tokens_o;
use objects\users_o;
use objects\subusers_o;
class authentication implements authentication_i
{
@@ -68,13 +69,17 @@ class authentication implements authentication_i
public function validate_token(string $token): bool
{
// Get the token from the database
$token = (new tokens_o())->getToken($token);
// Check if the token exists
if (!$token->id) {
return false;
// First: try validating as a classic user auth token
$dbToken = (new tokens_o())->getToken($token);
if ($dbToken && $dbToken->id) {
return true;
}
return true;
// Fallback: try validating as a subuser session token
$subuser = (new subusers_o())->getSubuserBySessionToken($token);
if ($subuser !== null) {
return true;
}
return false;
}
/**
@@ -129,6 +134,33 @@ class authentication implements authentication_i
return $token;
}
/**
* @throws Exception
*/
public function get_subuser(): subusers_o|false
{
// Try to resolve a subuser from an incoming bearer token or explicit token parameter
$headers = getallheaders();
$tmp = json_decode(file_get_contents('php://input'), true);
if (!is_array($tmp)) {
$tmp = [];
}
if (!isset($headers['Authorization']) && !isset($_GET['token']) && !isset($_POST['token']) && !isset($tmp['token'])) {
return false;
}
$token = $_GET['token'] ?? $headers['Authorization'] ?? $tmp['token'] ?? $_POST['token'];
// Strip the Bearer prefix (If the token is from the headers)
if (isset($headers['Authorization'])) {
$token = str_replace('Bearer ', '', $token);
}
// Resolve subuser session from cache
$subuser = (new subusers_o())->getSubuserBySessionToken($token);
if ($subuser === null) {
return false;
}
return $subuser;
}
public function hash_password($password): string
{
// Hash the password
@@ -0,0 +1,35 @@
<?php
namespace classes;
use modules\subusers\helpers\subusers_permission_node_key;
/**
* Lightweight DTO that links a classic string permission to an optional
* Subusers permission node key. Used by routes to declare permissions
* that can be satisfied by either the main user permission system or
* a subuser's granted permission node, depending on who is authenticated.
*/
class permission_node
{
/**
* The classic user permission string (e.g. "modules_motorapi_lookup")
*/
public string $permission;
/**
* Optional subusers node key that mirrors this permission for subusers
*/
public ?subusers_permission_node_key $subusers_node_key;
public function __construct(string $permission, ?subusers_permission_node_key $subusers_node_key = null)
{
$this->permission = $permission;
$this->subusers_node_key = $subusers_node_key;
}
public static function create(string $permission, ?subusers_permission_node_key $subusers_node_key = null): self
{
return new self($permission, $subusers_node_key);
}
}
@@ -56,4 +56,27 @@ class subuser_user_grant implements subusers_user_grant_i
}
}
}
/**
* Check if a specific permission node is granted for this subuser/customer pair
*/
public function hasNode(\modules\subusers\helpers\subusers_permission_node_key $key): bool
{
$reflection = new \ReflectionClass($this);
$properties = $reflection->getProperties();
foreach ($properties as $property) {
$propertyType = $property->getType();
if ($propertyType && is_a($propertyType->getName(), subusers_permission_nodes::class, true)) {
$permissionNodes = $property->getValue($this);
if (is_array($permissionNodes)) {
foreach ($permissionNodes as $node) {
if ($node->nodeKey->name === $key->name) {
return (bool)($node->value === true);
}
}
}
}
}
return false;
}
}
+93 -12
View File
@@ -3,9 +3,12 @@
namespace traits;
use classes\authentication;
use classes\permission_node;
use classes\recaptcha;
use classes\response;
use Exception;
use modules\subusers\helpers\subusers_permission_node_key;
use modules\subusers\classes\subuser_user_grant;
use objects\logs_o;
trait route_t
@@ -75,6 +78,25 @@ trait route_t
return true;
}
/**
* Define a permission for the route
* @description Used to standardize permission definitions and link them to subusers permission nodes if needed, this is used in the route definitions to define the permissions required for the route, and to link them to the subusers permission nodes for easier management in the subusers module
* @param string $permission The permission to define (Example: 'modules_motorapi_lookup')
* @param subusers_permission_node_key|null $subusers_permission_node_key The subusers permission node key to link the permission to (Example: subusers_permission_node_key::MOTORAPI_LOOKUP)
* @return permission_node
* @throws Exception
*/
public function definePermission(string $permission, subusers_permission_node_key|null $subusers_permission_node_key = null): permission_node
{
if ($permission === '') {
throw new Exception('Permission cannot be empty');
}
if ($subusers_permission_node_key !== null && !in_array($subusers_permission_node_key, subusers_permission_node_key::cases())) {
throw new Exception('Invalid subusers_permission_node_key');
}
return permission_node::create($permission, $subusers_permission_node_key);
}
/**
* Require a value to be in the given array
* @param mixed $value The value to check
@@ -261,23 +283,57 @@ trait route_t
/**
* Require permission
* @param string $permission
* @param string|permission_node $permission
* @return bool
*/
public function requirePermission(string $permission): bool
public function requirePermission(string|permission_node $permission): bool
{
global $response;
// Check if the users authorization token has the required permission
try {
$user = (new authentication())->get_user();
// If there is no user, return an error
// If permission is a node and a subuser is authenticated, evaluate node-based grant first
$auth = new authentication();
$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) {
(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);
}
$grant = new subuser_user_grant((int)$subuser->id, (int)$customer_number);
if ($grant->hasNode($permission->subusers_node_key)) {
return true;
}
(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);
}
// 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 (!$user->hasPermission($permission)) {
(new logs_o())->add('global', 'global', 1, $user->id, 'PERMISSION_DENIED', 'Permission denied. Missing permission: ' . $permission);
$response->error('Permission denied. Missing permission: ' . $permission . ' for user: ' . $user->id . ' In group: ' . $user->group_id->value(), 403);
$perm_string = $permission instanceof permission_node ? $permission->permission : $permission;
if (!$user->hasPermission($perm_string)) {
(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);
}
} catch (Exception $e) {
$response->error($e->getMessage(), 400);
@@ -287,21 +343,46 @@ trait route_t
/**
* Check if the user has a permission
* @param string $permission
* @param string|permission_node $permission
* @return bool
*/
public function hasPermission(string $permission): bool
public function hasPermission(string|permission_node $permission): bool
{
global $response;
// Check if the users authorization token has the required permission
try {
$user = (new authentication())->get_user();
// If there is no user, return an error
$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;
}
}
}
$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);
}
return $user->hasPermission($permission);
$perm_string = $permission instanceof permission_node ? $permission->permission : $permission;
return $user->hasPermission($perm_string);
} catch (Exception $e) {
$response->error($e->getMessage(), 400);
}