Release: merge development into beta#1711
Open
github-actions[bot] wants to merge 2593 commits into
Open
Conversation
…ta190 # Conflicts: # package-lock.json
…cked change proposal(s) Derived from the 2026-07-12 Specter deep-research on scholiq (competitors, user wishes, flows, standards, Nextcloud ecosystem; evidence persisted in the intelligence DB, tag gap-2026-07-12). Changes: notification-delivery-windows
…(scholiq gap-wave 2026-07-12)' (#352) from specs/scholiq-gap-wave-20260712 into development
…der, McpTool attribute (ADR-063)
…, derived provider, McpTool attribute (ADR-063)' (#353) from wip/mcp-platform-abstraction-spec into development
…s, critical bypass Implements openspec change notification-delivery-windows: QueuedNotification table + mapper + migration, dispatcher gate that queues (never drops) suppressed non-critical notifications, per-rule fixed-time digest schedules, critical:true bypass, IANA timezone evaluation with server fallback, NotificationQueueFlushJob; removes dead BatchNotificationJob/NotificationDigest. Tests 14118->14180, 0 new errors/failures; hydra gates pass for touched code.
…able digest queue (wip/notification-delivery-windows)' (#354) from wip/notification-delivery-windows into development
Adds the declarative x-openregister-mcp dialect: registers the key in Schema::ANNOTATION_VOCABULARY so it folds into schema configuration on import, adds McpAnnotationValidator (shape/type checks for enabled, the fixed five-verb CRUD template, per-verb description/scope/hints, and search-only filters cross-checked against real properties), and wires validateMcpAnnotation() into SchemaMapper::cleanObject() after validateHandoffAnnotation(). Default OFF, opt-in per schema; emits no MCP tool and changes no serving surface (that lands in the follow-up or-mcp-derived-tool-provider change). Documents the dialect in docs/features/ai-and-mcp.md and the changelog.
Moves the completed change to openspec/changes/archive/2026-07-12-or-mcp-schema-dialect/ and syncs its spec delta (REQ-DIALECT-001/002/003) into openspec/specs/ai-mcp/spec.md, marking or-mcp-schema-dialect shipped in the ADR-063 in-flight-changes list while keeping or-mcp-derived-tool-provider and or-mcp-tool-attribute noted as in-progress.
… validator (ADR-063 chain 1/3)' (#355) from wip/or-mcp-schema-dialect into development
…th serving surfaces (ADR-063 chain 2/3)
…DR-063 chain 2/3)' (#360) from wip/or-mcp-derived-tool-provider into development
- AnnotationNotifier: setIcon() only accepts absolute http(s) URLs since
NC requires them for desktop/mobile clients; the relative imagePath()
fallback threw InvalidValueException (extends InvalidArgumentException)
on every render of every openregister notification without an originApp
route — ~43 notifications x every poll = ~10,450 deprecation warnings
per 2h, and those notifications never reached any client. Wrap in
getAbsoluteURL().
- McpToolsService discovery: probing OCA\{App}\Mcp\{App}ToolProvider for
every enabled app logged a warning per app per MCP request (~1,600/2h).
Absence is the expected case: class-miss and alias NotFoundException
drop to debug, successful discovery drops to info; genuine resolve
failures stay warning.
- PropertyValidatorHandler / AggregationAnnotationValidator: union types
and array metrics hit string interpolation/casts, emitting PHP 'Array
to string conversion' warnings (~140/2h). Arrays now render as JSON in
the validation message / fail the metric check cleanly.
…s (ADR-063 chain 3/3)
Net-new PHP attribute marks a public service method for MCP exposure with
inputSchema/outputSchema inferred from type hints + docblocks. A reflection
scanner discovers attributed methods on an app's declared scannable service
classes (opt-in via IMcpScannableServices::<appId>) and registers one tool
per method, id {appId}.{toolName}, into the same catalog and both existing
serving surfaces as built-in and schema-derived tools — no new serving code.
Invocation resolves the owning app's already-DI-resolved service instance
and calls the method in-process (ADR-041, no cross-app RPC); the method
alone owns authorization/IDOR. Every invocation is audited identically to
the derived provider (one hash-chained record, params digest not raw args).
Collision policy: hand-written wins silently over attributed; attributed
colliding with a schema-derived id is rejected at discovery with a logged
error.
ADR-063 chain 3/3 shipped — chain now COMPLETE (dialect + derived provider + attribute). Deltas synced into ai-mcp and mcp-discovery capability specs.
Adds GET /api/v2/audit and /api/v2/audit/export so admins can query and export audit-entry objects (e.g. procest's aiAuditEntry) across every app/register/schema from one place, instead of each app building its own audit UI. AuditQueryService resolves candidate register/schema pairs (explicit filters, or a schema-agnostic "looks like audit" naming fallback when none are given) and searches them via ObjectService, clamping limit to 1-200. AuditQueryController gates both actions admin-only and streams CSV or JSON exports.
…ge paths @SPEC openspec/changes/{name}/... goes stale the moment a change is archived — hydra-gate-spec-anchor-existence (gate-46) correctly flagged both the new or-mcp-tool-attribute references and a pre-existing or-mcp-derived-tool-provider reference in Application.php now that the file is touched. Point all of them at openspec/specs/ai-mcp/spec.md, the living spec both chains' requirements were synced into.
…ner (ADR-063 chain 3/3)' (#363) from wip/or-mcp-tool-attribute into development
- MagicTableHandler::handleExistingTable compared required columns (keyed by camelCase property name) against live columns (keyed by snake_case physical name) with array_diff_key, so any schema with a multi-word property (allowedApps -> allowed_apps) was flagged as missing columns on every request and force-synced forever without converging (~6,600 warnings/2h). New MagicMapper::findMissingColumns() resolves the physical name first — the same resolution addMissingColumns() already used. - ImportHandler Pass 2: schemas skipped by Pass 1 as up-to-date are never in schemasMap; the per-schema map-miss log is the expected case on every boot of an unchanged config (~4,800/2h) — downgraded to debug.
RBAC fails closed for anonymous principals (#1955) — correct for requests, but Nextcloud boots apps BEFORE session resolution and webcron never has a user, so every app-initiated maintenance write (importing the app's own shipped register config, repair-step migrations, listener writes on webcron-created objects) is denied as 'Anonymous' on every boot. The existing trust only covered PHP_SAPI === 'cli'. - SystemOperationContext::run(callable): scoped, code-only elevation with depth counting; released in finally so exceptions cannot leak it. - MultiTenancyTrait::hasRbacPermission and PermissionHandler honour the scope exactly where they already trust CLI. - ConfigurationService::importFromApp runs inside the scope: fixes docudesk/petstore-style 'Access denied: update register entities' on every boot (~120 errors/2h). - ObjectService::runAsSystem(callable): public API for consuming apps' repair steps and event listeners (openbuild, docudesk, pipelinq, softwarecatalog adoption follows per app). - Unit tests for scope semantics (active/released/exception/nesting).
…(notifier icon, MCP probe, array-to-string)' (#361) from fix/log-noise-phase1 into development
…ss 2 map-miss noise (log-cleanup phase 2)' (#364) from fix/log-noise-phase2 into development
…user-less app maintenance (log-cleanup phase 4 platform)' (#365) from fix/system-operation-context into development
hasRbacPermission() has two anonymous guards: an early getCurrentUserId()===null check and a later userSession->getUser()===null check. #365 added the SystemOperationContext::isActive() trust only to the later block, but the early guard returns false first — so entity-level (register/schema) writes from a scoped system operation without a session (docudesk config import, webcron repair steps) were still denied as Anonymous. Add the same trust to the early guard, mirroring its CLI branch, so it short-circuits before the deny. Object-level PermissionHandler was already correct (checks isActive at the top).
…l spec Archives the implemented+merged change to archive/2026-07-13-* and merges its delta into the notificatie-engine canonical spec (+1 added, ~3 modified). 184 canonical specs validate --strict.
…o canonical specs Deterministic, comment-only repointer retargets @SPEC docblock anchors that pointed at archived change dirs (openspec/changes/<slug>/tasks.md#task-N) to their canonical openspec/specs/<cap>/spec.md[#requirement-<slug>] home. - 3,643 anchors repointed across 695 files (896 anchor-level, 2,747 file-level) - comment-only: 0 non-@SPEC changed lines / 7,030; every file ins==del (1:1) - gate-46 re-verify: OR broken 5,051 -> 1,408 - 1,408 residual-dangling anchors filed for human triage - tool + unit test committed under the change dir
…from wip/spec-anchor-repair into development
…aths
OR resolved `writeOnly` from top-level schema properties only. A secret
nested inside an untyped `object` property was therefore impossible to
protect: `writeOnly: true` is a JSON Schema keyword and can only attach to
a property the schema declares, and blanket-`writeOnly` on the parent
breaks the editors that legitimately read the rest of it back. Live
instances (openconnector#235, the openconnector#147 residual):
- source.configuration.authentication.{client_secret,username,password}
- rule.configuration.authentication.keys (apiKey -> userId impersonation map)
Both were returned in cleartext on every read, for everyone, with no way
to declare otherwise.
Declaration: a schema-level annotation listing dot-paths, e.g.
`x-openregister-writeonly-paths: ["configuration.authentication.keys"]`.
It composes with the existing x-openregister-* machinery and reuses the
dot-path vocabulary the relations mirror is already keyed by. A declared
path strips the value AND its whole sub-tree, which is what covers
`...authentication.keys` whose leaf segments are caller-supplied apiKeys
and cannot be enumerated in advance.
Enforcement rides the SAME hard render boundary as top-level writeOnly —
unconditional, admin included, NOT _rbac-gated (#389: an admin HTTP GET
renders with _rbac: false). Applied at every site that strips top-level
writeOnly, all of which funnel through
PropertyRbacHandler::stripWriteOnlyProperties():
- RenderObject::renderEntity() body strip + @self.relations mirror
- RenderObject::redactWriteOnlyFromRows() cheap list path (both row shapes)
- PropertyRbacHandler::filterReadableProperties() (property-authz path)
Gates now ask RenderObject::schemaHasWriteOnlyRule() so a caller cannot
check one spelling and forget the other. `_render: false` still bypasses
rendering entirely and returns the raw value (the credential migration and
CallService depend on it).
@self.relations: SaveObject::scanForRelations() flattens nested values into
LITERAL dot-path keys, so a nested secret is mirrored there under its full
path — the nested equivalent of the #429 gap. Stripped by exact key and by
`<path>.` prefix.
Validation fails loudly. This is the only configuration key exempt from
#419's per-key isolation: for every other key "drop the bad key, keep the
rest" degrades safely (the feature just doesn't fire), but for this one it
is fail-OPEN — the schema would save, look annotated to a reviewer, and
serve the secret. A malformed path, or one rooted at an undeclared property
(`configuratio.authentication.keys`), aborts the whole save.
Also fixes two pre-existing defects found on the way:
- testSystemContextReadIsNotRedacted asserted the PRE-#389 contract and
had been RED at HEAD, documenting a leak as intended behaviour.
- the canonical spec still said `_rbac: false` MUST return the secret,
which is what the stale test was pinned to. Split the bypass
requirement: property authorization.read bypasses, writeOnly never does.
Tests: 20 new (12 Schema validation, 8 render). Mutation-tested — removing
the nested strip fails 5 tests showing the plaintext across body, mirror,
_rbac:false, fields re-widen and cheap path. Zero new failures vs pristine
HEAD across tests/Unit/Db + tests/Unit/Service/Object (13 pre-existing,
1 fixed). phpcs unchanged vs HEAD (0 errors, 41 warnings).
Consumers: openconnector#235, openconnector#147. Neither is closed —
openconnector must still ADOPT the annotation for its rule/source schemas.
Refs openconnector#235, openconnector#147, openregister#389, openregister#429
…only-paths' (#459) from wip/nested-writeonly-paths into development
#460) An admin LIST/search read returned every `writeOnly` secret in cleartext. #389 established that writeOnly is a hard render-boundary rule that strips unconditionally, admins included, and hardened `RenderObject::renderEntity()`. Its sibling `redactWriteOnlyFromRows()` — the cheap list/search path — kept the pre-#389 bypass: if ($_rbac === false || SystemOperationContext::isActive() === true ...) return; Its comment claimed "same bypass as renderEntity's read-strip", which had been stale since #389 removed exactly that bypass. `ObjectsController` derives `$rbac = ($isAdmin === false)`, so an ADMIN list arrived here with `_rbac: false` and hit the early-return — the disclosure #389 was filed to close, still open on the list path. `_rbac: false` on an HTTP read means "this caller bypasses WHICH OBJECTS it may see", never "this caller may see secrets". Conflating those was the bug. Fix: mirror renderEntity's split. writeOnly (top-level and nested `x-openregister-writeonly-paths`, #459) strips unconditionally via the shared `schemaHasWriteOnlyRule()`/`stripWriteOnlyProperties()` choke point, including the `@self.relations` mirror (#429). Property `authorization.read` stripping and field decryption REMAIN `_rbac`/SystemOperationContext-gated — unchanged. Consumer investigation (the ocon#215/#226 regression risk): no consumer depends on the bypass. `redactWriteOnlyFromRows()` is reachable ONLY via `searchObjectsPaginated`; every internal caller (GraphQLResolver, ObjectsProvider, ContextRetrievalHandler, SearchController, SettingsController) passes `_rbac: true` or the default. The only `_rbac: false` callers are the three ObjectsController list sites, which serialize straight to a JSONResponse. The engines the comment named batch-read via `findAll()`/`find()`, which never enter this method; the sanctioned raw read stays `_render: false`. Nothing to migrate. Tests: the defect hid because tests exercised the helper (with `_rbac: true`, the one value that did strip) or mocked RenderObject out of the controller entirely. Adds ObjectsControllerWriteOnlyListLeakTest — the REAL controller list path with a REAL RenderObject, asserting on the serialized admin response (top-level, nested, and the relations mirror). Mutation-verified: restoring the early-return fails 6 tests, printing `"apiKey":"SECRET_APIKEY_MUST_NOT_LEAK"` in an admin list response. Inverts testCheapPathRowRedactionSkipsSystemContext, which pinned the leak as intended behaviour, and pins that authorization.read gating is untouched. Suites: 4682 tests (was 4675, +7); the 13 reds are byte-identical to an origin/development baseline — all pre-existing, none new. Refs #389, #429, #459. Closes #460.
Fixes three tool defects found by fleet-wide use: CRLF normalisation, raw fragment emission (produced anchors gate-46 still rejects), and the missing self-heal path for already-canonical targets. Adds regression tests for each. gate-46: 1411 -> 1408. Comment-only: every changed lib/src line is an @SPEC tag.
…y fleet-wide use (gate-46 1411 to 1408)' (#461) from wip/spec-anchor-repair-round2 into development
`writeOnly` stripped secrets on every read but nothing restored them on save, so the natural client round-trip destroyed the stored value: GET returns the object with the secret stripped (correct), the client edits one field and PUTs the body back without the secret it was never shown, and the PUT-semantic null-fill in prepareObjectForUpdate() nulls it. This is live, not theoretical. nc-vue's generic CnFormDialog spreads the loaded form data into its submit body and has no writeOnly awareness at all, so editing ANY field of an OpenConnector Source wiped its credentials (openconnector#245). It also blocked openconnector#147, whose remaining fix is to declare rule.configuration.authentication.keys write-only. Adds the save-side half of the contract, symmetric with the read-side strip and living next to it in PropertyRbacHandler: - collectOmittedWriteOnlyPaths() reports which declared locations an incoming update payload omits. A top-level `writeOnly: true` property is treated as a single-segment dot-path, so both declaration surfaces (#459) collapse into one rule and cannot drift apart. - restoreWriteOnlyValues() carries the stored values forward for exactly those locations, writing the LEAF into the incoming sub-tree rather than restoring the whole parent — so preserving configuration.authentication.client_secret does not revert the operator's edit to configuration.endpoint. absent vs explicit null: only an ABSENT location is preserved; an explicit null clears. Conflating them would make clearing a secret impossible — settable and rotatable but never removable. This also matches PATCH, which already merges the payload over the stored object. Both verbs agree: omit to keep, send null to clear. Ordering in prepareObjectForUpdate() is load-bearing in both directions: - detection runs on the RAW payload, before setDefaultValues() can materialize an omitted key and mask the omission; - restoration runs AFTER prepareObjectData() (that is where encryptProperties() runs, and the stored value of an encrypted property is ciphertext — restoring earlier would double-encrypt it) and BEFORE fillMissingSchemaPropertiesWithNull() (which materializes every absent property as null, erasing the distinction). The stored value is read from the raw ObjectEntity::getObject() snapshot already captured in the method. A rendered read would be useless by construction, and `_rbac: false` is NOT sufficient — the writeOnly strip is schema-gated and deliberately ignores _rbac (#389, #460), so an _rbac: false read still comes back stripped. Scope: covers the single-object path, which also serves saveObjectsStreaming() since it delegates per row. The bulk path (SaveObjects::saveObjects() -> ultraFastBulkSave upsert-by-UUID, which never reads the existing row) has the same class of exposure by a different mechanism and is documented in the spec as a tracked gap — it needs a chunk-level batched fetch and is a narrower vector, since bulk payloads come from import files rather than a stripped GET. Nothing on the read boundary is weakened: it stays unconditional (#389/#460) and authorization.read stays _rbac-gated. Secrets are never logged. Tests: 22 new (17 handler contract + 5 wiring through the real update path), including the round-trip that IS the bug — the real strip generates the payload that is fed back as the update, so strip and preserve cannot drift apart. Mutation-tested: removing the restore call fails the round-trip with the secret nulled, while the overwrite/explicit-null/ordinary-field tests still pass. Closes #463 Refs #459, #389, #460, openconnector#245, openconnector#147
…omitted Only call restoreWriteOnlyValues() when there is actually something to restore, mirroring how the read path gates its strip on RenderObject::schemaHasWriteOnlyRule(). Caught by a real regression, not by inspection: ObjectHandlers\SaveObjectTest:: testSaveObjectWithExistingUuidUpdatesObject went red because it mocks PropertyRbacHandler, so the unconditional restoreWriteOnlyValues() call returned PHPUnit's default for an `array` return type — an empty array — which wiped $preparedData and emptied the saved object. The sibling getUnauthorizedProperties() call never had this problem because it sits behind a hasPropertyAuthorization() gate that the mock answers false. Patching the test would have hidden a genuine fragility. The overwhelming majority of schemas declare no write-only location, and for those this rule should be provably inert rather than merely harmless — so the fix is the short-circuit, which also keeps the update hot path doing zero work for them. Re-verified: mutation test still fails with the secret nulled when the restore is disabled, so the guard does not weaken the contract.
…ent with or#463 write-only preserve Reunites the two diverged `development` lineages. GitHub carried one stranded commit (2de1328, wave-12 engine-primitive enforcement, ~7 weeks orphaned by the Codeberg migration); Codeberg carried the 1510 commits of subsequent work, including or#463/PR #468 (save-side write-only preserve). Both sides are security. Neither was dropped. Kept from wave-12: * PermissionHandler default-closed policy (`enforce_default_closed`, opt-in, BC default false) + per-object `_authorization` override (Fix 5). * ValidateObject::validateReadOnlyConstraints() read-only write-path enforcement, wired from ObjectService. * SaveObjects::applyBulkSafeguards() per-row bulk safeguards. * ConditionMatcher dot-syntax. * SaveObject::callerIsAdmin(). Kept from or#463: * SaveObject's save-side write-only preserve (collectOmittedWriteOnlyPaths on the raw payload, restoreWriteOnlyValues after prepareObjectData and before the PUT-semantic null-fill). Byte-for-byte unchanged. Semantic resolutions: * PermissionHandler::evaluatePermission — combined BOTH: the fail-closed try/catch on AuthorizationUnresolvableException AND wave-12's `object:` argument for the per-object override. * PermissionHandler unlisted-action branch — kept this lineage's unconditional deny, which strictly subsumes wave-12's narrower flag-gated deny. * PermissionHandler constructor — wave-12 added IAppConfig as an optional trailing arg while this lineage already had it as a REQUIRED arg; the naive merge produced a duplicate parameter (fatal). Kept the required one and dropped wave-12's now-unreachable null guard. * SaveObject::setSelfMetadata — kept this lineage's admin-OR-verified-member organisation gate (wave-12's admin-only gate is a subset; membership is verified, so SB1 stays closed) and routed its admin arm through wave-12's callerIsAdmin() so the helper is not orphaned. Retained the @self.folder access-control block. * SaveObjects constructor — kept this lineage's parameter order (a superset adding eventDispatcher + auditTrailMapper); wave-12 fixtures rebound by name. * ValidateObject — union: this lineage's memoized validator/prepared-schema caches plus wave-12's read-only methods. * appinfo/info.xml — version bumped to 0.2.17-unstable.15 (above both parents); kept licence EUPL-1.2 (GitHub's `agpl` predates the relicence). Test fixtures adapted (no enforcement weakened): * Wave12BulkSafeguardsTest / Wave12PermissionHandlerDefaultClosedTest / SaveObjectTest admin case — wave-12's positional constructor lists would now bind to the wrong parameters; rebound with named arguments. * PermissionHandlerRbacTest — its IAppConfig stub returned true for EVERY key, which silently switched wave-12's opt-in default-closed policy on and broke two default-open assertions. Made the stub key-aware (production reads a real per-key value defaulting to false). Verification: wave-12 suites (Bulk 7, DefaultClosed 10, ReadOnly 9, DotSyntax 12) and or#463 suites (SaveObjectWriteOnlyPreserve 5, PropertyRbacHandlerWriteOnly- Preserve 17) all pass. tests/Unit/Service/Object + tests/Unit/Db fail exactly the same 13 tests, by name, as pristine origin/development — zero new failures. phpcs: 0 errors on all changed lib/ files.
Reconcile diverged development: land wave-12 engine-primitive enforcement alongside or#463 write-only preserve
…side prerequisite Adds a thin wire so ObjectService::searchObjectsPaginated() can opt into matching on document body text via ChunkMapper::searchByKeyword() — the storage layer (openregister_chunks + PostgreSQL tsvector GIN) already ships from the merged hybrid-document-search change; only missing piece is the query-path exposure. Spec-proposal only — no runtime code in this commit: - proposal.md — why + what changes + cross-refs (kind: code, no depends) - design.md — 5 decisions with alternatives + risks + rollback - tasks.md — 7-item checklist through impl + tests + archive - specs/zoeken-filteren/spec.md — ADDED ZKN-CONTENT-001 / -002 / -003 `openspec validate expose-content-search-in-object-service` → is valid; `openspec status` → 4/4 artifacts complete (proposal + design + tasks + specs; applyRequires satisfied). Downstream consumer (HARD-blocked on this): opencatalogi #136 add-document-content-search (WOO-517). Its proposal frontmatter carries `depends_on: openregister:expose-content-search-in-object-service`; the slug matches this change so Hydra's dep-block will lift once this PR merges to development. Refs WOO-517 (OR-side prerequisite subtask). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…-service (WOO-517 OR-side, tracking issue #472) Written by the plan-to-issues flow. Contains: - tracking_issue: 472 (Codeberg openregister #472) - base_branch: development - feature_branch: feature/472/expose-content-search-in-object-service - 7 parsed tasks from tasks.md with spec-refs + acceptance criteria Hydra label wilco-ready-to-build attached to the tracking issue. Downstream opencatalogi #139 (WOO-517) is HARD-blocked on this change merging — Hydra's depends_on gate on that change lifts once this PR merges to development. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… (WOO-517 OR-side prerequisite)' (#471) from feat/expose-content-search-in-object-service into development Reviewed-on: https://codeberg.org/Conduction/openregister/pulls/471
…d register/schema enforceReadOnlyOnUpdate() loaded the prior record with a bare $this->objectMapper->find($uuid, ...), leaving find() to rely on the request's URL scope. Under a stale scope that path falls back to the deliberate cross-table search (openregister#1520). We are on the save path here with $this->currentRegister and $this->currentSchema already resolved, so pass them and take the scoped register/schema-table lookup directly — no reason to risk the fallback on a write-path method that runs on every update. Defensive hardening, not a hot N+1: in the common save flow the URL scope matches and the bare find() already hits the scoped path. This removes the dependency on that being true. Behaviour is unchanged — Wave12ReadOnlyEnforcementTest (9 cases) still passes; find() is mocked, so the arguments do not alter the tested contract. Split out of the now-closed #470, whose only real delta over the #2020 readOnly port was exactly this call. See #2015.
perf(objectservice): scope the readOnly guard's find() to the resolved register/schema
…get() getUses()/getUsedBy() resolved RegisterMapper and MagicMapper through the service locator (\OC::$server->get(...)) at call time. Under unit test \OC::$server is not a real container, so get() returned null and the next $registerMapper->findAll() threw 'Call to a member function findAll() on null' — four RelationHandlerTest cases errored, and the code was untestable by construction (the test authors left a comment acknowledging the workaround). MagicMapper was already constructor-injected as $this->objectEntityMapper, so only RegisterMapper needed adding. Both service-location call sites now use the injected instances. No behavioural change in production — the container returns the same singletons — but the dependency is now explicit and mockable. RelationHandlerTest: 71 pass / 4 error → 75 pass / 0 error. Full tests/Unit/Service/Object/ drops from 4 errors to 0 (the 2 remaining PermissionHandlerCustomScope failures are unrelated, tracked in #2023). No manual construction of RelationHandler exists (pure DI, auto-wired by type), so the new parameter resolves automatically. Part of #2023.
fix(relationhandler): inject RegisterMapper instead of \OC::$server->get()
…henticated grant The custom-scope listener dispatch (CustomScopeEvaluatingEvent — how a consuming app votes allow/deny on a custom verb it owns, e.g. ZGW besluit_nemen) sat AFTER the 'authenticated' pseudo-group grant in evaluatePermission(). For a schema with no authorization block that grant is default-open, so it returned true and short-circuited before any listener was consulted. Net effect: on exactly the unconfigured-schema case the mechanism exists to serve — a schema that declares no rules for its custom verb — the veto was DEAD. A listener voting deny was silently ignored; the action was granted to any authenticated user. This is the orphaned-capability shape: dispatched, wired, covered by a test, and never actually reached on the path that mattered. PermissionHandlerCustomScopeTest proved it: both listener-voting tests failed with zero events dispatched — the dispatch was never reached. Fix: move the custom-scope dispatch above the 'authenticated' grant. Ordering is now: admin bypass → custom-verb listener vote → authenticated/default-open → delegation → per-group → public-inherit. Admin bypass still precedes the veto (admins remain un-vetoable, matching the canonical-action short-circuit). SEMANTIC CHANGE — flagged for review: a listener verdict now takes precedence over BOTH the default-open grant AND an explicit 'authenticated' grant, for custom (non-canonical) verbs only, and only when a listener actively votes. This is the intended 'the app that owns the verb has final say' contract. When no listener votes, dispatch returns null and evaluation falls through unchanged — so behaviour is identical for every schema that has no custom-scope listener. Verified: PermissionHandlerCustomScopeTest 3/5→5/5; all 114 PermissionHandler tests green; full tests/Unit/Service/Object/ 2 failures→0. Closes the cluster-B half of #2023. Not live-verified against a running instance (shared dev off-limits).
fix(authz): consult the custom-scope veto before the default-open authenticated grant
…elopment Uncurated preservation snapshot. The 8080 openregister checkout was on wip/public-audit-query-endpoint (241 commits behind origin/development) with 40 modified + 25 untracked files in the working tree — a mix of work already landed on development in a different form (stale copies) and possible genuine local WIP (scheduled-reports, notification delivery windows, MCP attribute tooling, McpProviderBridge/ToolRegistry edits). Committed verbatim so nothing is lost while the checkout is brought in line with origin/development for live testing. NOT reconciled against development — several files here are older than their development counterparts, so this must be diffed against development and cherry-picked deliberately, never union-merged (the union-merge-drops-modifications hazard). Scratchpad audit notes excluded. Snapshot only — do not fast-forward development onto this.
…spection fix
DBAL virtual registers (type:database Sources introspected into a register)
served object reads live but had no aggregation path: the AggregationRunner
only knows the Postgres magic-table fast path + a PHP fallback that hydrates
magic-table rows, so every /api/objects/aggregations/{register}/{schema}/
value|timeseries|grouped call over an external register returned
{"value":0,"backend":"php-fallback"}. Dashboard KPI/stats-block + chart
widgets bound to such a register therefore read 0 while their list/detail
pages showed live data.
- DbalObjectSourceProvider::aggregate() computes count/sum/avg/min/max, a
categorical groupBy, or a date_trunc time-bucket series live in the external
DB, reusing the read path's source/table/column resolution, quoting and
allowlisting (Postgres native; MySQL DATE_FORMAT for fixed-width gaps).
Filter vocabulary matches the native path (eq + in/notIn/gt/gte/lt/lte/ne),
restricted to allowlisted columns. Fails closed (503/502) like the reads.
- AggregationRunner::runAdhoc() delegates to it (tryDbalAggregation) before the
magic-table paths when the schema carries x-openregister-object-source
provider 'dbal-source'. A provider error degrades to null (empty widget,
never a 500). The schema-level list RBAC gate still applies.
- SqlTypeMapper: map bytea (BINARY/BLOB) to string with no format instead of
the unsupported format:'binary', which OR's PropertyValidatorHandler rejects
— introspecting any table with a bytea column (e.g. tender_raw_json.
raw_json_gz) previously 502'd. bytea columns are already non-filterable.
Verified live against an external Postgres (spectr → intelligence-db): KPI
counts and monthly timeseries return backend:dbal-source with real values
(tenders 81950, requirements 157991, competitors 1468, ...). phpcs clean.
…-aggregations feat(dbal): aggregations for external virtual registers + bytea introspection fix
… of 500
An object detail page loads its integration tabs via
GET /api/objects/{register}/{schema}/{id}/integrations/{integrationId}.
For a DBAL virtual (external) register the object rows live in an external
database, so both the eager object load (ObjectService::setObject → MagicMapper)
and the integration providers resolve through a magic table that does not exist
(relation "oc_openregister_table_<reg>_<schema>" does not exist → 500). Every
integration tab on a detail page bound to an external register therefore 500'd.
ObjectIntegrationsController::index now detects an object-sourced schema
(Schema::getObjectSource() !== null) BEFORE guardObjectAccess — whose setObject
would throw — and returns an empty PaginatedResult. External read-only objects
carry no OR-native integration links, so the result is a constant empty set and
skipping the per-object RBAC guard leaks nothing. Registered SchemaMapper on the
controller's manual DI factory in Application.php.
Verified live (spectr → intelligence-db): the 9 integration endpoints
(deck/polls/bookmarks/shares/email/openproject/xwiki/cospend/sync-contract) that
returned 500 now return 200-empty; detail-page console errors dropped 28 → 19
(remaining are frontend 404 probes for other NC-native tabs). phpcs clean.
…ternal-register-guard fix(integrations): return empty for external-register objects instead of 500
… fully superseded
Reconciliation of the 8080 local working-tree snapshot against development:
* 0 files new to development
* 14 identical (already landed)
* 12 stale (3-way merge adds nothing over dev)
* 39 conflicting — local and dev edited the same regions, and on every one
development is larger and was actively developed 07-14..07-16, AFTER the
wip's 07-13 base. The 542 local-only lines are divergent phrasing, not new
logic. The only method absent from dev (ScheduledReportService::exportBytes)
is a private helper dev already implements as runExport(). Both local
migrations already exist on dev.
Development independently built more complete versions of every feature in the
snapshot (scheduled reports, notification delivery windows, MCP attribute
tooling, credential providers). Taking any of this branch's content would REVERT
development to older code, so this is a '-s ours' merge: the branch is recorded
as merged and retired; development's tree is kept verbatim. Nothing is lost —
the snapshot remains reachable in history for reference.
…aggregation - DbalObjectSourceProvider: introspect an external DBAL connection into a virtual register (Source type: database), reading objects/aggregations directly from the external schema. - AggregationRunner: DBAL aggregation execution path for external sources. - ObjectIntegrationsController: schema-aware integration endpoint; inject SchemaMapper via DI (Application.php registration). - SqlTypeMapper: external column-type mapping adjustments. Live-verified 2026-07-18 (spectr <-> intelligence-db external Postgres).
…r-source-provider feat(dbal): external database-backed register source provider + DBAL aggregation
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.
Automated PR to sync development changes to beta for beta release.
Merging this PR will trigger the beta release workflow.