fix(multitenancy): guard empty orX() in applyActiveOrgFilter — silently-empty object-source reads (fixes #2084) - #2088
Merged
Conversation
…ly-empty object-source reads (fixes #2084) MultiTenancyTrait::applyActiveOrgFilter() always called $qb->expr()->orX() with zero initial arguments and relied on every caller adding at least one predicate afterwards. OCP's IExpressionBuilder::orX() docblock documents that a zero-argument call "requires at least one defined when converting to string", and NC34 already logs a deprecation exception on every such call, flagging that it will throw in a future release. Build the organisation predicates into a plain array first (buildOrganisationConditions(), replacing addOrganisationConditions()) and only construct orX(...$predicates) once real predicates exist. If the predicate list is ever empty — an invariant break, since the only caller already guards against an empty $activeOrgUuids — fail CLOSED with an unconditional `1 = 0` instead of calling orX() with no arguments or skipping the WHERE clause, per the tenant-isolation guarantee in openspec/specs/tenant-isolation-audit/spec.md ("the system MUST verify that no Organisation's query filter returns objects belonging to another Organisation"). Adds a regression test driving applyActiveOrgFilter() directly (via reflection) with an empty active-org list, asserting orX() is never called with zero arguments and the query fails closed.
Contributor
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| composer | ❌ | ✅ 174/174 | |||
| npm | ✅ | ✅ 555/555 | |||
| PHPUnit | ⏭️ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ |
Quality workflow — 2026-07-24 20:05 UTC
Download the full PDF report from the workflow artifacts.
This was referenced Jul 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #2084.
MultiTenancyTrait::applyActiveOrgFilter()always constructed$qb->expr()->orX()with zero initial arguments and relied on everydownstream code path to
->add()at least one predicate afterwards.OCP's
IExpressionBuilder::orX()docblock documents that a zero-argumentcall "requires at least one defined when converting to string", and this
NC34 instance already logs a deprecation
Exceptionon every such call,warning that it "will throw soon" in a future release.
buildOrganisationConditions()(replacing the formeraddOrganisationConditions()) now returns a plain array of predicatesinstead of mutating a composite expression in place.
applyActiveOrgFilter()only calls$qb->expr()->orX(...$predicates)once at least one real predicate exists.
caller,
applyOrganisationFilter(), already guards against an empty$activeOrgUuidsbefore reaching this method), the guard failsCLOSED with an unconditional
1 = 0rather than callingorX()with no arguments, or skipping the
WHEREclause and failing open.This matches the tenant-isolation guarantee in
openspec/specs/tenant-isolation-audit/spec.md("the system MUSTverify that no Organisation's query filter returns objects belonging to
another Organisation") and mirrors the existing no-active-org behaviour
in
applyNoActiveOrgFilter().Root-cause note (see issue for full investigation)
Live-reproduced on the shared dev instance: flipping
multitenancyto{"saasMode":true,"adminOverride":true}and queryingthe
v-app-marketdbal-source (x-openregister-object-source) schemaunder register 2479 returned
total: 0instead of real rows. Thenextcloud.logtrace showed theorX()zero-arg deprecation warningfiring repeatedly (confirming this defect is real and present), but the
actual emptiness came from a second, related defect one layer up:
DbalObjectSourceProvider::resolveSource()callsSourceMapper::findAll(filters: ['uuid' => $sourceId]), which appliesthe same organisation-scoped filter (with
allowNullOrg: false) to theshared
Sourceentity. Because thespectr intelligence-dbSourcerow's own
organisationdiffers from the querying admin's activeorganisation, SaaS mode (which disables admin override) excludes the
Sourcerow entirely,resolveSource()returnsnull, and everydownstream
find/findAll/count/aggregateon that schema silentlyreturns empty with only a
warning-level log line(
[ObjectSource:dbal-source] no source resolved for schema ...) — noexception ever surfaces.
This PR fixes the
orX()defect exactly as reported and adds aregression test for it, but does not by itself restore dbal-source
results while
saasModeistrue, because that second defect (sharedsystem
Sourcerows being subject to per-tenant org filtering at all) isa separate design question. Filing a follow-up issue for that; the
{"saasMode":false,"adminOverride":true}workaround should stay in placeon the shared instance until it's resolved.
Test plan
php -l lib/Db/MultiTenancyTrait.phpcomposer phpcs(project-wide,lib/scope) — clean, 0 errorstests/Unit/Db/MultiTenancyTraitApplyActiveOrgFilterTest.php:drives
applyActiveOrgFilter()directly via reflection with anempty active-org list, asserts
orX()is never invoked (let alonewith zero args) and the query fails closed (
andWhere('1 = 0'));a second test confirms the happy path still builds a real
orX()filter with predicates.
composer test:unit, 15149 tests) run forregressions — the 111 errors / 18 failures present are pre-existing
and unrelated to this change (Maps/Photos/Polls deep-link URL
format, SqlTypeMapper, SchemaLinkedTypes
InvalidArgumentExceptionassertions); none reference
MultiTenancyTrait,applyActiveOrgFilter, orWebhookMapper.