Skip to content

feat: multi-org-membership — organisation switcher + self-service colleague access (#371) - #401

Merged
rubenvdlinde merged 7 commits into
developmentfrom
wip/multi-org-membership
Jul 24, 2026
Merged

feat: multi-org-membership — organisation switcher + self-service colleague access (#371)#401
rubenvdlinde merged 7 commits into
developmentfrom
wip/multi-org-membership

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

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) and OrganisationController's HTTP surface already exist and are correctly authorized. The switcher calls set-active directly — no proxy controller (that would trip hydra-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's beheerder role. OrganisationMembersController adds that domain-specific guard and then delegates the mutation to OpenRegister's own service.

Server-side membership verification (never a client claim)

  • Switch: OpenRegister derives membership from Organisation::hasUser($sessionUserId). A refused switch does not reload and leaves the active org unchanged.
  • Grant/revoke: requires both the global beheerder group 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 PortfolioReportControllerTest CSV 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 exclude and in the feature doc rather than claimed.

🤖 Generated with Claude Code

…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.
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