Refactor route_t and orderBookingRoute to simplify permission handling

- Add reusable helpers: `isOwnCustomerContext`, `resolveEffectiveCustomerNumber`, and `allowOwnOrDepartmentAccess` in `route_t` for streamlined permission checks.
- Replace inlined permission logic in `orderBookingRoute` with common helpers for creating, viewing, editing, and deleting bookings.
- Localize permission names, descriptions, and labels to Danish across relevant modules.
- Improve error handling and simplify context resolution for subuser and department-level permissions.
This commit is contained in:
Jeppe Bundgaard
2026-02-12 15:43:02 +01:00
parent cd894b4e3b
commit f265a83034
6 changed files with 173 additions and 159 deletions
@@ -11,13 +11,13 @@ class subusers_permission_nodes_bookings extends subusers_permission_nodes
public function __construct()
{
parent::__construct(
'Bookings',
'Bookings management permissions'
'Bookinger',
'Tilladelser til håndtering af bookinger'
);
$this->addNode(subusers_permission_node_key::BOOKINGS_LIST, 'List Bookings', 'Allows the user to view the list of bookings', subusers_permission_type::VIEW, false);
$this->addNode(subusers_permission_node_key::BOOKINGS_EDIT, 'Edit Bookings', 'Allows the user to edit existing bookings', subusers_permission_type::EDIT, false);
$this->addNode(subusers_permission_node_key::BOOKINGS_DELETE, 'Delete Bookings', 'Allows the user to delete existing bookings', subusers_permission_type::DELETE, false);
$this->addNode(subusers_permission_node_key::BOOKINGS_ADD, 'Add Bookings', 'Allows the user to add new bookings', subusers_permission_type::ADD, false);
$this->addNode(subusers_permission_node_key::BOOKINGS_LIST, 'Vis bookinger', 'Giver brugeren mulighed for at se listen over bookinger', subusers_permission_type::VIEW, false);
$this->addNode(subusers_permission_node_key::BOOKINGS_EDIT, 'Rediger bookinger', 'Giver brugeren mulighed for at redigere eksisterende bookinger', subusers_permission_type::EDIT, false);
$this->addNode(subusers_permission_node_key::BOOKINGS_DELETE, 'Slet bookinger', 'Giver brugeren mulighed for at slette eksisterende bookinger', subusers_permission_type::DELETE, false);
$this->addNode(subusers_permission_node_key::BOOKINGS_ADD, 'Tilføj bookinger', 'Giver brugeren mulighed for at tilføje nye bookinger', subusers_permission_type::ADD, false);
}
}
@@ -11,11 +11,11 @@ class subusers_permission_nodes_orders extends subusers_permission_nodes
public function __construct()
{
parent::__construct(
'Orders',
'Orders management permissions'
'Ordrer',
'Tilladelser til håndtering af ordrer'
);
$this->addNode(subusers_permission_node_key::ORDERS_LIST, 'View Orders', 'Allows the user to view the list of orders and order details', subusers_permission_type::VIEW, false);
$this->addNode(subusers_permission_node_key::ORDERS_EDIT, 'Edit Orders', 'Allows the user to edit existing orders', subusers_permission_type::EDIT, false);
$this->addNode(subusers_permission_node_key::ORDERS_LIST, 'Vis ordrer', 'Giver brugeren mulighed for at se listen over ordrer og ordredetaljer', subusers_permission_type::VIEW, false);
$this->addNode(subusers_permission_node_key::ORDERS_EDIT, 'Rediger ordrer', 'Giver brugeren mulighed for at redigere eksisterende ordrer', subusers_permission_type::EDIT, false);
}
}
@@ -11,10 +11,10 @@ class subusers_permission_nodes_selfserve extends subusers_permission_nodes
public function __construct()
{
parent::__construct(
'Selfserve',
'Self Serve Permissions',
'Selvbetjening',
'Tilladelser til selvbetjening',
);
$this->addNode(subusers_permission_node_key::SELFSERVE_ADD, 'Self Serve Add', 'Add self serve', subusers_permission_type::ADD, false);
$this->addNode(subusers_permission_node_key::SELFSERVE_ADD, 'Tilføj selvbetjening', 'Tilføj selvbetjening', subusers_permission_type::ADD, false);
}
}
@@ -11,13 +11,13 @@ class subusers_permission_nodes_subusers extends subusers_permission_nodes
public function __construct()
{
parent::__construct(
'Subusers',
'Set permissions related to subusers, including managing subusers and their permissions'
'Underbrugere',
'Angiv tilladelser relateret til underbrugere, herunder administration af underbrugere og deres tilladelser'
);
$this->addNode(subusers_permission_node_key::SUBUSERS_LIST, 'List Subusers', 'Allows the user to view the list of subusers', subusers_permission_type::VIEW, false);
$this->addNode(subusers_permission_node_key::SUBUSERS_EDIT, 'Edit Subusers', 'Allows the user to edit existing subusers', subusers_permission_type::EDIT, false);
$this->addNode(subusers_permission_node_key::SUBUSERS_DELETE, 'Delete Subusers', 'Allows the user to delete existing subusers', subusers_permission_type::DELETE, false);
$this->addNode(subusers_permission_node_key::SUBUSERS_ADD, 'Add Subusers', 'Allows the user to add new subusers', subusers_permission_type::ADD, false);
$this->addNode(subusers_permission_node_key::SUBUSERS_LIST, 'Vis underbrugere', 'Giver brugeren mulighed for at se listen over underbrugere', subusers_permission_type::VIEW, false);
$this->addNode(subusers_permission_node_key::SUBUSERS_EDIT, 'Rediger underbrugere', 'Giver brugeren mulighed for at redigere eksisterende underbrugere', subusers_permission_type::EDIT, false);
$this->addNode(subusers_permission_node_key::SUBUSERS_DELETE, 'Slet underbrugere', 'Giver brugeren mulighed for at slette eksisterende underbrugere', subusers_permission_type::DELETE, false);
$this->addNode(subusers_permission_node_key::SUBUSERS_ADD, 'Tilføj underbrugere', 'Giver brugeren mulighed for at tilføje nye underbrugere', subusers_permission_type::ADD, false);
}
}
+54 -140
View File
@@ -37,45 +37,18 @@ class orderBookingRoute
$pickup = self::getTargetPickup(); // Bool | Null
$items = self::getTargetItems(); // Array of order_items_o objects
/**
* Authentication & Permissions
* Permissions (clean helper)
*/
$auth = new authentication();
$user = $auth->get_user(); // Classic user (may be false if a subuser token is used)
$permission_own = self::definePermission('add_own_bookings', subusers_permission_node_key::BOOKINGS_ADD); // Subuser-aware: can add for own customer only
$permission_other = self::definePermission('add_bookings'); // Admin/department scope: can add for any customer within permitted departments
$has_permission_own = self::hasPermission($permission_own);
$has_permission_other = self::hasPermission($permission_other);
$has_permission = false; // Updated below
// Own booking creation (either classic user for own customer or subuser with proper node on target customer)
if ($has_permission_own) {
$targetCustomer = (int)$customer_number->customer_number->value();
$subuser = $auth->get_subuser();
if ($subuser !== false) {
// For subusers ensure provided customer_number matches X-Customer-Number target context
$sub_target = $auth->get_subuser_customer_number_target();
if ((int)$sub_target !== $targetCustomer) {
$response->error('You do not have permission to create bookings for this customer.', 403);
}
$has_permission = true; // Node permission already validated via hasPermission above
} else {
// Classic user: ensure creating for own customer only in the "own" permission path
if ($user !== false && (int)$user->customer_number->value() === $targetCustomer) {
$has_permission = true;
}
}
}
// Department/admin booking creation for other customers
if (!$has_permission && $has_permission_other) {
// Must be within permitted department
self::requireDepartmentAccess((int)$department->id);
$has_permission = true;
}
if (!$has_permission) {
$response->error('You do not have permission to create this order booking.', 403);
}
$permission_own = self::definePermission('add_own_bookings', subusers_permission_node_key::BOOKINGS_ADD);
$permission_other = self::definePermission('add_bookings');
self::allowOwnOrDepartmentAccess(
$permission_own,
$permission_other,
(int)$customer_number->customer_number->value(),
(int)$department->id,
null,
'You do not have permission to create this order booking.'
);
/**
* Input data
*/
@@ -112,45 +85,25 @@ class orderBookingRoute
* Parameters
*/
$object = self::getTargetObject();
/**
* Authentication
*/
/** Authentication */
$auth = new authentication();
$user = $auth->get_user();
/**
* Permissions
*/
$permission_own = self::definePermission('list_own_bookings', subusers_permission_node_key::BOOKINGS_LIST); // Only permits access to bookings with the user's customer number
$permission_other = self::definePermission('list_bookings'); // Requires access to the department as an admin to view other users' bookings
$has_permission_own = self::hasPermission($permission_own);
$permission_own = self::definePermission('list_own_bookings', subusers_permission_node_key::BOOKINGS_LIST);
$permission_other = self::definePermission('list_bookings');
$has_permission_other = self::hasPermission($permission_other);
$has_permission = false; // Updated below
// Check if the user has permission to view their own bookings
if ($has_permission_own) {
if (!$object) {
$has_permission = true;
} else {
$subuser = $auth->get_subuser();
if ($subuser !== false) {
$sub_target = $auth->get_subuser_customer_number_target();
if ((int)$object->customer_number->value() === (int)$sub_target) {
$has_permission = true;
}
} elseif ($user !== false && (int)$object->customer_number->value() === (int)$user->customer_number->value()) {
$has_permission = true;
}
}
}
// Check if the user has permission to view other users' bookings
if (!$has_permission && $has_permission_other) {
if (!empty($object)) self::requireDepartmentAccess((int)$object->department->value());
// Check if the user has access to the department
$has_permission = true;
}
// If the user does not have permission, return an error
if (!$has_permission) {
$response->error('You do not have permission to view this order booking.', 403);
}
$targetCustomer = !empty($object) ? (int)$object->customer_number->value() : null;
$deptId = !empty($object) ? (int)$object->department->value() : null;
self::allowOwnOrDepartmentAccess(
$permission_own,
$permission_other,
$targetCustomer,
$deptId,
null,
'You do not have permission to view this order booking.'
);
/**
* Return the object
*/
@@ -159,14 +112,15 @@ class orderBookingRoute
* Return the filtered list of objects
*/
$object = new order_bookings_o(); // New object for listing
$effectiveCustomer = self::resolveEffectiveCustomerNumber();
$response->success($object->listObjectsWithPaginationIfSet(
function ($booking) { return (new order_bookings_o())->select((int)$booking['id'])->asArray(); },
$object->forceRestrictFilters([
...($has_permission_other ? [
...($has_permission_other && $user !== false ? [
'department' => $user->getGroup()->getDepartments()
] : []),
...(!$has_permission_other ? [
'customer_number' => [(int)$user->customer_number->value()]
...(!$has_permission_other && $effectiveCustomer !== null ? [
'customer_number' => [(int)$effectiveCustomer]
] : [])
])
));
@@ -196,9 +150,7 @@ class orderBookingRoute
$pickup = self::getTargetPickup(false); // Bool | Null
$items = self::getTargetItems(false); // Array of order_items_o objects
$order_id = self::getTargetOrderId(false); // Int | Null
/**
* Authentication
*/
/** Authentication */
$auth = new authentication();
$user = $auth->get_user();
/**
@@ -207,40 +159,21 @@ class orderBookingRoute
if (!$object || !$object->exists()) {
$response->error('Order booking does not exist.', 400);
}
$permission_own = self::definePermission('edit_own_bookings', subusers_permission_node_key::BOOKINGS_EDIT); // Only permits editing bookings with the user's customer number (subusers node aware)
$permission_other = self::definePermission('edit_bookings'); // Requires access to the department as an admin to edit other users' bookings
$has_permission_own = self::hasPermission($permission_own);
$permission_own = self::definePermission('edit_own_bookings', subusers_permission_node_key::BOOKINGS_EDIT);
$permission_other = self::definePermission('edit_bookings');
$has_permission_other = self::hasPermission($permission_other);
$has_permission = false; // Updated below
// Check if the user has permission to edit their own bookings
if ($has_permission_own) {
$isOwn = false;
$subuser = $auth->get_subuser();
if ($subuser !== false) {
$sub_target = $auth->get_subuser_customer_number_target();
$isOwn = ((int)$object->customer_number->value() === (int)$sub_target);
} elseif ($user !== false) {
$isOwn = ((int)$object->customer_number->value() === (int)$user->customer_number->value());
}
if ($isOwn) {
// Check if the booking has a transaction associated with it
// If this is the case, prevent user from editing the booking
if (!$has_permission_other && $object->hasTransaction()) {
$response->error('You cannot edit this order booking because it has a transaction associated with it.', 403);
}
$has_permission = true;
}
}
// Check if the user has permission to edit other users' bookings
if (!$has_permission && $has_permission_other) {
self::requireDepartmentAccess((int)$object->department->value());
// Check if the user has access to the department
$has_permission = true;
}
// If the user does not have permission, return an error
if (!$has_permission) {
$response->error('You do not have permission to edit this order booking.', 403);
}
$ownGuard = function () use ($has_permission_other, $object) {
// Block own edits when a transaction exists, unless admin/department permission is present
return $has_permission_other || !$object->hasTransaction();
};
self::allowOwnOrDepartmentAccess(
$permission_own,
$permission_other,
(int)$object->customer_number->value(),
(int)$object->department->value(),
$ownGuard,
'You do not have permission to edit this order booking.'
);
/**
* Update the object
*/
@@ -279,9 +212,7 @@ class orderBookingRoute
* Parameters
*/
$object = self::getTargetObject();
/**
* Authentication
*/
/** Authentication */
$auth = new authentication();
$user = $auth->get_user();
/**
@@ -290,33 +221,16 @@ class orderBookingRoute
if (!$object || !$object->exists()) {
$response->error('Order booking does not exist.', 400);
}
$permission_own = self::definePermission('delete_own_bookings', subusers_permission_node_key::BOOKINGS_DELETE); // Only permits deleting bookings with the user's customer number (subusers node aware)
$permission_other = self::definePermission('delete_bookings'); // Requires access to the department as an admin to delete other users' bookings
$has_permission_own = self::hasPermission($permission_own);
$has_permission_other = self::hasPermission($permission_other);
$has_permission = false; // Updated below
// Check if the user has permission to delete their own bookings
if ($has_permission_own) {
$subuser = $auth->get_subuser();
if ($subuser !== false) {
$sub_target = $auth->get_subuser_customer_number_target();
if ((int)$object->customer_number->value() === (int)$sub_target) {
$has_permission = true;
}
} elseif ($user !== false && (int)$object->customer_number->value() === (int)$user->customer_number->value()) {
$has_permission = true;
}
}
// Check if the user has permission to delete other users' bookings
if (!$has_permission && $has_permission_other) {
self::requireDepartmentAccess((int)$object->department->value());
// Check if the user has access to the department
$has_permission = true;
}
// If the user does not have permission, return an error
if (!$has_permission) {
$response->error('You do not have permission to delete this order booking.', 403);
}
$permission_own = self::definePermission('delete_own_bookings', subusers_permission_node_key::BOOKINGS_DELETE);
$permission_other = self::definePermission('delete_bookings');
self::allowOwnOrDepartmentAccess(
$permission_own,
$permission_other,
(int)$object->customer_number->value(),
(int)$object->department->value(),
null,
'You do not have permission to delete this order booking.'
);
/**
* Delete the object
*/
+100
View File
@@ -395,6 +395,106 @@ trait route_t
return $this->evaluatePermission($permission, $customer_number, false);
}
/**
* Determine if the current principal (classic user or subuser) acts on their own customer context.
* - For classic users: compares against the authenticated user's customer_number
* - For subusers: compares against X-Customer-Number target header
*/
public function isOwnCustomerContext(int $targetCustomerNumber): bool
{
try {
$auth = new authentication();
$sub = $auth->get_subuser();
if ($sub !== false) {
$tgt = $auth->get_subuser_customer_number_target();
return ((int)$tgt === (int)$targetCustomerNumber);
}
$user = $auth->get_user();
if ($user !== false) {
return ((int)$user->customer_number->value() === (int)$targetCustomerNumber);
}
} catch (Exception) {
// fall through
}
return false;
}
/**
* Resolve the most relevant customer number for the current request context.
* Priority: classic user -> X-Customer-Number header -> query/body customer_number
*/
public function resolveEffectiveCustomerNumber(): ?int
{
try {
$auth = new authentication();
$user = $auth->get_user();
if ($user !== false && isset($user->customer_number)) {
return (int)$user->customer_number->value();
}
$sub = $auth->get_subuser();
if ($sub !== false) {
$tgt = $auth->get_subuser_customer_number_target();
if ($tgt !== false && $tgt !== null) return (int)$tgt;
}
// Fallbacks
if (isset($_GET['customer_number'])) return (int)$_GET['customer_number'];
if (isset($_POST['customer_number'])) return (int)$_POST['customer_number'];
} catch (Exception) {
// ignore
}
return null;
}
/**
* Common pattern helper: allow via own-permission (with optional guard) or via department/admin permission.
* If neither path allows, respond with 403 and provided message.
*
* @param string|permission_node $permissionOwn Own-scope permission (often linked to a subuser node)
* @param string|permission_node $permissionOther Department/admin permission
* @param int|null $targetCustomerNumber Customer number to validate "own" scope against (null means list context)
* @param int|null $departmentId Department id for admin path (will be validated when provided)
* @param callable|null $ownGuard Optional additional guard for own path. Return true to allow, false to deny own-path.
* @param string|null $denyMessage Message to return on deny (defaults to generic)
* @return bool True if access is allowed (also throws on deny)
*/
public function allowOwnOrDepartmentAccess(
string|permission_node $permissionOwn,
string|permission_node $permissionOther,
?int $targetCustomerNumber,
?int $departmentId,
?callable $ownGuard = null,
?string $denyMessage = null
): bool {
global $response;
$allowed = false;
$hasOwn = $this->hasPermission($permissionOwn);
$hasOther = $this->hasPermission($permissionOther);
// Try own path first
if ($hasOwn) {
$isOwnContext = ($targetCustomerNumber === null) ? true : $this->isOwnCustomerContext((int)$targetCustomerNumber);
if ($isOwnContext) {
$guardOk = $ownGuard ? (bool)call_user_func($ownGuard) : true;
if ($guardOk) {
$allowed = true;
}
}
}
// Fallback to department/admin path
if (!$allowed && $hasOther) {
if ($departmentId !== null) {
$this->requireDepartmentAccess((int)$departmentId);
}
$allowed = true;
}
if (!$allowed) {
$response->error($denyMessage ?? 'Permission denied.', 403);
}
return $allowed;
}
/**
* Require parameter to be a positive integer
* @param int $value The value to check