Repository navigation
fix(review): #4580 review findings, round 2 - #4696
Merged
Merged
Conversation
… read rules Thread: #4580 (comment) A condition on a related schema's property is a boolean oracle on its value, so the property read rule has to hold for a property a filter names, not only for one a response returns. New PropertyReadGuard asks the same question the facet path asks (PropertyRbacHandler::canReadProperty with an empty object, failing closed). - RelatedRowQueryApplier refuses a block whose condition fields (and, for _related, the join foreign key) the caller may not read; the message equals an unknown property so a withheld field cannot be told from a missing one. - Class sweep: the root property of a nested-path filter (<object>.<path>) on both the QueryBuilder and the raw UNION path now matches nothing when the caller cannot read the root. - Not covered here: the plain column filter path (named in the finding as pre-existing) and the outer property of a _referenced join. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…(multitenancy) filter Thread: #4580 (comment) The related schema's row predicate has no _organisation term, so on a schema without an authorization block rows of another organisation satisfied a related-row condition (forward: probe a foreign row by writing its uuid into an own object; inverse: foreign rows pointing at the caller's object). RelatedRowQueryApplier::apply() now takes the organisation scope for the related table and ANDs the boundary into the access predicate of every related-row subquery (IN / IN-or-NULL / NULL-only; SCOPE_NONE, an unknown mode or an empty uuid list match nothing). MagicSearchHandler decides that scope with the list path's own helpers (resolveMultitenancyFlag, multitenancyApplies, resolveOrganizationScope, cascade widening), made for the related schema with the request's flags, so subquery and list cannot disagree. Both _related and _referenced go through the one applier, so both are closed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d against the related schema Thread: #4580 (comment) A condition field only had to look like an identifier, so metadata columns of readable rows (_owner, _organisation, _authorization, _status) could be compared with gt/lt/in, and an unknown name reached the database as an unknown column (500 instead of a 400). - RelatedRowQueryApplier refuses, with InvalidArgumentException naming the field, every condition field (and, for _related, the join foreign key) that is not a property of the related schema. The message is the same one the property read rule uses, so a withheld field and a missing one look alike. - RelatedRowExistsClause::columnName now applies the full column-naming rule (snake_case, invalid characters to underscore, collapsed and trimmed underscores) instead of a one-line camel-to-snake conversion. The col_ prefix is deliberately not applied: the rule that creates the columns never applies it either. - The injection test now asserts the payload is neutralised by that rule; a leading-digit name remains refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ross-table UNION path Thread: #4580 (comment) buildWhereConditionsSql, the raw-SQL builder shared by UNION search, UNION count and UNION facets, skips underscore keys, so a multi-schema query carrying _related or _referenced answered the unfiltered set and its facet counts disagreed with the single-schema facet. It now refuses such a query with InvalidArgumentException (the type the single-table path throws), before building any SQL. Every caller of the builder (UNION search arms in MagicMapper, the count arms, both UNION facet branches in MagicFacetHandler) goes through that one entry point, so all of them are covered; honouring the keys in raw SQL is left for a follow-up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… not when shares change or an alert fires Thread: #4580 (comment) The rule "every recipient must be able to read this view" held at one instant on one path. - Delivery (b): ViewAlertCrossedListener re-checks every resolved recipient against the view as it is now (administrator, owner, shares, isPublic; the same ViewShareResolver::accessFor the save uses). A recipient who may no longer read it, or whose groups cannot be read, is dropped and logged. Both channels (nc-notification, email) and both senders of the crossing event (the sweep job is the only dispatcher) go through that one listener. - Save (a): ViewerReachResolver::assertMayNameRecipients also judges the STORED recipients: one who read the view as stored but would not read it as saved is refused (403, listed), so un-publishing or removing a share cannot leave a recipient on an alert about a view they can no longer open. - Judged view (c): on PUT the controller lays the isPublic the save will write (false when the body omits it) over the judged view, so the judged view equals the saved view. PATCH keeps the stored value, as it saves it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-clock UTC Thread: #4580 (comment) sinceParam returned new DateTime(trim($raw)) and the mapper binds it with PARAM_DATETIME_MUTABLE, which formats the object's own wall clock without converting, so '2026-10-19T12:00:00+02:00' was compared as 12:00:00 instead of 10:00:00Z on GET /api/flow-runs?since= and /api/flow-runs/summary. The parsed moment is converted to UTC before it is bound. Sweep: the summary's default window (-7 days) is normalised the same way, so both bound values reach the mapper in UTC. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a table cannot be read Thread: #4580 (comment) findFolderIdsByOrganisation swallowed every per-table exception and returned the folders of the tables it could read, so OrganisationUsageService showed a too-low number as a measured figure, against its own rule that a figure that cannot be measured is null. Each failing table is now logged (warning, with table, organisation and error), the scan continues so every unreadable table is named, and the method then throws a RuntimeException listing them. measureStorage already turns an exception into null, so the answer is null instead of an undercount. Not changed: the scan still visits every magic table per GET /api/organisations/{uuid}; caching or bounding it is left for a decision on the per-request cost. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing newline Thread: #4580 (comment) SEGMENT was /^[A-Za-z0-9_\-]+$/, and `$` also matches before a final newline, so `a\n` passed and was written into the JSON path literal, which MariaDB rejects (500 instead of the intended 1 = 0 refusal). It now ends in \z. Class sweep: the same `$`-anchored identifier check in RelatedRowExistsClause::quoteIdentifier and in MagicRbacHandler::buildRbacPredicateForAlias (alias guard) now ends in \z too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Thread: #4580 (comment) line() capped the count at 50 but stored each message as given, so a node writing one large string per line grew the run log by the string size times 50 per step. A message longer than FlowStepReport::MAX_MESSAGE_LENGTH (500 characters, counted in characters not bytes) is now cut, and the result, marker included, ends in FlowStepReport::TRUNCATION_MARKER. A message at the cap is untouched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and assignable Thread: #4580 (comment) Hidden/not-assignable was set only when the tag was created; findOrCreateTag returned an existing tag untouched, so tags created before the default, or first created by a user in the Files tag UI under the same name, stayed in every user's tag list. findOrCreateTag now repairs a found object: tag that is still visible or assignable with updateTag(userVisible: false, userAssignable: false), keeping its name and colour; this covers both lookup paths (the first search and the TagAlreadyExists retry). A refused update is logged and the tag is still used, so the file link is not lost. Tags that are never attached again are not reached; hiding those needs an upgrade step, which is left to the author. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fixes for rel-f1, rel-f2, rel-f4 and rel-f7 pushed four classes over a PHPMD threshold (coupling between objects, class length, method length, constructor parameter count, a boolean flag argument, a missing import). No behaviour change: - RelatedRowAccess takes over the property-read assertion and the organisation predicate from RelatedRowQueryApplier; the applier exposes mayReadProperty() so MagicSearchHandler no longer needs its own guard parameter. - Engine detection moves from the applier to RelatedRowExistsClause::engineFor. - ViewAlertCrossedListener re-checks recipients through ViewerReachResolver::recipientReads (now public), the rule the save already uses, instead of its own copy. - ViewsController::refuseForbiddenViewFields takes the saved body instead of a boolean flag. - phpmd.baseline.xml gains one CouplingBetweenObjects entry for RelatedRowQueryApplier, the established way this repo records a class that legitimately coordinates this many collaborators. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… RelatedRowQueryApplier The applier sat at the coupling limit only because it called the RBAC handler itself. RelatedRowAccess now owns the buildRbacPredicateForAlias call together with the organisation term (one predicate per related alias), so the applier no longer takes or references MagicRbacHandler and the CouplingBetweenObjects baseline entry added earlier is removed. phpmd passes on a clean export with the baseline as it is on the base commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ted/_referenced takes the sequential path The refusal added in buildWhereConditionsSql is an uncaught InvalidArgumentException on the controller routes (crossTableSearch), so a cross-table search carrying _related or _referenced ended in HTTP 500. MagicMapper::shouldUseUnionQuery now returns false when either key is present. Both the search (dispatchCrossTableSearch) and the count (countAcrossMultipleTables) decide through it, so the sequential path, which builds the EXISTS subqueries, answers both and total and rows agree. The refusal in buildWhereConditionsSql stays as a defensive guard for a caller that bypasses the routing. Not routed: the UNION facet branches (getSimpleFacetsUnion) have no sequential equivalent and still refuse. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e same 64-character cap as the read path RelatedRowExistsClause::columnName applied every step of the column-naming rule except the 64-character cap MagicSearchHandler::sanitizeColumnName applies (MagicMapper's own copy has none; no shared helper exists), so a property name over 64 characters was filtered on a different column than the read path uses. The cap is applied in the same order (trim underscores, then cut) and pinned in ReferencedRowFilterTest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d beyond the default flags Pins the scope the related-row subquery gets for _multitenancy=false (and its string and unreadable spellings), _rbac=false with an RBAC bypass available, an anonymous caller (the empty scope) and a public schema with and without an explicit multitenancy request. Test-only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…th _related/_referenced answers 400, not 500
Before this round those facets silently ignored the filter and returned wrong
counts; a clear 400 is the honest answer.
- New RelatedRowFilterNotSupportedException (an InvalidArgumentException) is
thrown by MagicSearchHandler::refuseRelatedRowFilters, now public.
- MagicFacetHandler::getSimpleFacetsUnion calls it first. The per-facet UNION
queries have their own catch blocks that return empty buckets, so the
refusal had to come before them or it would have been swallowed.
- New RelatedRowFilterMiddleware (registered in Application) answers the
exception with HTTP 400 and {"error": <message>}; every other exception is
rethrown. A middleware covers every controller that reaches the facets:
facets are only reached through getSimpleFacets (MagicMapper), which is
called by FacetHandler::getFacetsForObjects (list route with _facets,
ViewPresentationService, ObjectMetricSource); there is no facets-only
controller route. The UNION search builder keeps raising the same
exception as a defensive guard.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ✅ | ||||
| test-l10n-parity | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| composer | ✅ | ✅ 173/173 | |||
| npm | ✅ | ✅ 648/648 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ✅ | ||||
| lockfile sync | ✅ | ||||
| PHPUnit | ✅ 26637/26637 · 25 skipped | ||||
| Newman | ✅ 714/714 | ||||
| Playwright | ⏭️ deferred: E2E runs locally and on the promotion path only. This pull request targets development, so the suite is asked once per promotion into beta and main rather than once per push per open pull request. Run it locally with npx playwright test, or from the Actions tab on a branch with no open pull request into development. |
||||
| Hydra gates | ✅ |
Quality workflow — 2026-10-19 20:34 UTC
Download the full PDF report from the workflow artifacts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… filtered properties MagicSearchHandlerOrNullModifierTest (from #4697) built the handler with a bare RelatedRowQueryApplier mock, whose mayReadProperty() answers false. Since rel-f1 a nested-path filter whose root property the caller cannot read matches nothing (1 = 0), so testNestedPathAdmitsNullOnBothPaths saw the refusal instead of the orNull predicate. The mock now grants read access, as MagicSearchHandlerNestedPathFilterTest does. Test-only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ✅ | ||||
| test-l10n-parity | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| composer | ✅ | ✅ 173/173 | |||
| npm | ✅ | ✅ 648/648 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ✅ | ||||
| lockfile sync | ✅ | ||||
| PHPUnit | ✅ 26671/26671 · 25 skipped | ||||
| Newman | ✅ 714/714 | ||||
| Playwright | ⏭️ deferred: E2E runs locally and on the promotion path only. This pull request targets development, so the suite is asked once per promotion into beta and main rather than once per push per open pull request. Run it locally with npx playwright test, or from the Actions tab on a branch with no open pull request into development. |
||||
| Hydra gates | ✅ |
Quality workflow — 2026-10-19 21:05 UTC
Download the full PDF report from the workflow artifacts.
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.
Fixes for the round-2 review of #4580 (#4580 (review)). Merge into
development, then re-request the review; nothing here is merged by the reviewer.Findings → commits
98b1701618PropertyRbacHandler::canReadPropertywith an empty object, failing closed: the question the facet path asks), with the same message as an unknown property. A nested-path filter whose root the caller cannot read matches nothing.996b7a05cd,d5e39b1d1c,58bbec05a4d5e39b1d1cmoves the RBAC predicate intoRelatedRowAccessso no new phpmd baseline entry is needed;58bbec05a4pins the scope for_multitenancy=false,_rbac=false, an anonymous caller and a public schema.8b8683ad89,62f03f1b20,a1c270e92eRelatedRowFilterMiddleware, instead of empty or wrong buckets.68295b131fViewShareResolver::accessForthe save uses.sincewith a UTC offset is read as wall-clock UTCc2af88358cGET /api/flow-runs?since=and/api/flow-runs/summary.7f3bf8c7d1null(not measured) instead of a too-low number.4909fa15b4,13b0f0415dInvalidArgumentExceptionnaming the field, instead of reaching the database as an unknown column.13b0f0415dapplies the read path's 64-character column-name cap.1f3566bc24\z; the same$-anchored identifier checks in the related-row clause are fixed too.35944cfd5ae868c0b1e2findOrCreateTagrepairs a foundobject:tag that is still visible or assignable.87b2716631271c6938a0,c21d7fc12edevelopmentmerged in (#4697, #4698, #4699). The new orNull test from #4697 mocked the property read check as "no access", so with rel-f1 its nested-path filter matched nothing; the mock now grants read access. Test-only.Left to the author
?property=value, named in the finding as pre-existing) and the outer property of a_referencedjoin are not changed here.GET /api/organisations/{uuid}still scans every magic table on each request. Caching or bounding that scan needs a decision on the cost.findOrCreateTagfinds a tag. Tags that are never attached again keep their old flags; hiding those needs an upgrade step.Checks run on this branch
d5e39b1d1ctoa1c270e92e, then re-checkedcomposer phpcs→ exit 0;composer phpstan→ no errorsgit archiveexport with the baseline unchanged → exit 0phpunit-unit-local.xml) on the touched test folders → 9770 tests; the 41 errors and 7 failures are the same set as on the base commit (nopdo_sqlite, no Nextcloud server here)run-hydra-gates.sh --scope-to-diff --base origin/development→ exit 0, 45 of 45 applicable gates ranRelatedRowPrefixedDatabaseTest(needs SQLite), and a real HTTP request through the facet route (the 400 is pinned at the middleware)🤖 Generated with Claude Code