Fix limited-backoffice permissions and add department access restrictions

This commit is contained in:
copilot-swe-agent[bot]
2026-07-06 23:12:01 +00:00
committed by GitHub
parent 04bb26f1b0
commit 53d0636193
6 changed files with 596 additions and 9 deletions
@@ -1606,3 +1606,526 @@ it('rejects invalid limited backoffice employee contact details', function (): v
->assertSuccess(false)
->assertMessage('Phone number must be 4-15 digits.');
});
it('includes list_departments in all active limited backoffice role presets', function (): void {
foreach (['cashier', 'booking_coordinator', 'operations_lead', 'department_admin'] as $roleKey) {
$permissions = limited_backoffice_role_preset_permissions($roleKey);
expect($permissions)
->toContain('list_departments', "$roleKey must include list_departments");
}
});
it('includes list_department_daily_reports (dagsopgørelse) in all active limited backoffice role presets', function (): void {
foreach (['cashier', 'booking_coordinator', 'operations_lead', 'department_admin'] as $roleKey) {
$permissions = limited_backoffice_role_preset_permissions($roleKey);
expect($permissions)
->toContain('list_department_daily_reports', "$roleKey must include list_department_daily_reports");
}
});
it('includes list_notifications and list_own_notifications (Notifikationer) in all active limited backoffice role presets', function (): void {
foreach (['cashier', 'booking_coordinator', 'operations_lead', 'department_admin'] as $roleKey) {
$permissions = limited_backoffice_role_preset_permissions($roleKey);
expect($permissions)
->toContain('list_notifications', "$roleKey must include list_notifications")
->toContain('list_own_notifications', "$roleKey must include list_own_notifications");
}
});
it('includes statistics_orders_new and statistics_bookings_new (Overblik) in all active limited backoffice role presets', function (): void {
foreach (['cashier', 'booking_coordinator', 'operations_lead', 'department_admin'] as $roleKey) {
$permissions = limited_backoffice_role_preset_permissions($roleKey);
expect($permissions)
->toContain('statistics_orders_new', "$roleKey must include statistics_orders_new")
->toContain('statistics_bookings_new', "$roleKey must include statistics_bookings_new");
}
});
it('assigns list_departments to managed employees and enforces list_departments permission on GET /departments', function (): void {
api_test_covers('GET /departments', 'limited backoffice employee');
$department = api_fixtures()->createDepartment(['name' => 'LB Dept List Test']);
$manager = limited_backoffice_manager_session([(int)$department['id']]);
$result = api_client()->post('/limited-backoffice/employees', [
'role_key' => 'cashier',
'department_ids' => [(int)$department['id']],
'display_name' => 'Dept List Test Employee',
'email' => 'dept-list-test@example.test',
'password' => 'Secret123!',
], $manager['headers']);
$result->assertStatus(200)->assertEnvelope()->assertSuccess();
$employeeId = (int)($result->data()['id'] ?? 0);
expect($employeeId)->toBeGreaterThan(0);
limited_backoffice_cleanup_created_employee($employeeId);
$employeeRow = api_test_runtime()->queryOne(
'SELECT `managed_group_id` FROM `limited_backoffice_employees` WHERE `user_id` = ' . $employeeId . ' LIMIT 1'
);
$managedGroupId = (int)($employeeRow['managed_group_id'] ?? 0);
expect($managedGroupId)->toBeGreaterThan(0);
$permRow = api_test_runtime()->queryOne(
'SELECT 1 FROM `groups_permissions`
WHERE `group_id` = ' . $managedGroupId . " AND `permission` = 'list_departments' LIMIT 1"
);
expect($permRow)->not->toBeNull('Managed employee must have list_departments permission');
});
it('assigns list_department_daily_reports to managed employees', function (): void {
$department = api_fixtures()->createDepartment(['name' => 'LB Daily Report Perm Test']);
$manager = limited_backoffice_manager_session([(int)$department['id']]);
$result = api_client()->post('/limited-backoffice/employees', [
'role_key' => 'cashier',
'department_ids' => [(int)$department['id']],
'display_name' => 'Daily Report Perm Employee',
'email' => 'daily-report-perm@example.test',
'password' => 'Secret123!',
], $manager['headers']);
$result->assertStatus(200)->assertEnvelope()->assertSuccess();
$employeeId = (int)($result->data()['id'] ?? 0);
expect($employeeId)->toBeGreaterThan(0);
limited_backoffice_cleanup_created_employee($employeeId);
$employeeRow = api_test_runtime()->queryOne(
'SELECT `managed_group_id` FROM `limited_backoffice_employees` WHERE `user_id` = ' . $employeeId . ' LIMIT 1'
);
$managedGroupId = (int)($employeeRow['managed_group_id'] ?? 0);
expect($managedGroupId)->toBeGreaterThan(0);
$permRow = api_test_runtime()->queryOne(
'SELECT 1 FROM `groups_permissions`
WHERE `group_id` = ' . $managedGroupId . " AND `permission` = 'list_department_daily_reports' LIMIT 1"
);
expect($permRow)->not->toBeNull('Managed employee must have list_department_daily_reports permission');
});
it('assigns list_notifications and list_own_notifications to managed employees', function (): void {
$department = api_fixtures()->createDepartment(['name' => 'LB Notifications Perm Test']);
$manager = limited_backoffice_manager_session([(int)$department['id']]);
$result = api_client()->post('/limited-backoffice/employees', [
'role_key' => 'cashier',
'department_ids' => [(int)$department['id']],
'display_name' => 'Notifications Perm Employee',
'email' => 'notifications-perm@example.test',
'password' => 'Secret123!',
], $manager['headers']);
$result->assertStatus(200)->assertEnvelope()->assertSuccess();
$employeeId = (int)($result->data()['id'] ?? 0);
expect($employeeId)->toBeGreaterThan(0);
limited_backoffice_cleanup_created_employee($employeeId);
$employeeRow = api_test_runtime()->queryOne(
'SELECT `managed_group_id` FROM `limited_backoffice_employees` WHERE `user_id` = ' . $employeeId . ' LIMIT 1'
);
$managedGroupId = (int)($employeeRow['managed_group_id'] ?? 0);
expect($managedGroupId)->toBeGreaterThan(0);
foreach (['list_notifications', 'list_own_notifications'] as $perm) {
$permRow = api_test_runtime()->queryOne(
'SELECT 1 FROM `groups_permissions`
WHERE `group_id` = ' . $managedGroupId . " AND `permission` = '" . $perm . "' LIMIT 1"
);
expect($permRow)->not->toBeNull("Managed employee must have $perm permission");
}
});
it('assigns statistics permissions (Overblik) to all active limited backoffice role presets including cashier and booking_coordinator', function (): void {
$department = api_fixtures()->createDepartment(['name' => 'LB Stats Perm Test']);
$manager = limited_backoffice_manager_session([(int)$department['id']]);
foreach (['cashier', 'booking_coordinator'] as $roleKey) {
$result = api_client()->post('/limited-backoffice/employees', [
'role_key' => $roleKey,
'department_ids' => [(int)$department['id']],
'display_name' => 'Stats Perm ' . $roleKey,
'email' => 'stats-perm-' . $roleKey . '@example.test',
'password' => 'Secret123!',
], $manager['headers']);
$result->assertStatus(200)->assertEnvelope()->assertSuccess();
$employeeId = (int)($result->data()['id'] ?? 0);
expect($employeeId)->toBeGreaterThan(0);
limited_backoffice_cleanup_created_employee($employeeId);
$employeeRow = api_test_runtime()->queryOne(
'SELECT `managed_group_id` FROM `limited_backoffice_employees` WHERE `user_id` = ' . $employeeId . ' LIMIT 1'
);
$managedGroupId = (int)($employeeRow['managed_group_id'] ?? 0);
expect($managedGroupId)->toBeGreaterThan(0);
foreach (['statistics_orders_new', 'statistics_bookings_new'] as $perm) {
$permRow = api_test_runtime()->queryOne(
'SELECT 1 FROM `groups_permissions`
WHERE `group_id` = ' . $managedGroupId . " AND `permission` = '" . $perm . "' LIMIT 1"
);
expect($permRow)->not->toBeNull("$roleKey managed employee must have $perm permission");
}
}
});
it('enforces department access when creating an order via POST /orders', function (): void {
api_test_covers('POST /orders', 'department access');
$allowedDepartment = api_fixtures()->createDepartment(['name' => 'Order Create Allowed Dept']);
$deniedDepartment = api_fixtures()->createDepartment(['name' => 'Order Create Denied Dept']);
$customer = api_fixtures()->createUser(['display_name' => 'Order Create Customer']);
$session = api_fixtures()->createUserSession([
'add_order',
'department_access_' . (int)$allowedDepartment['id'],
]);
// Should succeed for accessible department
$created = api_client()->post('/orders', [
'customer_id' => $customer['customer_number'],
'department_id' => (int)$allowedDepartment['id'],
'reference' => 'DEPT-ACCESS-TEST',
'notes' => '',
'reg_1' => 'ABCD111',
], $session['headers']);
$created->assertStatus(200)->assertEnvelope()->assertSuccess();
// Should fail for inaccessible department
api_client()->post('/orders', [
'customer_id' => $customer['customer_number'],
'department_id' => (int)$deniedDepartment['id'],
'reference' => 'DEPT-ACCESS-DENIED',
'notes' => '',
'reg_1' => 'ABCD222',
], $session['headers'])
->assertStatus(403)
->assertEnvelope()
->assertSuccess(false)
->assertMissingPermissions(['department_access_' . (int)$deniedDepartment['id']]);
});
it('enforces department access on the existing order when editing via PUT /order', function (): void {
api_test_covers('PUT /order', 'department access');
$allowedDepartment = api_fixtures()->createDepartment(['name' => 'Order Edit Allowed Dept']);
$deniedDepartment = api_fixtures()->createDepartment(['name' => 'Order Edit Denied Dept']);
$customer = api_fixtures()->createUser(['display_name' => 'Order Edit Customer']);
$orderInAllowed = api_fixtures()->createOrder([
'customer_id' => $customer['customer_number'],
'department_id' => (int)$allowedDepartment['id'],
]);
$orderInDenied = api_fixtures()->createOrder([
'customer_id' => $customer['customer_number'],
'department_id' => (int)$deniedDepartment['id'],
]);
$session = api_fixtures()->createUserSession([
'edit_order',
'department_access_' . (int)$allowedDepartment['id'],
]);
// Should succeed editing order in accessible department
api_client()->put('/order', [
'id' => (int)$orderInAllowed['id'],
'notes' => 'updated',
], $session['headers'])
->assertStatus(200)
->assertEnvelope()
->assertSuccess();
// Should fail editing order in inaccessible department
api_client()->put('/order', [
'id' => (int)$orderInDenied['id'],
'notes' => 'should be denied',
], $session['headers'])
->assertStatus(403)
->assertEnvelope()
->assertSuccess(false)
->assertMissingPermissions(['department_access_' . (int)$deniedDepartment['id']]);
});
it('enforces department access when moving an order to a new department via PUT /order', function (): void {
api_test_covers('PUT /order', 'department access move');
$allowedDepartment = api_fixtures()->createDepartment(['name' => 'Order Move Allowed Dept']);
$deniedDepartment = api_fixtures()->createDepartment(['name' => 'Order Move Denied Dept']);
$customer = api_fixtures()->createUser(['display_name' => 'Order Move Customer']);
$order = api_fixtures()->createOrder([
'customer_id' => $customer['customer_number'],
'department_id' => (int)$allowedDepartment['id'],
]);
$sessionBothDepts = api_fixtures()->createUserSession([
'edit_order',
'department_access_' . (int)$allowedDepartment['id'],
'department_access_' . (int)$deniedDepartment['id'],
]);
$sessionOnlyAllowed = api_fixtures()->createUserSession([
'edit_order',
'department_access_' . (int)$allowedDepartment['id'],
]);
// Should fail when moving to inaccessible department (user only has access to allowedDepartment)
api_client()->put('/order', [
'id' => (int)$order['id'],
'department_id' => (int)$deniedDepartment['id'],
], $sessionOnlyAllowed['headers'])
->assertStatus(403)
->assertEnvelope()
->assertSuccess(false)
->assertMissingPermissions(['department_access_' . (int)$deniedDepartment['id']]);
// Should succeed when user has access to both departments
api_client()->put('/order', [
'id' => (int)$order['id'],
'department_id' => (int)$deniedDepartment['id'],
], $sessionBothDepts['headers'])
->assertStatus(200)
->assertEnvelope()
->assertSuccess();
});
it('enforces department access when marking an order as completed via POST /orders/mark_as_completed', function (): void {
api_test_covers('POST /orders/mark_as_completed', 'department access');
$allowedDepartment = api_fixtures()->createDepartment(['name' => 'Order Complete Allowed Dept']);
$deniedDepartment = api_fixtures()->createDepartment(['name' => 'Order Complete Denied Dept']);
$customer = api_fixtures()->createUser(['display_name' => 'Order Complete Customer']);
$orderAllowed = api_fixtures()->createOrder([
'customer_id' => $customer['customer_number'],
'department_id' => (int)$allowedDepartment['id'],
]);
$orderDenied = api_fixtures()->createOrder([
'customer_id' => $customer['customer_number'],
'department_id' => (int)$deniedDepartment['id'],
]);
$session = api_fixtures()->createUserSession([
'mark_order_as_completed',
'department_access_' . (int)$allowedDepartment['id'],
]);
// Should fail for inaccessible department
api_client()->post('/orders/mark_as_completed', [
'id' => (int)$orderDenied['id'],
], $session['headers'])
->assertStatus(403)
->assertEnvelope()
->assertSuccess(false)
->assertMissingPermissions(['department_access_' . (int)$deniedDepartment['id']]);
// Should succeed for accessible department
api_client()->post('/orders/mark_as_completed', [
'id' => (int)$orderAllowed['id'],
], $session['headers'])
->assertStatus(200)
->assertEnvelope()
->assertSuccess();
});
it('enforces department access when deleting order attachments via DELETE /orders/attachments', function (): void {
api_test_covers('DELETE /orders/attachments', 'department access');
$allowedDepartment = api_fixtures()->createDepartment(['name' => 'Order Del Attach Allowed']);
$deniedDepartment = api_fixtures()->createDepartment(['name' => 'Order Del Attach Denied']);
$customer = api_fixtures()->createUser(['display_name' => 'Order Del Attach Customer']);
$orderAllowed = api_fixtures()->createOrder([
'customer_id' => $customer['customer_number'],
'department_id' => (int)$allowedDepartment['id'],
]);
$orderDenied = api_fixtures()->createOrder([
'customer_id' => $customer['customer_number'],
'department_id' => (int)$deniedDepartment['id'],
]);
$attachmentAllowed = api_fixtures()->createOrderAttachment(['order_id' => (int)$orderAllowed['id']]);
$attachmentDenied = api_fixtures()->createOrderAttachment(['order_id' => (int)$orderDenied['id']]);
$session = api_fixtures()->createUserSession([
'delete_order_attachments',
'department_access_' . (int)$allowedDepartment['id'],
]);
// Should fail for inaccessible department
api_client()->delete('/orders/attachments', [
'order_id' => (int)$orderDenied['id'],
'attachment_id' => (int)$attachmentDenied['id'],
], $session['headers'])
->assertStatus(403)
->assertEnvelope()
->assertSuccess(false)
->assertMissingPermissions(['department_access_' . (int)$deniedDepartment['id']]);
// Should succeed for accessible department
api_client()->delete('/orders/attachments', [
'order_id' => (int)$orderAllowed['id'],
'attachment_id' => (int)$attachmentAllowed['id'],
], $session['headers'])
->assertStatus(200)
->assertEnvelope()
->assertSuccess();
});
it('enforces department access when adding order items via POST /order/items', function (): void {
api_test_covers('POST /order/items', 'department access');
$allowedDepartment = api_fixtures()->createDepartment(['name' => 'Order Item Add Allowed']);
$deniedDepartment = api_fixtures()->createDepartment(['name' => 'Order Item Add Denied']);
$customer = api_fixtures()->createUser(['display_name' => 'Order Item Add Customer']);
$category = api_fixtures()->createCategory(['name' => 'Order Item Dept Access Category']);
$product = api_fixtures()->createProduct(['name' => 'Order Item Dept Access Product', 'category' => $category['id']]);
$orderAllowed = api_fixtures()->createOrder([
'customer_id' => $customer['customer_number'],
'department_id' => (int)$allowedDepartment['id'],
]);
$orderDenied = api_fixtures()->createOrder([
'customer_id' => $customer['customer_number'],
'department_id' => (int)$deniedDepartment['id'],
]);
$session = api_fixtures()->createUserSession([
'add_order_items',
'department_access_' . (int)$allowedDepartment['id'],
]);
// Should fail for inaccessible department
api_client()->post('/order/items', [
'order_id' => (int)$orderDenied['id'],
'product_id' => (int)$product['id'],
'quantity' => 1,
], $session['headers'])
->assertStatus(403)
->assertEnvelope()
->assertSuccess(false)
->assertMissingPermissions(['department_access_' . (int)$deniedDepartment['id']]);
// Should succeed for accessible department
api_client()->post('/order/items', [
'order_id' => (int)$orderAllowed['id'],
'product_id' => (int)$product['id'],
'quantity' => 1,
], $session['headers'])
->assertStatus(200)
->assertEnvelope()
->assertSuccess();
});
it('enforces department access when deleting order items via DELETE /order/items', function (): void {
api_test_covers('DELETE /order/items', 'department access');
$allowedDepartment = api_fixtures()->createDepartment(['name' => 'Order Item Del Allowed']);
$deniedDepartment = api_fixtures()->createDepartment(['name' => 'Order Item Del Denied']);
$customer = api_fixtures()->createUser(['display_name' => 'Order Item Del Customer']);
$category = api_fixtures()->createCategory(['name' => 'Order Item Del Category']);
$product = api_fixtures()->createProduct(['name' => 'Order Item Del Product', 'category' => $category['id']]);
$orderAllowed = api_fixtures()->createOrder([
'customer_id' => $customer['customer_number'],
'department_id' => (int)$allowedDepartment['id'],
]);
$orderDenied = api_fixtures()->createOrder([
'customer_id' => $customer['customer_number'],
'department_id' => (int)$deniedDepartment['id'],
]);
$session = api_fixtures()->createUserSession([
'delete_order_items',
'department_access_' . (int)$allowedDepartment['id'],
]);
$superuserSession = api_fixtures()->createUserSession([], ['group_id' => 1]);
$itemAllowedResponse = api_client()->post('/order/items', [
'order_id' => (int)$orderAllowed['id'],
'product_id' => (int)$product['id'],
'quantity' => 1,
], $superuserSession['headers']);
$itemAllowed = $itemAllowedResponse->data();
$itemDeniedResponse = api_client()->post('/order/items', [
'order_id' => (int)$orderDenied['id'],
'product_id' => (int)$product['id'],
'quantity' => 1,
], $superuserSession['headers']);
$itemDenied = $itemDeniedResponse->data();
// Should fail for inaccessible department
api_client()->delete('/order/items', ['id' => (int)$itemDenied['id']], $session['headers'])
->assertStatus(403)
->assertEnvelope()
->assertSuccess(false)
->assertMissingPermissions(['department_access_' . (int)$deniedDepartment['id']]);
// Should succeed for accessible department
api_client()->delete('/order/items', ['id' => (int)$itemAllowed['id']], $session['headers'])
->assertStatus(200)
->assertEnvelope()
->assertSuccess();
});
it('enforces department access when editing order items via PUT /order/items', function (): void {
api_test_covers('PUT /order/items', 'department access');
$allowedDepartment = api_fixtures()->createDepartment(['name' => 'Order Item Edit Allowed']);
$deniedDepartment = api_fixtures()->createDepartment(['name' => 'Order Item Edit Denied']);
$customer = api_fixtures()->createUser(['display_name' => 'Order Item Edit Customer']);
$category = api_fixtures()->createCategory(['name' => 'Order Item Edit Category']);
$product = api_fixtures()->createProduct(['name' => 'Order Item Edit Product', 'category' => $category['id']]);
$orderAllowed = api_fixtures()->createOrder([
'customer_id' => $customer['customer_number'],
'department_id' => (int)$allowedDepartment['id'],
]);
$orderDenied = api_fixtures()->createOrder([
'customer_id' => $customer['customer_number'],
'department_id' => (int)$deniedDepartment['id'],
]);
$itemAllowed = api_fixtures()->createOrderItem([
'order_id' => (int)$orderAllowed['id'],
'product_id' => (int)$product['id'],
'cashier_id' => 1,
'price' => 100,
]);
$itemDenied = api_fixtures()->createOrderItem([
'order_id' => (int)$orderDenied['id'],
'product_id' => (int)$product['id'],
'cashier_id' => 1,
'price' => 200,
]);
$session = api_fixtures()->createUserSession([
'edit_order_items',
'list_order_items',
'department_access_' . (int)$allowedDepartment['id'],
]);
// Should fail for inaccessible department
api_client()->put('/order/items', [
'id' => (int)$itemDenied['id'],
'price' => 999,
'notes' => '',
'reference' => '',
'quantity' => 1,
], $session['headers'])
->assertStatus(403)
->assertEnvelope()
->assertSuccess(false)
->assertMissingPermissions(['department_access_' . (int)$deniedDepartment['id']]);
// Should succeed for accessible department
api_client()->put('/order/items', [
'id' => (int)$itemAllowed['id'],
'price' => 150,
'notes' => '',
'reference' => '',
'quantity' => 1,
], $session['headers'])
->assertStatus(200)
->assertEnvelope()
->assertSuccess();
});