feat: multi-org-membership — organisation switcher + self-service colleague access (#371) - #401
Merged
Merged
Conversation
…cess backend Adds OrganisationMembersController so a beheerder of an organisation can grant or revoke an EXISTING Nextcloud user's access to it (VNG Softwarecatalogus #65), without an administrator. OpenRegister's own join/leave endpoints only authorize a Nextcloud admin or the organisation's single owner field to manage another user's membership, which doesn't match SoftwareCatalog's beheerder role — this controller adds that domain-specific authorization guard (global beheerder group AND OpenRegister-verified membership of the TARGET organisation, both derived server-side, never from a client claim) and then delegates the actual mutation to OpenRegister's own OrganisationService::joinOrganisation()/ leaveOrganisation(). No parallel membership store is introduced. Also extends the existing /api/me endpoint with an isBeheerder flag so the frontend can decide whether to render the "manage members" affordance at all (a UX hint only — the real authorization is server-side). Covered by 8 PHPUnit tests including the mandatory negative cases: a non-beheerder is refused, a beheerder of a different organisation is refused, and a grant to a non-existent Nextcloud user is refused — all asserting the OpenRegister mutation is never invoked on a denied path. @SPEC openspec/specs/multi-org-membership/spec.md
…ss UI
Adds an organisation switcher to the app header, listing the user's own
organisations (sourced from the existing /api/me aggregate) and switching
the active one directly through OpenRegister's own
POST /api/organisations/{uuid}/set-active — membership is verified
server-side there; SoftwareCatalog adds no proxy controller for this
(ADR-011/ADR-022). A successful switch reloads the page so every list,
dashboard, and detail view re-fetches under the new active organisation
with zero risk of a partially-updated, cross-organisation-stale surface. A
refused switch (non-member) surfaces inline and never reloads.
The switcher's dropdown also hosts a beheerder-only "Manage members" entry
opening GrantOrganisationAccessModal — reads the member list from
OpenRegister's own GET /api/organisations/{uuid}, grants access via
NcSelectUsers (existing users only), and revokes it, both against the new
OrganisationMembersController backend.
App.vue fetches /api/me once at boot to seed CnAppRoot's tenant-context
props for first paint and orClient.js's module-level active-organisation
getter, which softwarecatalogPlugin.js's write paths now read to stamp
X-OpenRegister-Organisation (completing the header-stamping contract
already specified, but not yet wired up, by softwarecatalog-adopt-or-
abstractions). The switcher itself is deliberately self-contained rather
than relying on nc-vue's provide/inject propagating through a slot
override — see design.md's "no slot-inject dependency" decision.
Decision logic (switch-refusal handling, organisation-list derivation,
grant/revoke payload extraction) is extracted into pure, unit-tested
helper modules per the project's established colocated-.spec.js
convention.
@SPEC openspec/specs/multi-org-membership/spec.md
…ccess modal Dutch and English translations for the organisation switcher, the "manage members" entry, and the grant/revoke dialog (ADR-005/ADR-007).
… and self-service access Screenshots deferred — this change deliberately did not deploy to the shared dev Nextcloud instance; a follow-up pass should capture the header switcher and the "Manage members" dialog via Playwright MCP (ADR-010).
Proposal, design, spec deltas, tasks, and test-plan for the organisation switcher + self-service colleague access change, archived after implementation and verification. New capability spec/multi-org-membership adds 6 requirements (REQ-001..REQ-006); no pre-existing spec requirement was touched (hydra#376 archiver-data-loss guard checked: requirement counts before/after archive are identical across all 53 pre-existing spec files).
…trophes hydra-gate-spec-anchor-existence (gate-46) flagged 10 unresolved @SPEC targets once the canonical openspec/specs/multi-org-membership/spec.md existed to check anchors against (the gate could not validate anchors against a not-yet-archived spec, so this was invisible before archiving). Two requirement headers contain a possessive apostrophe — "the authenticated user's own organisations" (REQ-003) and "OpenRegister's OrganisationService" (REQ-006) — and this project's anchor convention turns every non-alphanumeric character into a word-break hyphen (e.g. the existing vendor-visibility-rbac anchor "...to-the-caller-s-own- organisation-req-003"), not a silent drop. The anchors here had dropped the apostrophes instead, producing "...-users-..." / "...-openregisters-..." instead of "...-user-s-..." / "...-openregister- s-...". Also fixes a trailing-period typo on the REQ-004 anchor in ContactpersonenController.php (introduced while satisfying a phpcs inline-comment-punctuation rule) that made that one anchor unresolvable too. gate-46 is green again; no behavioural change.
…loyment this pass hydra-gate-e2e-coverage (gate-19) flagged all 13 scenarios as missing Playwright coverage. This change's implementation pass deliberately did not deploy to the shared dev Nextcloud instance (see design.md), so no browser-driven e2e run was possible; server-side authorization is instead covered by PHPUnit (allowed + denied paths against mocked OpenRegister collaborators) and client-side decision logic by pure-logic unit tests. Blanket @e2e exclude after ## Purpose (mirrors the existing sc-handlers spec's convention) with the reason and a pointer to the follow-up.
This was referenced Jul 24, 2026
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.
Closes #371. Evidence: VNG issues #57/#60 (one account active in multiple organisations) and #65 (self-service colleague access).
Consumed OpenRegister rather than wrapping it
OrganisationService(getUserOrganisations,getActiveOrganisation,setActiveOrganisation,joinOrganisation,leaveOrganisation) andOrganisationController's HTTP surface already exist and are correctly authorized. The switcher callsset-activedirectly — no proxy controller (that would triphydra-gate-redundant-controller).One genuine gap justified new backend code: OpenRegister's join/leave authorize only an NC admin or the org's single
owner— not SoftwareCatalog'sbeheerderrole.OrganisationMembersControlleradds that domain-specific guard and then delegates the mutation to OpenRegister's own service.Server-side membership verification (never a client claim)
Organisation::hasUser($sessionUserId). A refused switch does not reload and leaves the active org unchanged.beheerdergroup and that the caller's own session-derived memberships include the target org — checked before any mutation, so a beheerder of org B cannot touch org A.Interacts with the RBAC model hardened in #395: switching changes what you can see, and cannot become a way to read a non-member organisation.
Tests: PHP 473 / 1510 assertions, 0 failures (8 new, all mandatory negatives) · Jest 113 · Vitest 210 · manifest Ajv PASS. Archive guard (hydra#376) run: requirement counts identical across all pre-existing spec files; new spec adds 6, none modified or removed.
Note: the
PortfolioReportControllerTestCSV failure tracked as #393 now passes — closing that separately.Not done: no live browser verification this pass (shared-container constraints); recorded honestly as a reason-bearing
@e2e excludeand in the feature doc rather than claimed.🤖 Generated with Claude Code