# Role and group application repairs

Local source fixes completed 2026-09-18 for the custom-role resolver and group membership failures described in the roles/groups learner review. No course or learner-review files were edited. No deployment, invitations, real membership changes or customer messages occurred.

## Reproduced failures

- Actual `UsersService.getUserPermissionsAndRoles` with a saved `LeadEditor` role failed before the change with **Cannot read properties of undefined (reading 'content')**. The backend passed custom names to the SDK's built-in-only role resolver.
- The actual GroupEditor Save handler, extracted and executed with isolated collaborators, failed before the change with **User should be updated using user api** for both addition and removal. It saved the group, then attempted the deliberately forbidden generic repository user write.
- Backend group-add read an enriched user and saved the entire record, risking promotion of inherited roles to permanent direct grants. It also changed groups to a comma-separated string. The new regressions check that only membership changes and revocation removes the inherited grant.
- `findUser` spread an unawaited permission promise into session data, so middleware sessions did not receive the calculated grants. Awaiting it required separating profile-edit reads from session enrichment to prevent inherited grants being persisted by userUpdate.

## Changes

AppEngine:

- `src/users/role-permissions.ts`: resolves saved roles within the requested organization, including overrides of built-in roles. Stored permission arrays are filtered to supported verbs. Known built-ins retain the SDK fallback; unknown/deleted names and malformed grants grant nothing. Lookup failures propagate instead of silently granting fallback permissions. Tenant root-role grants remain prohibited.
- `src/users/users.service.ts`: resolves direct and group-derived roles; supports arrays, CSV and historical JSON arrays/group IDs; awaits session resolution; profile edits merge the stored user record rather than effective session grants. Group add/remove resolves canonical group names, validates the resolved group's root restrictions, and writes only `data.groups` through the repository partial-update path. Fresh readback verifies persistence. Other groups, direct roles, credentials and unrelated fields remain intact. Existing access-change notification behavior is retained in application code; its command bus is mocked in tests.
- `src/users/users.permissions.spec.ts`: 23 isolated service regressions.

Studio (`/Users/imzee/projects/websitemint`):

- `packages/ui/src/components/user-management/group-editor.tsx`: uses the existing `usersService.addGroups` / `removeGroups` APIs with only email and group name; it no longer submits a full user to the generic repository writer.
- `packages/ui/src/components/user-management/__tests__/group-membership.test.cjs`: four tests executing the actual Save handler, covering addition, removal, user-write failure and group-write failure.

The existing `profile/user/group/*` endpoint aliases are valid because the backend controller registers both `profile` and `user` prefixes; no endpoint routing change was needed. Preexisting application changes and the earlier customer-profile repair were preserved. No applicable AGENTS.md was found in the repository/ancestor paths inspected.

## Validation

- Before/after failures reproduced in isolated source tests; logs for this session are in `/tmp/appmint-permissions-fix/`.
- **23/23 new AppEngine tests passed**: exact custom grants, direct grants, multiple-group union, historical shapes, stored built-in overrides, cross-org isolation, unknown/malformed roles, tenant root-role refusal, backend lookup failure, middleware session resolution, profile-merge preservation, membership addition/removal, canonical IDs, unrelated concurrent field preservation, no full-record membership write, and failed/silent-no-op persistence.
- Earlier customer-profile regression suite also passed: **67/67 combined tests**.
- **4/4 Studio Save-handler tests passed**, after reproducing the original generic-user-save rejection.
- Full AppEngine `tsc --noEmit --incremental false --pretty false` passed with no diagnostics.
- Both repository `git diff --check` checks passed. Studio tests transpile and execute the changed handler; no full Studio build or browser test is claimed.

Commands:

```sh
# appengine
node node_modules/jest/bin/jest.js --runInBand --no-cache --cacheDirectory=/tmp/appmint-permissions-jest --testPathPattern='users.permissions.spec.ts|client-account.profile.spec.ts' --globals='{"ts-jest":{"isolatedModules":true,"diagnostics":false}}'
NODE_OPTIONS=--max-old-space-size=8192 node node_modules/typescript/bin/tsc --noEmit --incremental false --pretty false

# websitemint
node --test packages/ui/src/components/user-management/__tests__/group-membership.test.cjs
```

## Release and scope limits

Deploy both backend and Studio changes, then validate a controlled colleague's fresh sign-in, permitted edit, denied action, direct navigation refusal, and access after removing membership. These runtime checks remain unperformed; saved role/menu configuration is not a per-collection authorization rule.

Group and user membership writes remain multiple requests, not a transaction. If a later request fails, earlier writes can persist; the drawer remains open with the error and no overall completion callback. The backend preserves unrelated fields, but simultaneous edits to the same user's membership list are not serialized by this repair. Existing records that already contain accidentally materialized direct roles need a separate authorized review; this change does not remove existing direct grants automatically. Full user-profile editing remains its existing administrative API and is not replaced with a universal restricted patch endpoint here.
