Skip to content

fix(access): enforce the access ratchets on the import records, first aid and the import guards (D23) - #1124

Merged
rubenvdlinde merged 2 commits into
developmentfrom
fix/access-control-ratchet-compliance
Sep 28, 2026
Merged

rubenvdlinde merged 2 commits into
developmentfrom
fix/access-control-ratchet-compliance

Conversation

@rubenvdlinde

@rubenvdlinde rubenvdlinde commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Scope

Six of the twelve red tests on development are access rules that four round-one schemas never met. This PR applies the ratchets' own rules to them, as decision D23 asks, and changes no other schema's access.

What changes

Schema or class Before After
LvsResult no authorization block, so the register cascade applied: instructors, hr, compliance-officers, team-leads read and wrote every row, a pupil read nothing. appendOnly: true read: coordinators, compliance-officers and the pupil's own row (learnerId); create and update: coordinators, compliance-officers; no delete; not append-only
OsoImportDossier no block (cascade) read, create, update: coordinators, compliance-officers. No self-read: learnerEckId is not a Nextcloud user
FirstAidIncident read instructors, compliance-officers; appendOnly: true read adds {authenticated, match: {reportedBy: $userId}}; not append-only. Writers unchanged
LvsResultVerifyGuard, OsoImportAcceptGuard, OsoImportRejectGuard tested coordinator, a group nobody is in test coordinators, the declared group
DossierNote pins in DeclaredAudienceEnforcedTest, RbacScopeKindsRegisterTest expected the read list without the care-team entry expect both conditional entries; the register is unchanged

Versions: the three schemas 0.1.0 to 0.2.0, info.version 0.28.1 to 0.29.0.

Access narrows or stays equal for every group except two, both named by their specs. coordinators gain read on both import records, because the verify, accept and reject guards name them as the actor and they cannot act on a row they cannot find (ImportRecordAccessTest::testNoGroupGainsReadExceptTheGuardsActor pins that nothing else gains read). The pupil gains read on their own LVS results, which lvs-import-contract specified. instructors, hr and team-leads lose read and write on both import records.

Writers follow D23, not the cascade: coordinators and compliance-officers. The nextcloud-app requirement that cascade schemas keep the cascade writers gets an explicit exception for these two records (spec delta in this change).

Tests red before, green now

  • DeclaredAudienceEnforcedTest::testNoSchemaDeclaresAnAudienceWithoutAnAuthorizationBlock, ::testEverySelfMatchIsEnforced, ::testGroupOnlyBlocksAddThePersonTheRowIsAbout
  • GuardGroupsAreDeclaredTest::testEveryGroupAGuardTestsIsDeclared
  • LifecycleSchemasAreNotAppendOnlyTest::testNoSchemaIsAppendOnlyAndTransitions
  • RbacScopeKindsRegisterTest::testDossierNoteGainsCareTeamPropertyAndReadEntry

New tests, each shown red on development's register and guards before the change: ImportRecordAccessTest (5 tests), testSingularCoordinatorGroupIsDenied in each of the three guard tests. LifecycleSchemasAreNotAppendOnlyTest runs LvsResult.verify and FirstAidIncident.startHandling.

