Merge pull request #233 from copenhagentruckwash/fix-cross-tenant-vehicle-reference-leak
Restrict vehicle reference suggestions by department context
This commit is contained in:
@@ -46,7 +46,7 @@ class order_reference_suggestions_service
|
||||
$rows = [
|
||||
...$this->fetchBookingRows($departmentId, $search),
|
||||
...$this->fetchOrderRows($departmentId, $search),
|
||||
...$this->fetchVehicleRows($customerId, $plates, $search),
|
||||
...$this->fetchVehicleRows($departmentId, $customerId, $plates, $search),
|
||||
];
|
||||
|
||||
$suggestions = $this->aggregateRows($rows, $search, $customerId, $plates);
|
||||
@@ -137,7 +137,7 @@ class order_reference_suggestions_service
|
||||
* @param array<int, string> $plates
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function fetchVehicleRows(?int $customerId, array $plates, string $search): array
|
||||
private function fetchVehicleRows(int $departmentId, ?int $customerId, array $plates, string $search): array
|
||||
{
|
||||
$contextWhere = [];
|
||||
$params = [];
|
||||
@@ -171,6 +171,10 @@ class order_reference_suggestions_service
|
||||
$params['search'] = '%' . $this->lower($search) . '%';
|
||||
}
|
||||
|
||||
$where[] = $this->vehicleDepartmentAccessPredicate();
|
||||
$params['orders_department_id'] = $departmentId;
|
||||
$params['bookings_department_id'] = $departmentId;
|
||||
|
||||
$sql = "SELECT
|
||||
'vehicle' AS source,
|
||||
id AS origin_id,
|
||||
@@ -190,6 +194,41 @@ class order_reference_suggestions_service
|
||||
return $this->fetchRows($sql, $params);
|
||||
}
|
||||
|
||||
private function vehicleDepartmentAccessPredicate(): string
|
||||
{
|
||||
$ordersWhere = [
|
||||
'authorized_orders.department_id = :orders_department_id',
|
||||
'(authorized_orders.customer_id = customer_vehicles.customer_id'
|
||||
. " OR UPPER(REPLACE(authorized_orders.reg_1, ' ', '')) = UPPER(REPLACE(customer_vehicles.reg, ' ', ''))"
|
||||
. " OR UPPER(REPLACE(authorized_orders.reg_2, ' ', '')) = UPPER(REPLACE(customer_vehicles.reg, ' ', ''))"
|
||||
. " OR UPPER(REPLACE(authorized_orders.reg_3, ' ', '')) = UPPER(REPLACE(customer_vehicles.reg, ' ', '')))"
|
||||
];
|
||||
if ($this->tableHasColumn('orders', 'deleted_at')) {
|
||||
$ordersWhere[] = 'authorized_orders.deleted_at IS NULL';
|
||||
}
|
||||
|
||||
$bookingsWhere = [
|
||||
'authorized_bookings.department = :bookings_department_id',
|
||||
'(authorized_bookings.customer_number = customer_vehicles.customer_id'
|
||||
. " OR UPPER(REPLACE(authorized_bookings.reg_1, ' ', '')) = UPPER(REPLACE(customer_vehicles.reg, ' ', ''))"
|
||||
. " OR UPPER(REPLACE(authorized_bookings.reg_2, ' ', '')) = UPPER(REPLACE(customer_vehicles.reg, ' ', ''))"
|
||||
. " OR UPPER(REPLACE(authorized_bookings.reg_3, ' ', '')) = UPPER(REPLACE(customer_vehicles.reg, ' ', '')))"
|
||||
];
|
||||
if ($this->tableHasColumn('order_bookings', 'deleted_at')) {
|
||||
$bookingsWhere[] = 'authorized_bookings.deleted_at IS NULL';
|
||||
}
|
||||
|
||||
return '(EXISTS (
|
||||
SELECT 1
|
||||
FROM orders authorized_orders
|
||||
WHERE ' . implode(' AND ', $ordersWhere) . '
|
||||
) OR EXISTS (
|
||||
SELECT 1
|
||||
FROM order_bookings authorized_bookings
|
||||
WHERE ' . implode(' AND ', $bookingsWhere) . '
|
||||
))';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $params
|
||||
* @return array<int, array<string, mixed>>
|
||||
|
||||
@@ -179,6 +179,59 @@ it('orders reference suggestions by match relevance before context and frequency
|
||||
expect(array_slice($references, 0, 3))->toBe(['ABC', 'ABC-PREFIX', 'X-ABC-CONTAINS']);
|
||||
});
|
||||
|
||||
it('does not return vehicle reference suggestions outside the authorized department context', function (): void {
|
||||
api_test_covers('GET /orders/reference-suggestions', 'security');
|
||||
|
||||
$authorizedDepartment = api_fixtures()->createDepartment();
|
||||
$otherDepartment = api_fixtures()->createDepartment();
|
||||
$authorizedCustomer = api_fixtures()->createUser(['display_name' => 'Authorized Reference Customer']);
|
||||
$otherCustomer = api_fixtures()->createUser(['display_name' => 'Other Tenant Reference Customer']);
|
||||
$cashier = api_fixtures()->createUser(['display_name' => 'Reference Security Cashier']);
|
||||
|
||||
api_fixtures()->createOrder([
|
||||
'customer_id' => $authorizedCustomer['customer_number'],
|
||||
'cashier_id' => $cashier['id'],
|
||||
'department_id' => $authorizedDepartment['id'],
|
||||
'reference' => 'SAFE-DEPARTMENT-REF',
|
||||
'reg_1' => 'SAFE1',
|
||||
]);
|
||||
api_fixtures()->createOrder([
|
||||
'customer_id' => $otherCustomer['customer_number'],
|
||||
'cashier_id' => $cashier['id'],
|
||||
'department_id' => $otherDepartment['id'],
|
||||
'reference' => 'LEAK-ORDER-REF',
|
||||
'reg_1' => 'LEAK1',
|
||||
]);
|
||||
api_fixtures()->createVehicle([
|
||||
'customer_id' => $otherCustomer['customer_number'],
|
||||
'type' => 53,
|
||||
'reg' => 'LEAK1',
|
||||
'reference' => 'LEAK-VEHICLE-REF',
|
||||
]);
|
||||
|
||||
$session = api_fixtures()->createUserSession([
|
||||
'list_orders',
|
||||
'department_access_' . $authorizedDepartment['id'],
|
||||
]);
|
||||
|
||||
$response = api_client()->get('/orders/reference-suggestions?' . http_build_query([
|
||||
'search' => 'LEAK',
|
||||
'department_id' => $authorizedDepartment['id'],
|
||||
'customer_id' => $otherCustomer['customer_number'],
|
||||
'reg_1' => 'LEAK1',
|
||||
]), $session['headers']);
|
||||
|
||||
$response
|
||||
->assertStatus(200)
|
||||
->assertEnvelope()
|
||||
->assertSuccess();
|
||||
|
||||
$suggestions = $response->data();
|
||||
expect($suggestions)->toBeArray();
|
||||
expect(reference_suggestion_by_reference($suggestions, 'LEAK-VEHICLE-REF'))->toBeNull();
|
||||
expect(reference_suggestion_by_reference($suggestions, 'LEAK-ORDER-REF'))->toBeNull();
|
||||
});
|
||||
|
||||
it('enforces authentication, list permission, and department access for reference suggestions', function (): void {
|
||||
api_test_covers('GET /orders/reference-suggestions', 'auth');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user