Refactor permission handling to leverage standardized "forbidden" responses and enhance unit test coverage.

This commit is contained in:
Jeppe Bundgaard
2026-03-19 15:59:25 +01:00
parent 3752fdec4c
commit b547a8b029
17 changed files with 321 additions and 89 deletions
+3 -3
View File
@@ -49,7 +49,7 @@ class bookingsRoute
}
// Check if the user has access to the booking
if (!$user->hasAccessToBooking((int)self::getParameter('id'))) {
$response->error('You are not allowed to access this booking', 403);
$response->forbidden(['list_bookings']);
}
// Return the booking
$response->success(
@@ -158,7 +158,7 @@ class bookingsRoute
}
// Check if the user has access to the booking
if (!$user->hasAccessToBooking((int)$this->getParameter('id'))) {
$response->error('You are not allowed to update this booking', 403);
$response->forbidden(['list_bookings']);
}
// Check if the optional parameters are set
if (self::isParametersSet(['reference_number'])) {
@@ -530,4 +530,4 @@ class bookingsRoute
]
);
}
}
}
+2 -4
View File
@@ -16,9 +16,7 @@ class cronRoute
// Get the post data
global $response;
// Make sure the user has the SUPERUSER_RUN_CRON permission
if (!$this->requirePermission('SUPERUSER_RUN_CRON')) {
$response->error('Permission denied', 403);
}
$this->requirePermission('SUPERUSER_RUN_CRON');
// Get the user object
$user = (new authentication())->get_user();
// Get the post data
@@ -49,4 +47,4 @@ class cronRoute
]
);
}
}
}
@@ -54,7 +54,11 @@ class departmentGoalsRoute
}
// Access control: must have all departments unless superuser
if (!$isSuperuser && !$hasAllDepartments((array)$goal->departments->value())) {
$response->error('You are not allowed to access this goal', 403);
$missingDepartments = array_values(array_map('intval', array_diff(array_map('intval', (array)$goal->departments->value()), array_map('intval', $userDepartments))));
$response->forbidden([
...array_map(static fn($departmentId) => 'department_access_' . $departmentId, $missingDepartments),
'superuser'
]);
}
$response->success($goal->asArray());
}
@@ -115,7 +119,10 @@ class departmentGoalsRoute
$goalDepartments = array_map(fn($d) => (int)$d->id, $departments);
$missing = array_diff($goalDepartments, array_map('intval', $userDepartments));
if (count($missing) > 0) {
$response->error('You are not allowed to create goals for one or more selected departments', 403);
$response->forbidden([
...array_map(static fn($departmentId) => 'department_access_' . (int)$departmentId, $missing),
'superuser'
]);
}
}
@@ -159,7 +166,11 @@ class departmentGoalsRoute
// If not superuser or creator, require access to existing goal departments
if (!$isSuperuser && !$isCreator && !$hasAllDepartments((array)$goal->departments->value(), $userDepartments)) {
$response->error('You are not allowed to update this goal', 403);
$missingDepartments = array_values(array_map('intval', array_diff(array_map('intval', (array)$goal->departments->value()), array_map('intval', $userDepartments))));
$response->forbidden([
...array_map(static fn($departmentId) => 'department_access_' . $departmentId, $missingDepartments),
'superuser'
]);
}
$dataToUpdate = [];
@@ -177,7 +188,10 @@ class departmentGoalsRoute
if (!$isSuperuser && !$isCreator) {
$missing = array_diff(array_map('intval', $departmentsInput), array_map('intval', $userDepartments));
if (count($missing) > 0) {
$response->error('You are not allowed to assign one or more selected departments to this goal', 403);
$response->forbidden([
...array_map(static fn($departmentId) => 'department_access_' . (int)$departmentId, $missing),
'superuser'
]);
}
}
// store as ids array in column
@@ -247,7 +261,11 @@ class departmentGoalsRoute
$hasAll = count(array_diff(array_map('intval', $goalDepartments), array_map('intval', $userDepartments))) === 0;
if (!$isSuperuser && !$isCreator && !$hasAll) {
$response->error('You are not allowed to delete this goal', 403);
$missingDepartments = array_values(array_map('intval', array_diff(array_map('intval', $goalDepartments), array_map('intval', $userDepartments))));
$response->forbidden([
...array_map(static fn($departmentId) => 'department_access_' . $departmentId, $missingDepartments),
'superuser'
]);
}
$goal->delete();
$response->success(['message' => 'Deleted']);
@@ -282,7 +300,11 @@ class departmentGoalsRoute
$goalDepartments = (array)$goal->departments->value();
$hasAll = count(array_diff(array_map('intval', $goalDepartments), array_map('intval', $userDepartments))) === 0;
if (!$isSuperuser && !$hasAll) {
$response->error('You are not allowed to send progress alerts for this goal', 403);
$missingDepartments = array_values(array_map('intval', array_diff(array_map('intval', $goalDepartments), array_map('intval', $userDepartments))));
$response->forbidden([
...array_map(static fn($departmentId) => 'department_access_' . $departmentId, $missingDepartments),
'superuser'
]);
}
// Rebuild criteria from stored array
@@ -40,7 +40,7 @@ class departmentSelfserveConditionRulesRoute
$authorized_department_ids = $user->getGroup()->getDepartments();
if ($condition_o->exists()) {
if (!$this->canAccessDepartment($authorized_department_ids, (int)$condition_o->department->value()) && !$this->hasPermission('view_all_department_selfserve_condition_rules')) {
$response->error('You do not have access to this department', 403);
$this->forbidDepartmentAccess((int)$condition_o->department->value(), ['view_all_department_selfserve_condition_rules']);
}
}
$response->success($rules_o->asArray());
@@ -59,7 +59,7 @@ class departmentSelfserveConditionRulesRoute
if ($condition_o->exists()) {
$authorized_department_ids = $user->getGroup()->getDepartments();
if (!$this->canAccessDepartment($authorized_department_ids, (int)$condition_o->department->value()) && !$this->hasPermission('view_all_department_selfserve_condition_rules')) {
$response->error('You do not have access to this department', 403);
$this->forbidDepartmentAccess((int)$condition_o->department->value(), ['view_all_department_selfserve_condition_rules']);
}
}
}
@@ -119,7 +119,7 @@ class departmentSelfserveConditionRulesRoute
}
$authorized_department_ids = $user->getGroup()->getDepartments();
if (!$this->canAccessDepartment($authorized_department_ids, (int)$condition_o->department->value())) {
$response->error('You do not have access to this department', 403);
$this->forbidDepartmentAccess((int)$condition_o->department->value());
}
try {
@@ -167,7 +167,7 @@ class departmentSelfserveConditionRulesRoute
$condition_o->select((int)$rule_o->condition_id->value());
$authorized_department_ids = $user->getGroup()->getDepartments();
if ($condition_o->exists() && !$this->canAccessDepartment($authorized_department_ids, (int)$condition_o->department->value())) {
$response->error('You do not have access to this department', 403);
$this->forbidDepartmentAccess((int)$condition_o->department->value());
}
if ($response->isRequestParameterSet('condition_id')) {
@@ -178,7 +178,7 @@ class departmentSelfserveConditionRulesRoute
$response->error('Target condition not found', 404);
}
if (!$this->canAccessDepartment($authorized_department_ids, (int)$new_condition_o->department->value())) {
$response->error('You do not have access to the target department', 403);
$this->forbidDepartmentAccess((int)$new_condition_o->department->value());
}
$rule_o->condition_id->update($new_condition_id);
}
@@ -231,7 +231,7 @@ class departmentSelfserveConditionRulesRoute
$condition_o->select((int)$rule_o->condition_id->value());
$authorized_department_ids = $user->getGroup()->getDepartments();
if ($condition_o->exists() && !$this->canAccessDepartment($authorized_department_ids, (int)$condition_o->department->value())) {
$response->error('You do not have access to this department', 403);
$this->forbidDepartmentAccess((int)$condition_o->department->value());
}
$rule_o->delete();
@@ -37,7 +37,7 @@ class departmentSelfserveConditionsRoute
if ($conditions_o->exists()) {
if (!$this->canAccessDepartment($authorized_department_ids, (int)$conditions_o->department->value())) {
if (!$has_view_all_permission) {
$response->error('You do not have access to this department', 403);
$this->forbidDepartmentAccess((int)$conditions_o->department->value(), ['view_all_department_selfserve_conditions']);
}
}
$response->success($conditions_o->asArray());
@@ -50,7 +50,7 @@ class departmentSelfserveConditionsRoute
if (self::isParametersSet(['department'])) {
$requested_department = (int)self::getParameter('department');
if (!$this->canAccessDepartment($authorized_department_ids, $requested_department) && !$has_view_all_permission) {
$response->error('You do not have access to this department', 403);
$this->forbidDepartmentAccess($requested_department, ['view_all_department_selfserve_conditions']);
}
$filters['department'] = $requested_department;
} else {
@@ -125,7 +125,7 @@ class departmentSelfserveConditionsRoute
$authorized_department_ids = $user->getGroup()->getDepartments();
if (!$this->canAccessDepartment($authorized_department_ids, $department)) {
$response->error('You do not have access to this department', 403);
$this->forbidDepartmentAccess($department);
}
try {
@@ -171,13 +171,13 @@ class departmentSelfserveConditionsRoute
$authorized_department_ids = $user->getGroup()->getDepartments();
if (!$this->canAccessDepartment($authorized_department_ids, (int)$condition_o->department->value())) {
$response->error('You do not have access to this department', 403);
$this->forbidDepartmentAccess((int)$condition_o->department->value());
}
if ($response->isRequestParameterSet('department')) {
$new_department = (int)$response->getRequestParameter('department');
if (!$this->canAccessDepartment($authorized_department_ids, $new_department)) {
$response->error('You do not have access to the target department', 403);
$this->forbidDepartmentAccess($new_department);
}
$condition_o->department->update($new_department);
}
@@ -232,7 +232,7 @@ class departmentSelfserveConditionsRoute
$authorized_department_ids = $user->getGroup()->getDepartments();
if (!$this->canAccessDepartment($authorized_department_ids, (int)$condition_o->department->value())) {
$response->error('You do not have access to this department', 403);
$this->forbidDepartmentAccess((int)$condition_o->department->value());
}
$condition_o->delete();
@@ -37,7 +37,7 @@ class departmentSelfserveQuestionsRoute
if ($questions_o->exists()) {
if (!$this->canAccessDepartment($authorized_department_ids, (int)$questions_o->department->value())) {
if (!$has_view_all_permission) {
$response->error('You do not have access to this department', 403);
$this->forbidDepartmentAccess((int)$questions_o->department->value(), ['view_all_department_selfserve_questions']);
}
}
$response->success($questions_o->asArray());
@@ -50,7 +50,7 @@ class departmentSelfserveQuestionsRoute
if (self::isParametersSet(['department'])) {
$requested_department = (int)self::getParameter('department');
if (!$this->canAccessDepartment($authorized_department_ids, $requested_department) && !$has_view_all_permission) {
$response->error('You do not have access to this department', 403);
$this->forbidDepartmentAccess($requested_department, ['view_all_department_selfserve_questions']);
}
$filters['department'] = $requested_department;
} else {
@@ -116,7 +116,7 @@ class departmentSelfserveQuestionsRoute
$authorized_department_ids = $user->getGroup()->getDepartments();
if (!$this->canAccessDepartment($authorized_department_ids, $department)) {
$response->error('You do not have access to this department', 403);
$this->forbidDepartmentAccess($department);
}
try {
@@ -160,13 +160,13 @@ class departmentSelfserveQuestionsRoute
$authorized_department_ids = $user->getGroup()->getDepartments();
if (!$this->canAccessDepartment($authorized_department_ids, (int)$question_o->department->value())) {
$response->error('You do not have access to this department', 403);
$this->forbidDepartmentAccess((int)$question_o->department->value());
}
if (self::isParametersSet(['department'])) {
$new_department = (int)self::getParameter('department');
if (!$this->canAccessDepartment($authorized_department_ids, $new_department)) {
$response->error('You do not have access to the new department', 403);
$this->forbidDepartmentAccess($new_department);
}
$question_o->department->set($new_department);
}
@@ -218,7 +218,7 @@ class departmentSelfserveQuestionsRoute
$authorized_department_ids = $user->getGroup()->getDepartments();
if (!$this->canAccessDepartment($authorized_department_ids, (int)$question_o->department->value())) {
$response->error('You do not have access to this department', 403);
$this->forbidDepartmentAccess((int)$question_o->department->value());
}
$question_o->delete();
@@ -41,7 +41,7 @@ class departmentSelfserveTasksRoute
if ($tasks_o->exists()) {
if (!$this->canAccessDepartment($authorized_department_ids, (int)$tasks_o->department->value())) {
if (!$has_view_all_permission) {
$response->error('You do not have access to this department', 403);
$this->forbidDepartmentAccess((int)$tasks_o->department->value(), ['view_all_department_selfserve_tasks']);
}
}
$response->success($tasks_o->asArray());
@@ -54,7 +54,7 @@ class departmentSelfserveTasksRoute
if (self::isParametersSet(['department'])) {
$requested_department = (int)self::getParameter('department');
if (!$this->canAccessDepartment($authorized_department_ids, $requested_department) && !$has_view_all_permission) {
$response->error('You do not have access to this department', 403);
$this->forbidDepartmentAccess($requested_department, ['view_all_department_selfserve_tasks']);
}
$filters['department'] = $requested_department;
} else {
@@ -195,7 +195,7 @@ class departmentSelfserveTasksRoute
$authorized_department_ids = $user->getGroup()->getDepartments();
if (!$this->canAccessDepartment($authorized_department_ids, $department)) {
$response->error('You do not have access to this department', 403);
$this->forbidDepartmentAccess($department);
}
try {
@@ -243,13 +243,13 @@ class departmentSelfserveTasksRoute
$authorized_department_ids = $user->getGroup()->getDepartments();
if (!$this->canAccessDepartment($authorized_department_ids, (int)$task_o->department->value())) {
$response->error('You do not have access to this department', 403);
$this->forbidDepartmentAccess((int)$task_o->department->value());
}
if (self::isParametersSet(['department'])) {
$new_department = (int)self::getParameter('department');
if (!$this->canAccessDepartment($authorized_department_ids, $new_department)) {
$response->error('You do not have access to the new department', 403);
$this->forbidDepartmentAccess($new_department);
}
$task_o->department->set($new_department);
}
@@ -372,7 +372,7 @@ class departmentSelfserveTasksRoute
$authorized_department_ids = $user->getGroup()->getDepartments();
if (!$this->canAccessDepartment($authorized_department_ids, (int)$task_o->department->value())) {
$response->error('You do not have access to this department', 403);
$this->forbidDepartmentAccess((int)$task_o->department->value());
}
$task_o->delete();
@@ -408,7 +408,7 @@ class departmentSelfserveTasksRoute
$has_view_all_permission = $this->hasPermission('view_all_department_selfserve_tasks');
if (!$this->canAccessDepartment($authorized_department_ids, (int)$task_o->department->value())) {
if (!$has_view_all_permission) {
$response->error('You do not have access to this department', 403);
$this->forbidDepartmentAccess((int)$task_o->department->value(), ['view_all_department_selfserve_tasks']);
}
}
@@ -454,7 +454,7 @@ class departmentSelfserveTasksRoute
$has_view_all_permission = $this->hasPermission('view_all_department_selfserve_tasks');
if (!$this->canAccessDepartment($authorized_department_ids, (int)$task_o->department->value())) {
if (!$has_view_all_permission) {
$response->error('You do not have access to this department', 403);
$this->forbidDepartmentAccess((int)$task_o->department->value(), ['view_all_department_selfserve_tasks']);
}
}
@@ -491,7 +491,7 @@ class departmentSelfserveTasksRoute
$authorized_department_ids = $user->getGroup()->getDepartments();
if (!$this->canAccessDepartment($authorized_department_ids, (int)$task_o->department->value())) {
$response->error('You do not have access to this department', 403);
$this->forbidDepartmentAccess((int)$task_o->department->value());
}
$attachment_store = new attachment_store();
@@ -536,7 +536,7 @@ class departmentSelfserveTasksRoute
$authorized_department_ids = $user->getGroup()->getDepartments();
if (!$this->canAccessDepartment($authorized_department_ids, (int)$task_o->department->value())) {
$response->error('You do not have access to this department', 403);
$this->forbidDepartmentAccess((int)$task_o->department->value());
}
$task_o->removeAttachment($attachment_id);
@@ -35,7 +35,7 @@ class departmentSelfserveVehicleConditionsRoute
$has_own = $user->hasPermission('list_own_department_selfserve_vehicle_conditions');
if (!$has_global && !$has_own) {
$response->error('Permission denied', 403);
$response->forbidden(['list_department_selfserve_vehicle_conditions', 'list_own_department_selfserve_vehicle_conditions']);
}
(new logs_o())->add('department_selfserve_vehicle_conditions', 'global', 1, $user->id, 'LIST_VEHICLE_CONDITIONS', 'User listed department self-serve vehicle conditions');
@@ -49,11 +49,11 @@ class departmentSelfserveVehicleConditionsRoute
if ($has_global) {
$authorized_department_ids = $user->getGroup()->getDepartments();
if (!in_array((int)$conditions_o->department->value(), $authorized_department_ids, true)) {
$response->error('You do not have access to this department', 403);
$this->forbidDepartmentAccess((int)$conditions_o->department->value());
}
} else {
if ((int)$conditions_o->customer_id->value() !== $customer_number) {
$response->error('You do not have access to this condition', 403);
$response->forbidden(['list_department_selfserve_vehicle_conditions']);
}
}
$response->success($conditions_o->asArray());
@@ -68,7 +68,7 @@ class departmentSelfserveVehicleConditionsRoute
if (self::isParametersSet(['department'])) {
$requested_department = (int)self::getParameter('department');
if (!in_array($requested_department, $authorized_department_ids, true)) {
$response->error('You do not have access to this department', 403);
$this->forbidDepartmentAccess($requested_department);
}
$filters['department'] = $requested_department;
} else {
@@ -123,7 +123,7 @@ class departmentSelfserveVehicleConditionsRoute
$has_global = $user->hasPermission('list_department_selfserve_vehicle_conditions');
$has_own = $user->hasPermission('list_own_department_selfserve_vehicle_conditions');
if (!$has_global && !$has_own) {
$response->error('Permission denied', 403);
$response->forbidden(['list_department_selfserve_vehicle_conditions', 'list_own_department_selfserve_vehicle_conditions']);
}
self::requireParameters(['lane_id', 'reg']);
@@ -133,7 +133,7 @@ class departmentSelfserveVehicleConditionsRoute
$lane = $this->assertLaneAccess($user, $lane_id, $has_global);
$customer_number = null;
if (!$has_global && $has_own) {
$vehicle = $this->assertOwnVehicle($user, $reg);
$vehicle = $this->assertOwnVehicle($user, $reg, 'list_department_selfserve_vehicle_conditions');
$customer_number = (int)$vehicle->customer_id->value();
}
@@ -157,14 +157,14 @@ class departmentSelfserveVehicleConditionsRoute
$has_global = $user->hasPermission('list_department_selfserve_vehicle_conditions');
$has_own = $user->hasPermission('list_own_department_selfserve_vehicle_conditions');
if (!$has_global && !$has_own) {
$response->error('Permission denied', 403);
$response->forbidden(['list_department_selfserve_vehicle_conditions', 'list_own_department_selfserve_vehicle_conditions']);
}
$flow = $this->getWashFlow();
try {
if (self::isParametersSet(['session_id'])) {
$summary = $flow->getSessionSummary((int)self::getParameter('session_id'));
$this->assertSummaryAccess($user, $summary, $has_global);
$this->assertSummaryAccess($user, $summary, $has_global, 'list_department_selfserve_vehicle_conditions');
$response->success($summary);
}
@@ -174,11 +174,11 @@ class departmentSelfserveVehicleConditionsRoute
$this->assertLaneAccess($user, $lane_id, $has_global);
if (!$has_global && $has_own) {
$this->assertOwnVehicle($user, $reg);
$this->assertOwnVehicle($user, $reg, 'list_department_selfserve_vehicle_conditions');
}
$summary = $flow->getLatestSessionSummary($lane_id, $reg);
$this->assertSummaryAccess($user, $summary, $has_global);
$this->assertSummaryAccess($user, $summary, $has_global, 'list_department_selfserve_vehicle_conditions');
$response->success($summary);
} catch (\RuntimeException $e) {
$response->error($e->getMessage(), 404);
@@ -202,7 +202,7 @@ class departmentSelfserveVehicleConditionsRoute
$has_own = $user->hasPermission('add_own_department_selfserve_vehicle_conditions');
if (!$has_global && !$has_own) {
$response->error('Permission denied', 403);
$response->forbidden(['add_department_selfserve_vehicle_conditions', 'add_own_department_selfserve_vehicle_conditions']);
}
$department = (int)$response->getRequestParameter('department');
@@ -219,11 +219,11 @@ class departmentSelfserveVehicleConditionsRoute
$customer_id = $response->isRequestParameterSet('customer_id') ? (int)$response->getRequestParameter('customer_id') : null;
$authorized_department_ids = $user->getGroup()->getDepartments();
if (!in_array($department, $authorized_department_ids, true)) {
$response->error('You do not have access to this department', 403);
$this->forbidDepartmentAccess($department);
}
} else {
$customer_id = (int)$user->customer_number->value();
$vehicle_o = $this->assertOwnVehicle($user, $reg);
$vehicle_o = $this->assertOwnVehicle($user, $reg, 'add_department_selfserve_vehicle_conditions');
$customer_id = (int)$vehicle_o->customer_id->value();
}
@@ -258,7 +258,7 @@ class departmentSelfserveVehicleConditionsRoute
$has_own = $user->hasPermission('update_own_department_selfserve_vehicle_conditions');
if (!$has_global && !$has_own) {
$response->error('Permission denied', 403);
$response->forbidden(['update_department_selfserve_vehicle_conditions', 'update_own_department_selfserve_vehicle_conditions']);
}
$id = (int)$response->getRequestParameter('id');
@@ -277,11 +277,11 @@ class departmentSelfserveVehicleConditionsRoute
if ($has_global) {
$authorized_department_ids = $user->getGroup()->getDepartments();
if (!in_array((int)$condition_o->department->value(), $authorized_department_ids, true)) {
$response->error('You do not have access to this department', 403);
$this->forbidDepartmentAccess((int)$condition_o->department->value());
}
} else {
if ((int)$condition_o->customer_id->value() !== $customer_number) {
$response->error('You do not have access to this condition', 403);
$response->forbidden(['update_department_selfserve_vehicle_conditions']);
}
}
@@ -290,7 +290,7 @@ class departmentSelfserveVehicleConditionsRoute
if ($has_global) {
$authorized_department_ids = $user->getGroup()->getDepartments();
if (!in_array($new_department, $authorized_department_ids, true)) {
$response->error('You do not have access to the target department', 403);
$this->forbidDepartmentAccess($new_department);
}
}
$condition_o->department->update($new_department);
@@ -301,9 +301,9 @@ class departmentSelfserveVehicleConditionsRoute
if ($response->isRequestParameterSet('reg')) {
$new_reg = selfserve::standardize_registration((string)$response->getRequestParameter('reg'));
if (!$has_global && $has_own) {
$vehicle_o = $this->assertOwnVehicle($user, $new_reg);
$vehicle_o = $this->assertOwnVehicle($user, $new_reg, 'update_department_selfserve_vehicle_conditions');
if ((int)$vehicle_o->customer_id->value() !== $customer_number) {
$response->error('You do not own this vehicle', 403);
$response->forbidden(['update_department_selfserve_vehicle_conditions']);
}
}
$condition_o->reg->update($new_reg);
@@ -317,7 +317,7 @@ class departmentSelfserveVehicleConditionsRoute
if ($response->isRequestParameterSet('customer_id')) {
$new_customer_id = (int)$response->getRequestParameter('customer_id');
if (!$has_global && $has_own && $new_customer_id !== $customer_number) {
$response->error('You cannot change the customer ID to another customer', 403);
$response->forbidden(['update_department_selfserve_vehicle_conditions']);
}
$condition_o->customer_id->update($new_customer_id);
}
@@ -355,7 +355,7 @@ class departmentSelfserveVehicleConditionsRoute
$has_own = $user->hasPermission('delete_own_department_selfserve_vehicle_conditions');
if (!$has_global && !$has_own) {
$response->error('Permission denied', 403);
$response->forbidden(['delete_department_selfserve_vehicle_conditions', 'delete_own_department_selfserve_vehicle_conditions']);
}
$id = (int)$response->getRequestParameter('id');
@@ -372,11 +372,11 @@ class departmentSelfserveVehicleConditionsRoute
if ($has_global) {
$authorized_department_ids = $user->getGroup()->getDepartments();
if (!in_array((int)$condition_o->department->value(), $authorized_department_ids, true)) {
$response->error('You do not have access to this department', 403);
$this->forbidDepartmentAccess((int)$condition_o->department->value());
}
} else {
if ((int)$condition_o->customer_id->value() !== (int)$user->customer_number->value()) {
$response->error('You do not have access to this condition', 403);
$response->forbidden(['delete_department_selfserve_vehicle_conditions']);
}
}
@@ -420,26 +420,26 @@ class departmentSelfserveVehicleConditionsRoute
if ($hasGlobalPermission) {
$authorized_department_ids = $user->getGroup()->getDepartments();
if (!in_array((int)$lane->department->value(), $authorized_department_ids, true)) {
$response->error('You do not have access to this department', 403);
$this->forbidDepartmentAccess((int)$lane->department->value());
}
}
return $lane;
}
private function assertOwnVehicle(object $user, string $reg): customer_vehicles_o
private function assertOwnVehicle(object $user, string $reg, ?string $elevatedPermission = null): customer_vehicles_o
{
global $response;
$vehicle_o = (new customer_vehicles_o())->selectByPlate($reg);
if (!$vehicle_o->exists() || (int)$vehicle_o->customer_id->value() !== (int)$user->customer_number->value()) {
$response->error('You do not own this vehicle', 403);
$response->forbidden([$elevatedPermission ?? 'list_department_selfserve_vehicle_conditions']);
}
return $vehicle_o;
}
private function assertSummaryAccess(object $user, array $summary, bool $hasGlobalPermission): void
private function assertSummaryAccess(object $user, array $summary, bool $hasGlobalPermission, string $elevatedPermission): void
{
global $response;
@@ -447,7 +447,7 @@ class departmentSelfserveVehicleConditionsRoute
$authorized_department_ids = $user->getGroup()->getDepartments();
$lane_department = (int)($summary['lane']['department'] ?? 0);
if (!in_array($lane_department, $authorized_department_ids, true)) {
$response->error('You do not have access to this department', 403);
$this->forbidDepartmentAccess($lane_department);
}
return;
}
@@ -463,6 +463,6 @@ class departmentSelfserveVehicleConditionsRoute
return;
}
$response->error('You do not have access to this wash summary', 403);
$response->forbidden([$elevatedPermission]);
}
}
@@ -17,9 +17,7 @@ class intimidateRoute
// Get the post data
global $response;
// Make sure the user has the SUPERUSER_INTIMIDATE permission
if (!$this->requirePermission('SUPERUSER_INTIMIDATE')) {
$response->error('Permission denied', 403);
}
$this->requirePermission('SUPERUSER_INTIMIDATE');
// Get the user object
$user = (new authentication())->get_user();
// Get the post data
@@ -42,4 +40,4 @@ class intimidateRoute
]
);
}
}
}
@@ -100,7 +100,7 @@ class orderItemsRoute
$hasPermission = $this->hasPermission('list_own_order_items');
$hasAttribute = $user->showPricesOnBookingPage(); // Check if the user has the attribute to show prices on the booking page
if (!$hasPermission && !$hasAttribute) {
$response->error('You do not have permission to list order items, neither your own nor all orders', 403);
$response->forbidden(['list_own_order_items', 'list_order_items']);
}
} else {
$this->requirePermission('list_order_items');
@@ -223,4 +223,4 @@ class orderItemsRoute
]
);
}
}
}
+5 -5
View File
@@ -306,7 +306,7 @@ class ordersRoute
if (!$has_permission_other) {
$effectiveCustomer = self::resolveEffectiveCustomerNumber();
if ($effectiveCustomer === null || (int)$order->customer_id->value() !== (int)$effectiveCustomer) {
$response->error('You do not have permission to download this order attachment', 403);
$response->forbidden([$permission_other->permission]);
}
}
// Get the attachment
@@ -368,7 +368,7 @@ class ordersRoute
self::requirePermission($permission_own);
$effectiveCustomer = self::resolveEffectiveCustomerNumber();
if ($effectiveCustomer === null || (int)$order->customer_id->value() !== (int)$effectiveCustomer) {
$response->error('You do not have permission to view attachments for this order.', 403);
$response->forbidden([$permission_other->permission]);
}
}
// Get the attachments
@@ -882,12 +882,12 @@ class ordersRoute
if ($subuser_own_path) {
$effectiveCustomer = self::resolveEffectiveCustomerNumber();
if ($effectiveCustomer === null || (int)$order->customer_id->value() !== (int)$effectiveCustomer) {
$response->error('You do not have permission to edit this order', 403);
$response->forbidden([$permission_other->permission]);
}
} else {
// Classic own path — compare against authenticated user customer number
if ($order->customer_id->value() !== $user->customer_number->value()) {
$response->error('You do not have permission to edit this order', 403);
$response->forbidden([$permission_other->permission]);
}
// Ensure classic own-path requires user permission
$this->requirePermission('user');
@@ -1038,4 +1038,4 @@ class ordersRoute
// Optional fields are not checked here, as they are optional and can be empty
return $data;
}
}
}
+2 -2
View File
@@ -79,7 +79,7 @@ class subusersRoute
// If not admin/department permission, force restrict to effective customer
if (!$has_permission_other) {
if ($effectiveCustomer === null) {
$response->error('Missing customer context. Subusers must provide X-Customer-Number header.', 403);
$response->forbidden([$permission_other->permission]);
}
$filters['billing_customer_number'] = (int)$effectiveCustomer;
$hasFilter = true; // ensure we don't fail below
@@ -673,4 +673,4 @@ class subusersRoute
$response->success(['cvr' => $cvr, 'customer_number' => $results[0]->customerNumber]);
});
}
}
}
@@ -92,7 +92,7 @@ class systemSearchRoute
[$allowedTypes, $ownOnlyTypes] = $this->resolveAllowedTypes();
if (empty($allowedTypes)) {
$response->error('Permission denied. No searchable entity types available for this user.', 403);
$response->forbidden($this->searchAccessPermissionCandidates());
}
$permissionsCatalogAll = $this->flattenPermissionCatalog((array)$router->getPermissions());
@@ -578,4 +578,27 @@ class systemSearchRoute
{
return array_keys($this->entityPermissionMap());
}
/**
* Return all permission keys that can unlock at least one searchable entity type.
*
* @return array<int, string>
*/
private function searchAccessPermissionCandidates(): array
{
$permissions = [];
foreach ($this->entityPermissionMap() as $permissionSets) {
foreach ((array)($permissionSets['all'] ?? []) as $permission) {
if (is_string($permission) && $permission !== '') {
$permissions[] = $permission;
}
}
foreach ((array)($permissionSets['own'] ?? []) as $permission) {
if (is_string($permission) && $permission !== '') {
$permissions[] = $permission;
}
}
}
return array_values(array_unique($permissions));
}
}
@@ -0,0 +1,92 @@
<?php
app_require('traits/route_t.php');
use traits\route_t;
class AllowOwnOrDepartmentAccessForbiddenHost
{
use route_t;
/** @var array<string, bool> */
public array $permissionsByKey = [];
public bool $ownContext = true;
/** @var array<int, string>|null */
public ?array $lastForbidden = null;
public bool $departmentAccessChecked = false;
public function __construct()
{
// no-op for unit tests
}
public function hasPermission(string|\classes\permission_node $permission, int $customer_number = null): bool
{
$key = is_string($permission) ? $permission : (string)$permission->permission;
return (bool)($this->permissionsByKey[$key] ?? false);
}
public function isOwnCustomerContext(int $targetCustomerNumber): bool
{
return $this->ownContext;
}
public function requireDepartmentAccess(string $department, string|null $permission = null): void
{
$this->departmentAccessChecked = true;
}
protected function emitForbidden(array $permissions): void
{
$normalized = [];
foreach ($permissions as $permission) {
$key = is_string($permission) ? trim($permission) : trim((string)$permission->permission);
if ($key !== '') {
$normalized[] = $key;
}
}
$this->lastForbidden = array_values(array_unique($normalized));
throw new RuntimeException('forbidden');
}
}
it('returns both own and elevated permissions when both are missing', function (): void {
$host = new AllowOwnOrDepartmentAccessForbiddenHost();
$host->permissionsByKey = [
'perm_own' => false,
'perm_other' => false,
];
expect(fn() => $host->allowOwnOrDepartmentAccess('perm_own', 'perm_other', 101, null))
->toThrow(RuntimeException::class, 'forbidden');
expect($host->lastForbidden)->toBe(['perm_own', 'perm_other']);
});
it('returns only elevated permission when own permission exists but own context fails', function (): void {
$host = new AllowOwnOrDepartmentAccessForbiddenHost();
$host->permissionsByKey = [
'perm_own' => true,
'perm_other' => false,
];
$host->ownContext = false;
expect(fn() => $host->allowOwnOrDepartmentAccess('perm_own', 'perm_other', 202, null))
->toThrow(RuntimeException::class, 'forbidden');
expect($host->lastForbidden)->toBe(['perm_other']);
});
it('allows elevated permission path without forbidden and validates department access', function (): void {
$host = new AllowOwnOrDepartmentAccessForbiddenHost();
$host->permissionsByKey = [
'perm_own' => false,
'perm_other' => true,
];
$allowed = $host->allowOwnOrDepartmentAccess('perm_own', 'perm_other', 303, 77);
expect($allowed)->toBeTrue();
expect($host->departmentAccessChecked)->toBeTrue();
expect($host->lastForbidden)->toBeNull();
});
@@ -0,0 +1,19 @@
<?php
it('routes and trait use forbidden responses for permission and ownership denials', function (): void {
$routeTrait = file_get_contents(app_path('traits/route_t.php'));
expect($routeTrait)->not->toBeFalse();
expect($routeTrait)->toContain('protected function emitForbidden(array $permissions): void');
expect($routeTrait)->toContain('$response->forbidden($this->normalizePermissionKeys($permissions));');
expect($routeTrait)->toContain('$this->emitForbidden($missingPermissions);');
$selfserveRoute = file_get_contents(app_path('routes/departmentSelfserveVehicleConditionsRoute.php'));
expect($selfserveRoute)->not->toBeFalse();
expect($selfserveRoute)->toContain("forbidden(['list_department_selfserve_vehicle_conditions', 'list_own_department_selfserve_vehicle_conditions'])");
expect($selfserveRoute)->toContain('forbidDepartmentAccess(');
$ordersRoute = file_get_contents(app_path('routes/ordersRoute.php'));
expect($ordersRoute)->not->toBeFalse();
expect($ordersRoute)->toContain('$response->forbidden([$permission_other->permission]);');
expect($ordersRoute)->not->toContain("\$response->error('You do not have permission to edit this order', 403);");
});
@@ -16,6 +16,7 @@ namespace classes {
}
class response {
public function error($msg, $code) { throw new \Exception("Response Error ($code): $msg"); }
public function forbidden(array $permissions) { throw new \Exception("Response Error (403): Missing permission(s): " . implode(', ', $permissions)); }
public function add_meta($k, $v) {}
}
class permission_node {
+84 -5
View File
@@ -321,6 +321,71 @@ trait route_t
return null;
}
/**
* Convert a permission definition to its canonical string key.
*/
private function permissionKey(string|permission_node $permission): string
{
return $permission instanceof permission_node
? (string)$permission->permission
: (string)$permission;
}
/**
* Normalize and de-duplicate permission keys for forbidden responses.
* Accepts string keys and permission_node definitions.
*
* @param array<int, string|permission_node> $permissions
* @return array<int, string>
*/
private function normalizePermissionKeys(array $permissions): array
{
$keys = [];
foreach ($permissions as $permission) {
if ($permission instanceof permission_node) {
$key = trim((string)$permission->permission);
} elseif (is_string($permission)) {
$key = trim($permission);
} else {
continue;
}
if ($key !== '') {
$keys[] = $key;
}
}
return array_values(array_unique($keys));
}
/**
* Emit standardized forbidden response payload with missing permission keys.
* Extracted to allow focused unit tests by overriding this method.
*
* @param array<int, string|permission_node> $permissions
*/
protected function emitForbidden(array $permissions): void
{
global $response;
$response->forbidden($this->normalizePermissionKeys($permissions));
}
/**
* Emit a forbidden response for missing department scope access.
* Optionally include bypass permissions if they are relevant and missing.
*
* @param int $departmentId
* @param array<int, string|permission_node> $optionalBypassPermissions
*/
public function forbidDepartmentAccess(int $departmentId, array $optionalBypassPermissions = []): void
{
$missing = ['department_access_' . $departmentId];
foreach ($optionalBypassPermissions as $permission) {
if (!$this->hasPermission($permission)) {
$missing[] = $permission;
}
}
$this->emitForbidden($missing);
}
/**
* 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.
@@ -340,7 +405,7 @@ trait route_t
if ($resolvedCustomer === null) {
(new logs_o())->add('global', 'global', 1, $subuser->id ?? 0, 'PERMISSION_DENIED', 'Missing customer context for subuser permission evaluation: ' . $permission->permission);
if ($throwOnDeny) {
$response->error('Permission denied. Missing customer context for subuser.', 403);
$this->emitForbidden([$permission]);
}
return false;
}
@@ -367,7 +432,7 @@ trait route_t
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);
$this->emitForbidden([$permission]);
}
return $subuser_has_permission;
}
@@ -404,7 +469,7 @@ trait route_t
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);
$this->emitForbidden([$perm_string]);
}
return $allowed;
} catch (Exception $e) {
@@ -528,7 +593,21 @@ trait route_t
}
if (!$allowed) {
$response->error($denyMessage ?? 'Permission denied.', 403);
$missingPermissions = [];
if (!$hasOwn) {
$missingPermissions[] = $permissionOwn;
}
if (!$hasOther) {
$missingPermissions[] = $permissionOther;
}
// In own-scope failures (wrong customer/guard failure), report missing elevated permission only.
if ($hasOwn && !$hasOther) {
$missingPermissions[] = $permissionOther;
}
if (count($missingPermissions) === 0) {
$missingPermissions[] = $permissionOther;
}
$this->emitForbidden($missingPermissions);
}
return $allowed;
}
@@ -816,4 +895,4 @@ trait route_t
// Check if route is the same, or if it matches the regex pattern
return $route === $this->route || preg_match($route, $this->route);
}
}
}