# Direct-role changes — local application repair

The owner profile displayed direct roles but offered no removal control. The existing removal endpoint used an authentication-enriched user record and saved the whole record, which could turn inherited Sales/LeadEditor access into a permanent direct role when removing User.

## Change

`appengine/src/users/users.service.ts` now resolves the org-scoped raw user, writes only `data.roles`, confirms stored readback, preserves groups and unrelated data, and returns a password-redacted record. It supports historical comma-separated roles and removal of obsolete direct roles without requiring an existing role definition. Existing endpoint authorization remains in place; the service also enforces the root-tier restriction. Failed or silently missed writes cannot announce success or send a change notice.

`websitemint/packages/ui/src/components/user-management/modern-user-profile.tsx` labels the section **Direct roles**, explains inherited group access, and offers the organization owner a **Remove** action with confirmation. Owner, System and root-tier roles are excluded from this control. The request sends only email and the selected role, handles server refusals visibly, and uses confirmed server readback. Opening a profile refreshes both request cache layers.

## Validation

- 46 invitation/permissions service tests passed, including six new removal tests covering group inheritance, obsolete roles, unrelated concurrent changes, root/cross-org/empty-input refusal, and failed/readback-missed writes.
- Eight tests execute the actual profile handler: narrow payload, confirmed record, non-owner/busy/protected-role refusal, server error, and wrong-record response.
- Combined AppEngine full 8 GB build passed with stock-return fixes; private runtime environment restored byte-for-byte.
- Normal API PID 91876/session 20156 health 200 and `isHealthy: true` at 20:10:49 UTC.
- Studio restarted from the preserved original environment plus existing support overrides, PID 92762. Actual owner browser proof passed: Maya Ito’s direct User role was removed through confirmation, then a full reload/reopen showed Sales membership and No direct roles. The root walkthrough performed this authorized UI mutation; this implementation worker did not mutate user records.

Source tests: `src/users/users.permissions.spec.ts`, `src/users/users.invitation.spec.ts`, and `packages/ui/src/components/user-management/__tests__/direct-role-removal.test.cjs`. Saved test output in this report’s assets directory.

## Actual owner walkthrough

The owner opened Maya Ito’s profile showing Sales and direct User, selected **Remove**, reviewed **Remove direct role?**, confirmed **Remove role**, and fully reloaded/reopened the profile. Sales remained; Direct roles displayed **No direct roles**. Screenshots and text: `assets/local-roles/28-maya-direct-user-before-removal`, `29-maya-direct-role-confirmation`, `30-maya-direct-role-removed`, and `31-maya-removal-reloaded` (PNG/TXT pairs). A fresh Maya sign-in subsequently showed only Sales/LeadEditor, with no User, and CRM → Leads opened successfully (captures53–54 and `assets/local-roles/maya-recovered-effective-grants.json`).

Nora Ellis’s owner profile independently shows Sales and No direct roles after the repaired invitation acceptance (capture 27), while her actual session inherited only LeadEditor (capture 25) and opened Leads (26).

## Follow-up: direct-role addition

Review found that `roleAdd` still saved an authentication-enriched whole-user record. It now shares the same raw-read, direct-role-only partial-write and confirmed-readback helper with removal. Existing direct grants are preserved and deduplicated; group-derived roles are not materialized. Custom role IDs resolve to the org-scoped canonical role name, with root restrictions checked against both the requested name/ID and the resolved name. Unknown/foreign roles and missing input fail before writes. The established controller authorization remains intact.

**52 invitation/permission tests pass**, including six new addition regressions: inherited role preservation, repeated/canonical-ID addition, legacy direct roles plus unrelated concurrent changes, missing/foreign input, root name/ID refusal, and persistence failures. Saved `assets/direct-role-removal/add-tests.txt`. This follow-up compiled successfully and is present in the running API 98501; actual direct-add user mutation remains unperformed by this worker. No user mutations were performed for this follow-up.

## Fresh-session inheritance proof

Root completed a fresh Maya Ito sign-in after removing her legacy direct User grant. The session contains Sales and inherited LeadEditor only (captures 53–54 in `assets/local-roles/`). Together with the owner’s persisted No direct roles readback, this verifies the removal preserved intended group access without materializing inherited grants.