Verification (on this branch, d949174a)

  • composer check:strict (through the lane semaphore, TMPDIR=$PWD/.tmp): exit 1, from two inherited reds only.
    • lint, phpcs, psalm (no errors), phpstan (no errors): green.
    • phpmd: one finding, CaseListenerRegistrar coupling 13, present on development; round1-landing-repairs (stacked on this PR) moves the bridge that causes it.
    • PHPUnit: 1929 tests, 6 failures. All six are in development's known set and belong to round1-landing-repairs: AttendanceThresholdRegisterTest::testThresholdCrossedNotificationNotifiesMentorAndCoordinator, DataMappingProfilePresetsRegisterTest::testExistingSeedsUnchangedAndFiveAdded, LvsResultRegisterTest::testDataExchangeJobTargetDescribesLvsResults, OsoImportDossierRegisterTest::testDataExchangeJobTargetDescribesOsoImport, UwlrEduvBasispoortRegisterTest::testExistingTimetableImportSeedsUnchanged, ::testTargetDescriptionsNameNewConnections. No new failure.
  • npm run lint: exit 0 (19 inherited warnings). npm run format: exit 0. npm run test:l10n: no such script.
  • npm run check:specs, check:schema-l10n, check:l10n-js: exit 0.
  • openspec validate access-control-ratchet-compliance: valid.
  • opsx-verify (headless, code level; no live instance for this lane): 10 of 10 tasks, 3 of 3 requirements implemented, every scenario pinned by a named test, design decisions D1 to D4 followed. 0 critical, 0 warnings. One suggestion, the LVS score freeze listener below.
  • Hydra gates, HYDRA_GATE_BASE_REF=origin/development --scope-to-diff: exit 4, 43 of 44 applicable gates ran. gate-16 spec-coverage, gate-47 security-change-has-tests, gate-51, gate-101 pass. The reds are inherited: gate-108 (three Entitlement objects in learniq_mock_register.json still carry orderLineId, which feat(payments): retire orders for shillinq; raise fee contributions there and grant entitlements on its settled signal (D19) #1082 dropped from the schema; this PR does not touch the mock register), gate-113 (six @e2e exclude lines in openspec/specs/payments/spec.md cite tests feat(payments): retire orders for shillinq; raise fee contributions there and grant entitlements on its settled signal (D19) #1082 deleted), gate-112 newman-reach, gate-53 and gate-68 (the gate's own Node script fails to load as an ES module on this box).

Inherited, not touched

phpmd's CaseListenerRegistrar coupling, the six landing-repair tests above, check:schema-l10n reporting 9 strings fewer than its baseline, and the gate reds listed above.

Follow-ups named, not built

  • Dropping appendOnly lets coordinators and compliance-officers edit an imported LVS score. Before, nobody could, but nobody could verify either. A freeze listener in the style of AssessmentResultIntegrityListener would keep the score fixed after import.
  • Notification recipients across the register name role words that are not declared groups (coordinator 10 times, mentor, examboard, compliance-officer, study-advisor, exam-board), so those notifications reach nobody. Same class as the guard groups fixed here; not in D23's scope.

Landing

round1-landing-repairs is stacked on this branch; land this one first. Both touch lib/Settings/learniq_register.json. D25 (integriq takes over data exchange) is another round-three lane and may touch DataExchangeJob; this PR does not.

OpenSpec change: openspec/changes/access-control-ratchet-compliance/. Decision D23 (Ruben, 2026-09-27).

CI (read once, 2026-09-28)

PHPUnit in CI: 1929 tests, 6 failures, exactly the six listed above for #1126; phpmd red on the inherited CaseListenerRegistrar coupling. The CI PHPUnit job also runs with coverage, where failOnRisky and beStrictAboutCoverageMetadata turn 10 tests into risky ones (CourseSharingControllerTest, StoreControllerTest, CourseShareExportServiceTest, CourseMetadataFilterTest, CourseStorePublisherTest: they execute classes their #[CoversClass] does not list). That makes the job exit 1 even with zero failures. None of those files is touched here; it is inherited from the course store changes and does not show locally with --no-coverage. CI Hydra Gates runs full-tree and reports the same inherited set as a local full run (gate-3, gate-25, gate-49, gate-55, gate-62, gate-108); none is on a file this PR touches.

🤖 Generated with Claude Code

…d the import guards (D23)

LvsResult and OsoImportDossier get authorization blocks (read and write by coordinators and
compliance-officers, plus the pupil's own LVS result), FirstAidIncident enforces its reporter's
self-read, the verify/accept/reject guards test the declared coordinators group, and LvsResult
and FirstAidIncident drop appendOnly so their lifecycles run. The DossierNote audience pins learn
the care-team entry. Closes six of development's twelve red tests.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/learniq @ cde3134

Check PHP Vue Security License Tests
lint ✅
phpcs ✅
phpmd ❌
psalm ✅
phpstan ✅
phpmetrics ✅
eslint ✅
stylelint ✅
build ✅
check-specs ✅
format ✅
check-l10n-js ✅
check-schema-l10n ✅
composer ✅ ✅ 107/107
npm ✅ ✅ 747/747
app:check-code ⏭️
info.xml ✅
REUSE ❌
lockfile sync ✅
PHPUnit ❌
Newman ⏭️
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 on any branch from the Actions tab, or locally with npx playwright test.
Hydra gates ❌

Quality workflow — 2026-09-27 18:32 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde
rubenvdlinde merged commit 5226c21 into development Sep 28, 2026
34 of 36 checks passed
rubenvdlinde added a commit that referenced this pull request Sep 28, 2026
…its tree equals fix/access-control-ratchet-compliance)
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/learniq @ f81f8c8

Check PHP Vue Security License Tests
lint ✅
phpcs ✅
phpmd ❌
psalm ✅
phpstan ✅
phpmetrics ✅
eslint ✅
stylelint ✅
build ✅
check-specs ✅
format ✅
check-l10n-js ✅
check-schema-l10n ✅
composer ✅ ✅ 107/107
npm ✅ ✅ 747/747
app:check-code ⏭️
info.xml ✅
REUSE ❌
lockfile sync ✅
PHPUnit ❌
Newman ⏭️
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 on any branch from the Actions tab, or locally with npx playwright test.
Hydra gates ❌

Quality workflow — 2026-09-28 07:24 UTC

Download the full PDF report from the workflow artifacts.

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