Skip to content

fix(access): only administration managers change the segment; portal absence reports get their owner stamped on the server - #1129

Merged
rubenvdlinde merged 2 commits into
developmentfrom
feat/settings-and-excuse-authorization
Sep 28, 2026
Merged

rubenvdlinde merged 2 commits into
developmentfrom
feat/settings-and-excuse-authorization

Conversation

@rubenvdlinde

@rubenvdlinde rubenvdlinde commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Scope

Two write defects: any instructor could change the organisation's segment, and every portal absence report was refused before learniq saw it. LearniqSettings gets an authorization block, and ExcuseRequest's owner fields move from required into a server-side stamp, the way #1068 did it for Submission.

What changes

LearniqSettings had no authorization block, so the register cascade (read-write for instructors, hr, compliance-officers, team-leads) applied. The menu entry was admin-only; the objects API was not. Now:

action groups
read instructors, hr, compliance-officers, team-leads, coordinators, administration-managers, confidential-counsellors
create, update administration-managers (admins pass OpenRegister's bypass)
delete nobody but admins

SegmentService reads and writes the row with _rbac: false from the admin-only setup contract, so the wizard is unaffected.

ExcuseRequest listed learnerId, submittedBy, submittedAuthLevel and tenant_id as required. Portaliq's createExcuseRequest (pupil minTrust: low, guardian minTrust: substantial, portaliq#607) sends only the dates, reason, kind and attachment, plus its scope stamp. OpenRegister validates required in saveObject() before ObjectCreatingEvent, so every portal report got a 400. Now required is dateFrom, dateTo, reason, reasonKind, and ExcuseRequestOwnerStamp (on create and update, registered in IntegrityListenerRegistrar):

  • portal report (no session, no learnerId, a learnerRef): learnerId, learnerRef, tenant_id from the pupil's LearnerProfile; unknown pupil or failed lookup refuses the write;
  • pupil's own report: the pupil is the submitter, submittedAuthLevel: basic;
  • guardian's report (submittedByRef set): refused unless the child's profile lists that guardian in guardianRefs; submittedBy is the guardian's user id when they have one; submittedAuthLevel: substantial;
  • every other write: learnerRef derived from learnerId (a client value is replaced), submittedAuthLevel: basic when none is sent;
  • every write that still lacks the pupil, a submitter (submittedBy or submittedByRef) or the school is refused with excuse-owner-missing.

The recorded level is the minimum the portal action enforced, because portaliq does not send the level it reached.

Versions: both schemas 0.2.0 to 0.3.0, info.version 0.28.1 to 0.29.0. Docs: a short section on portal reports in docs/user-guide/user/05-attendance.md.

Tests

  • ExcuseRequestOwnerStampTest (13 tests): pupil and guardian stamps, guardian without an account, guardian of another child, unknown pupil, failed lookup, signed-in create without a pupil, staff create (derived ref, forged ref replaced, default and kept level), missing owner fields, update keeping the stored ref, another schema ignored, registration on create and update.
  • ExcuseRequestRegisterTest (3): the required list, and every required field is in both portal actions' field lists (read from the real PortalContributionProvider).
  • SegmentFeatureFlagsRegisterTest::testOnlyAdministrationManagersChangeTheSegment.
  • The register tests were shown red on development's register first.

Verification (on this branch, bb390304, cut from a84b6273)

Landing

Cut from development (a84b6273), independent of #1124 and #1126, so its full-suite failure set is development's known twelve. All three PRs edit lib/Settings/learniq_register.json (info.version and info.description); the schemas touched here (LearniqSettings, ExcuseRequest) are touched by neither of the other two. IntegrityListenerRegistrar: the Submission owner stamp moves, unchanged and in the same order, into a private registerOwnerStamps() beside the new one, which keeps register() under phpmd's method length limit.

OpenSpec change: openspec/changes/settings-and-excuse-authorization/.

CI (read once, 2026-09-28)

PHPUnit in CI: 1936 tests, 12 failures, development's known set; 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

…absence reports get their owner stamped on the server

LearniqSettings gets an authorization block (staff read, administration-managers write), so an
instructor can no longer change the organisation's segment through the objects API.
ExcuseRequest.required keeps only what every caller sends; ExcuseRequestOwnerStamp fills the
pupil, submitter, level and tenant of a portal report from the profile (a guardian only for a
child that lists them), derives learnerRef for every other write, and refuses any write that
still lacks the pupil, a submitter or the school, as SubmissionOwnerStamp does for Submission.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/learniq @ f5a2b85

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:52 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde
rubenvdlinde merged commit 132d1d7 into development Sep 28, 2026
34 of 35 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/learniq @ 1d286a3

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:26 UTC

Download the full PDF report from the workflow artifacts.

rubenvdlinde added a commit that referenced this pull request Sep 28, 2026
Resolves the IntegrityListenerRegistrar conflict with #1129 by moving the
attempt time limit into its own AttemptLimitListenerRegistrar, called next
to the attempt gate, since the merged registrar reached phpmd's coupling
limit. Full PHPUnit after the merge: 2005 tests, no failures.
rubenvdlinde added a commit that referenced this pull request Sep 28, 2026
…ver and delete LearnerProfileLookup

#1129 landed with a LearnerProfileLookup caller while this change was open;
with it moved, no caller is left and the facade goes.
rubenvdlinde added a commit that referenced this pull request Sep 28, 2026
… one resolver find a learner's profile (#1147)

* fix(learners): back-fill learnerRefs on existing submissions, and let one resolver find a learner's profile

Submissions written before the server stamped learnerRefs (#1056) and
learnerRef (#1068) never reached the portal. BackfillSubmissionLearnerRefs,
a post-migration repair step after InitializeSettings, stamps what the
write-path stamps derive today, saves only rows that differ, and never
overwrites on a failed lookup.

Portal\LearnerProfileLookup, added twice as a copy of LearnerRefResolver
(#1068, #1096), folds into it: byRef() and resolveAcrossTenants() join
resolve(), so no caller's read changes. LearnerProfileLookup stays as a
deprecated facade because #1129 adds a caller; delete it once none is left.
See openspec/changes/learnerrefs-backfill-and-lookup-dedupe/.

* chore(release): move the version so an upgrade runs the new submission back-fill

* chore(learners): record the verification of learnerrefs-backfill-and-lookup-dedupe

* fix(learners): move the excuse request owner stamp to LearnerRefResolver and delete LearnerProfileLookup

#1129 landed with a LearnerProfileLookup caller while this change was open;
with it moved, no caller is left and the facade goes.
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