Skip to content

Fixed case search fan-out by scoping role check to the current tenant. - #153

Merged
richard-churchman merged 1 commit into
masterfrom
Fan-Out-Security-Isolation-in-Case-Page
Aug 31, 2026
Merged

Fixed case search fan-out by scoping role check to the current tenant.#153
richard-churchman merged 1 commit into
masterfrom
Fan-Out-Security-Isolation-in-Case-Page

Conversation

@richard-churchman

@richard-churchman richard-churchman commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixed case search fan-out by scoping role check to the current tenant. CompileAsync built the case-search WHERE clause with a derived "RoleRegistry" table that resolved the caller's role by UserRegistry.Name alone, with no tenant predicate. UserRegistry rows are not unique per username across tenants (the same login can belong to multiple tenants via UserInTenant), so a user in more than one tenant could get back more than one RoleRegistry.Guid here. That fed the CaseWorkflowRole/CaseWorkflowStatusRole join predicates, which are junction tables (one row per workflow/status x granted role). Each extra role match multiplied the Case row in the joined result, producing duplicate cases in the search results, sourced from role grants in other tenants.

SessionCaseSearchCompiledSqlController's limit-100 query has no DISTINCT, so the duplicates could also crowd out genuinely distinct cases within a page.

Moved the CaseWorkflowRole/CaseWorkflowStatusRole/RoleRegistry checks out of the joined FROM list and into EXISTS subqueries correlated to the current CaseWorkflow/CaseWorkflowStatus/TenantRegistry, matching the EXISTS-based permission-check pattern already used elsewhere (GetCaseByIdQuery, GetCaseByCaseKeyValueQuery, etc.). Role matches can no longer multiply the outer Case row regardless of how many tenants or roles the user holds.

Not reproduced locally; but matched a known fan-out/duplicate-case pattern seen before. Have verified this fix against a live case search.

CompileAsync built the case-search WHERE clause with a derived
"RoleRegistry" table that resolved the caller's role by
UserRegistry.Name alone, with no tenant predicate. UserRegistry rows
are not unique per username across tenants (the same login can belong
to multiple tenants via UserInTenant), so a user in more than one
tenant could get back more than one RoleRegistry.Guid here.

That fed the CaseWorkflowRole/CaseWorkflowStatusRole join predicates,
which are junction tables (one row per workflow/status x granted
role). Each extra role match multiplied the Case row in the joined
result, producing duplicate cases in the search results, sourced from
role grants in other tenants. SessionCaseSearchCompiledSqlController's
limit-100 query has no DISTINCT, so the duplicates could also crowd
out genuinely distinct cases within a page.

Moved the CaseWorkflowRole/CaseWorkflowStatusRole/RoleRegistry checks
out of the joined FROM list and into EXISTS subqueries correlated to
the current CaseWorkflow/CaseWorkflowStatus/TenantRegistry, matching
the EXISTS-based permission-check pattern already used elsewhere
(GetCaseByIdQuery, GetCaseByCaseKeyValueQuery, etc.). Role matches can
no longer multiply the outer Case row regardless of how many tenants
or roles the user holds.

Not reproduced locally; but matched a known fan-out/duplicate-case pattern seen before. Have verified this fix against a live case search.
@richard-churchman
richard-churchman merged commit 92d2fa1 into master Aug 31, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant