fix(lifecycle): initial-only x-openregister-lifecycle must not fail-closed every status change#2040
Merged
rubenvdlinde merged 2 commits intoJul 22, 2026
Conversation
added 2 commits
July 20, 2026 13:03
…ses every internal read RegisterMapper::getAllRegisterIdsWithSchema() and SettingsController's diagnostic query called Doctrine's fetchAllAssociative() on Nextcloud's IResult (OC\DB\ResultAdapter), which does not expose that method on every supported server — it is absent on NC 32 and throws 'Call to undefined method'. getAllRegisterIdsWithSchema() runs inside PermissionHandler's authorization resolver (getRegisterForSchema); its catch re-throws as AuthorizationUnresolvableException and FAIL-CLOSES the action. The net effect on NC 32: every RBAC-checked read via ObjectService::find() — dashboard widgets, the status-transition engine (available-transitions → current status never resolves), single-object reads — is denied even for admin, while the list path (which uses findMultipleOptimized, no per-row resolver) still works, masking the cause. Iterate fetch() instead, matching the fix already applied in MarkerLookupTrait. Verified on a live NC-32 instance: available-transitions now resolves the status name and procest dashboard widgets return 200 instead of 403. DbalObjectSourceProvider's fetchAllAssociative() calls are left as-is — those run against an external raw \Doctrine\DBAL\Query\QueryBuilder whose Result does expose the method.
…losed every status change
LifecycleValidationListener validates a status change against the schema
annotation's `transitions`, rejecting when no transition matches. But a schema
may legitimately declare `x-openregister-lifecycle` with ONLY an `initial` block
and NO `transitions` — to have OR derive the start state while the app owns
transition validation itself. procest's `case` schema does exactly this
(`{ field: status, initial: { from: caseType, field: initialStatus } }`) and
routes every advance through its own workflow-template state engine.
With no declared transitions, `findTransitionByTarget([])` returns null, so the
listener rejected EVERY status change (`lifecycle-invalid-transition`) — silently
breaking case status advancement app-wide (procest's transition engine 500s with
"No transition allows moving …"). Treat an empty/absent transition set as
"app-managed" and skip transition enforcement; the initial state is still pinned
by LifecycleInitialStateListener, and schemas that DO declare transitions are
still enforced (incl. their guards).
Found running procest's live-NC e2e suite on NC 32; verified the case
state-machine specs (advance + invalid-transition-blocked) now pass.
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.
Problem
LifecycleValidationListenervalidates a status change against the schema annotation'stransitionsand rejects when no transition matches (findTransitionByTarget(...) === null). But there is no guard for an empty/absent transition set.A schema may legitimately declare
x-openregister-lifecyclewith only aninitialblock and notransitions— to have OR derive the start state while the app owns transition validation itself. procest'scaseschema does exactly this:{ "field": "status", "initial": { "from": "caseType", "field": "initialStatus" } }procest routes every advance through its own workflow-template state engine. With no declared transitions,
findTransitionByTarget([])returns null, so the listener rejected every status change (lifecycle-invalid-transition) — silently breaking case status advancement app-wide. procest's transition engine surfaces it as a 500 (No transition allows moving "status" from … to …).Fix
Treat an empty/absent
transitionsset as app-managed: skip transition enforcement (early return). The initial state is still pinned byLifecycleInitialStateListener, and schemas that DO declaretransitionsare still fully enforced, including their guards.Verification
Reproduced + fixed on a live NC-32 instance (procest + OpenRegister). Before: procest
executeTransition→ 500No transition allows moving …. After: procest's case state-machine e2e specs pass — the advance transition succeeds and an invalid transition (missing required field) is still correctly blocked.Companion to #2035 (both surfaced running procest's live-NC e2e on NC 32).
🤖 Generated with Claude Code