Fix system search department scoping for generic entities

This commit is contained in:
Jeppe B
2026-06-01 23:58:03 +02:00
parent a22a3f4ee9
commit 8a749cffa3
3 changed files with 68 additions and 2 deletions
@@ -28,6 +28,7 @@ class system_search_service
$allowedTypes = $this->normalizeTypes((array)($options['allowed_types'] ?? []));
$ownOnlyTypes = $this->normalizeTypes((array)($options['own_only_types'] ?? []));
$ownCustomerNumber = isset($options['own_customer_number']) ? (int)$options['own_customer_number'] : null;
$allowedDepartmentIds = array_values(array_unique(array_map('intval', (array)($options['allowed_department_ids'] ?? []))));
$permissionsCatalogAll = (array)($options['permissions_catalog_all'] ?? []);
$permissionsCatalogOwn = (array)($options['permissions_catalog_own'] ?? []);
$moduleConfigVisibility = (array)($options['module_config_visibility'] ?? []);
@@ -81,6 +82,7 @@ class system_search_service
'offset' => $offset,
'own' => $ownCustomerNumber,
'own_only' => $ownOnlyTypes,
'dept' => $allowedDepartmentIds,
'assoc' => $includeAssociations,
'dbg' => $debugIntent,
'ctx' => $this->permissionContextFingerprint($permissionsCatalogAll, $permissionsCatalogOwn, $moduleConfigVisibility),
@@ -103,7 +105,8 @@ class system_search_service
$ownCustomerNumber,
$permissionsCatalogAll,
$permissionsCatalogOwn,
$moduleConfigVisibility
$moduleConfigVisibility,
$allowedDepartmentIds
);
$intentMeta = [
@@ -147,7 +150,8 @@ class system_search_service
$ownCustomerNumber,
$permissionsCatalogAll,
$permissionsCatalogOwn,
$moduleConfigVisibility
$moduleConfigVisibility,
$allowedDepartmentIds
);
} else {
$intentMeta['status'] = 'fallback';
@@ -177,6 +181,7 @@ class system_search_service
$permissionsCatalogAll,
$permissionsCatalogOwn,
$moduleConfigVisibility,
$allowedDepartmentIds,
[$customerNumber]
);
foreach ($associated as &$item) {
@@ -237,6 +242,7 @@ class system_search_service
* @param array<string, string> $permissionsCatalogAll
* @param array<int, string> $permissionsCatalogOwn
* @param array<string, bool> $moduleConfigVisibility
* @param array<int, int> $allowedDepartmentIds
* @param array<int, int> $forcedCustomerNumbers
* @return array<int, array<string, mixed>>
*/
@@ -249,6 +255,7 @@ class system_search_service
array $permissionsCatalogAll,
array $permissionsCatalogOwn,
array $moduleConfigVisibility,
array $allowedDepartmentIds = [],
array $forcedCustomerNumbers = []
): array {
$results = [];
@@ -264,6 +271,7 @@ class system_search_service
$permissionsCatalogAll,
$permissionsCatalogOwn,
$moduleConfigVisibility,
$allowedDepartmentIds,
$forcedCustomerNumbers
);
$results = $this->mergeResults($results, $rows);
@@ -276,6 +284,7 @@ class system_search_service
* @param array<string, string> $permissionsCatalogAll
* @param array<int, string> $permissionsCatalogOwn
* @param array<string, bool> $moduleConfigVisibility
* @param array<int, int> $allowedDepartmentIds
* @param array<int, int> $forcedCustomerNumbers
* @return array<int, array<string, mixed>>
*/
@@ -288,6 +297,7 @@ class system_search_service
array $permissionsCatalogAll,
array $permissionsCatalogOwn,
array $moduleConfigVisibility,
array $allowedDepartmentIds,
array $forcedCustomerNumbers
): array {
if ($this->isGenericEntityType($entityType)) {
@@ -297,6 +307,7 @@ class system_search_service
$entityBoost,
$ownOnly,
$ownCustomerNumber,
$allowedDepartmentIds,
$forcedCustomerNumbers
);
}
@@ -784,6 +795,7 @@ class system_search_service
/**
* @param array<int, string> $terms
* @param array<int, int> $allowedDepartmentIds
* @param array<int, int> $forcedCustomerNumbers
* @return array<int, array<string, mixed>>
*/
@@ -793,6 +805,7 @@ class system_search_service
int $entityBoost,
bool $ownOnly,
?int $ownCustomerNumber,
array $allowedDepartmentIds = [],
array $forcedCustomerNumbers = []
): array {
if (empty($terms)) {
@@ -891,6 +904,8 @@ class system_search_service
$terms,
$customerNumbers,
$customerField,
$allowedDepartmentIds,
$departmentField,
$fixedConditions
);
@@ -1067,6 +1082,8 @@ class system_search_service
* @param array<int, string> $terms
* @param array<int, int> $customerNumbers
* @param string|null $customerField
* @param array<int, int> $departmentIds
* @param string|null $departmentField
* @param array<string, mixed> $fixedConditions
* @return array<int, array<string, mixed>>
*/
@@ -1077,6 +1094,8 @@ class system_search_service
array $terms,
array $customerNumbers = [],
?string $customerField = null,
array $departmentIds = [],
?string $departmentField = null,
array $fixedConditions = []
): array {
global $db;
@@ -1109,6 +1128,10 @@ class system_search_service
$wheres[] = "`$customerField` IN (" . implode(',', array_map('intval', $customerNumbers)) . ")";
}
if (!empty($departmentIds) && $departmentField !== null && in_array($departmentField, $fields, true)) {
$wheres[] = "`$departmentField` IN (" . implode(',', array_map('intval', $departmentIds)) . ")";
}
$termClauses = [];
foreach ($terms as $term) {
$escaped = $db->escape_string($term);
@@ -113,6 +113,7 @@ class systemSearchRoute
'allowed_types' => $allowedTypes,
'own_only_types' => $ownOnlyTypes,
'own_customer_number' => $this->resolveEffectiveCustomerNumber(),
'allowed_department_ids' => $this->resolveAllowedDepartmentIds($user),
'permissions_catalog_all' => $permissionsCatalogAll,
'permissions_catalog_own' => $permissionsCatalogOwn,
'module_config_visibility' => $this->buildModuleConfigVisibility(),
@@ -503,6 +504,27 @@ class systemSearchRoute
return $flat;
}
/**
* @param mixed $user
* @return array<int, int>
*/
private function resolveAllowedDepartmentIds(mixed $user): array
{
if ($user === false) {
return [];
}
try {
$departments = $user->getGroup()->getDepartments();
if (!is_array($departments)) {
return [];
}
return array_values(array_unique(array_map('intval', $departments)));
} catch (Throwable) {
return [];
}
}
private function parseTypeList(mixed $value): array
{
$result = [];
@@ -127,6 +127,7 @@ if (!class_exists('TestableSystemSearchService')) {
array $permissionsCatalogAll,
array $permissionsCatalogOwn,
array $moduleConfigVisibility,
array $allowedDepartmentIds = [],
array $forcedCustomerNumbers = []
): array {
$this->lexicalCalls[] = [
@@ -135,6 +136,7 @@ if (!class_exists('TestableSystemSearchService')) {
'entityBoost' => $entityBoost,
'ownOnlyTypes' => $ownOnlyTypes,
'ownCustomerNumber' => $ownCustomerNumber,
'allowedDepartmentIds' => $allowedDepartmentIds,
'forcedCustomerNumbers' => $forcedCustomerNumbers,
];
if (empty($this->queuedLexicalResults)) {
@@ -348,3 +350,22 @@ it('caps AI-driven expanded terms to prevent query amplification', function ():
}
expect($maxLen)->toBeLessThanOrEqual(64);
});
it('passes allowed department ids into lexical execution context', function (): void {
$parser = new FakeSystemSearchIntentParser();
$service = new TestableSystemSearchService($parser, [[
['entity_type' => 'orders', 'entity_id' => '1', 'title' => 'Order #1', 'score' => 70],
]]);
$service->search([
'query' => 'order',
'allowed_types' => ['orders'],
'include_associations' => false,
'allowed_department_ids' => [3, '7', 3],
]);
expect(count($service->lexicalCalls))->toBe(1);
expect($service->lexicalCalls[0]['allowedDepartmentIds'])->toBe([3, 7]);
});