# User Record Read Gate PRD

Status: current migration design for the User generic-read gate.
Date: 2026-05-25
Origin: merged PR #1370.
Issue: pingcap-inc/nexuscrm-pingcap-pack#1660

## 1. Problem

NexusCRM currently treats `User` as a `system_core` SObject whose generic read
surface is open to any authenticated caller unless an explicit
`requiredReadPermission` is configured. That means a Standard User with API
access can issue:

```text
GET /services/data/v63.0/sobjects/User/<any-user-id>
```

and receive another user's full `User` row, including identifiers and personal
or security-relevant fields such as username, email, phone, role, profile,
manager, login metadata, and audit fields.

This is an information-disclosure bug. It turns one low-privilege account into
an org-directory reconnaissance primitive.

The obvious one-line mitigation is to set:

```json
"requiredReadPermission": "ViewAllUsers"
```

on `User` in `backend/internal/bootstrap/system_tables.json`.

PR #1370 tried that. The attempt exposed a larger design problem: many live
features relied on generic `User` reads for lookup labels, pickers, ownership,
approval authoring, approval reassignment, connected apps, setup pages, record
comments, record sharing, dashboard running users, and admin tools. The PR then
added purpose-specific bypasses, most notably `/ui-api/user-lookup`, to keep
those flows alive.

That shape is not acceptable as the target architecture:

- it migrates callers by round-by-round enumeration instead of by contract;
- it authorizes User lookup by client-supplied `purpose` strings;
- it uses `SystemContext` to query `User`, bypassing the same read gate this
  change is meant to establish;
- it hardcodes application/admin workflow names in L0 REST code;
- it lacks real end-to-end proof that ordinary users cannot enumerate users
  through the replacement surfaces.

The PR should be redesigned around explicit User visibility primitives before
the generic `User` read gate flips.

## 2. Salesforce Behavior To Emulate Or Deliberately Tighten

Salesforce treats User visibility as a first-class security surface, not as a
plain object read that every feature may bypass.

Relevant documented behavior:

- Granting access to a `User` record makes the user detail visible and also
  makes that user visible in lookups, list views, search, and similar surfaces.
- `View All Users` grants read access to all users regardless of user sharing;
  `Manage Users` automatically carries that ability.
- User org-wide defaults and user sharing can make User records private or
  public read-only, with own-record and role-hierarchy behavior.
- The OAuth UserInfo endpoint returns information only for the current user and
  does not expose other users in the org.
- UI/API query surfaces expose only objects and fields the context user has
  access to.
- Lookup display names are a narrower surface than full record reads. Salesforce
  has a separate "View All Lookup Record Names" concept for name visibility in
  lookup/system fields.

NexusCRM intentionally chooses a stricter default than Salesforce's common
internal-user public-read deployment posture: Standard User must not get broad
generic `User` read or full-directory enumeration by default. That stricter
policy is acceptable only if it is modeled as metadata/security substrate, not
as a pile of one-off bypasses.

Two deltas must be explicit:

- NexusCRM is not implementing full Salesforce User Sharing in this PR. The
  target for this P1 is `ViewAllUsers`-class full reads plus scoped primitives.
- NexusCRM is not implementing a full `ViewAllLookupRecordNames` permission in
  this PR. Exact source-record lookup-label hydration is allowed as a narrow
  temporary delta because the server derives the User ID from a field already
  visible on a source record the caller can read. It must not become arbitrary
  User ID hydration or directory search.

For Salesforce parity, `ManageUsers` must satisfy the same full-User-read tier
as `ViewAllUsers`. NexusCRM can implement that as a system-permission
dependency or as a comma-separated `requiredReadPermission` value such as
`"ViewAllUsers,ManageUsers"`, but the design must not leave `ManageUsers`
delegated admins in a state where they can create/edit users but cannot read
the corresponding user-management surfaces.

Sources:

- Salesforce Help: User Sharing and Visibility / User Sharing Considerations
- Salesforce Help: "View All" and "Modify All" Permissions Overview
- Salesforce Help: Query for User Information
- Salesforce Help: Require Permission to View Record Names in Lookup Fields
- Salesforce Developers: GraphQL UI API Query Objects

## 3. Current NexusCRM Status Quo

### 3.1 Permission Gate

`PermissionService.checkObjectPermissionWithState` already honors
`ObjectMetadata.RequiredReadPermission` for `system*` tables. For reads:

- `BLOCKED` denies even superuser-like generic access;
- a non-empty permission token list is OR-matched against system permissions;
- an empty value allows any authenticated user to read system tables;
- `SystemContext` or scoped internal reads bypass user access in query paths.

So the platform already has the low-level mechanism needed to require the
full-User-read tier for generic `User` reads.

### 3.2 Generic Read Paths

Before PR #1370, `User` has `requiredWritePermission: "ManageUsers"` but no
`requiredReadPermission`. Therefore generic REST, SOQL, search, lookup
hydration, UI API, and frontend lookup widgets can treat `User` like a normal
readable object.

After the one-line gate flip, any caller who does not satisfy the full-User-read
tier loses those generic reads. That is the security objective for direct
arbitrary User reads, but it also breaks legitimate workflows that only need one
of these narrower capabilities:

- current user profile/detail rendering;
- showing labels for `OwnerId`, `CreatedById`, `LastModifiedById`, and other
  visible User-valued fields on a record the caller can already read;
- selecting an owner or actor for a server-authorized operation;
- resolving users named in approval metadata or connected app metadata for
  admins who can manage that metadata;
- listing users for delegated setup pages that are gated by `ManageUsers`,
  `ManageSharing`, `ManageRoles`, or `ManageProfilesPermSets`;
- hydrating comment authors for a record the caller can read;
- loading dashboard running users for analytics execution.

These are not all the same permission. The mistake in the current PR is trying
to repair them with one public "User lookup" endpoint plus a growing list of
purpose strings.

There is another less-obvious status-quo path: analytics query relationship
projection and lookup-name hydration can derive a target object from metadata
and load the referenced rows without a literal `TableUser` at the call site.
Examples include `_Name` suffix hydration and relationship selects such as
`Owner.Name`, `CreatedBy.Name`, or `Owner.Email`. Those paths currently batch
through repository reads and record-level access checks. They must be treated
as first-class User visibility surfaces because target-object read permission
and field projection are part of the same leak boundary.

### 3.3 PR #1370 Attempted Shape

PR #1370 currently:

- adds `User.requiredReadPermission = "ViewAllUsers"`;
- adds a current-user self-read carve-out in `PersistenceService.Retrieve` and
  `RetrieveWithAccess`;
- adapts UI API record aggregation for current-user record UI;
- adds `/ui-api/user-lookup`, `/ui-api/record-comment-authors`, and
  `/ui-api/record-share/user-candidates`;
- migrates many frontend widgets to the new lookup route;
- adds purpose literals such as `approvalMetadata`, `connectedApp`,
  `setupUserAdmin`, `setupGroupMembers`, `permissionAnalyzer`,
  `setupRoleUsers`, and `standardActionOwner`;
- reads `User` through `SystemContext` inside the lookup endpoint and several
  approval helpers.

This is a transitional patch, not a target design.

## 4. Goals

1. Close arbitrary generic User-record reads for users who do not satisfy the
   full-User-read tier.
2. Preserve current-user self-profile/profile-UI reads without granting broad
   directory visibility.
3. Preserve legitimate User-valued workflow operations through explicit,
   server-owned capabilities.
4. Make each User visibility surface auditable by contract: full record read,
   lookup label hydration, candidate search, metadata-admin listing, and
   server-internal actor resolution must be separate.
5. Keep L0 platform code generic and multi-app neutral.
6. Land the read-gate switch only after all read paths have one authoritative
   owner and end-to-end coverage.

## 5. Non-Goals

- Do not implement the full Salesforce User Sharing feature in this PR.
- Do not expose generic `User` search/list/query to all authenticated users.
- Do not make every feature that mentions a user require the full-User-read
  tier.
- Do not add more L1-shaped `purpose` enum branches to L0 REST handlers.
- Do not treat frontend caller migration as the authorization boundary.
- Do not solve tenant-specific user-visibility policy in platform code.

## 6. Product And Security Requirements

### R1. Generic User Read Gate

Direct generic `User` reads through SObject REST, SOQL, generic search, UI API
object-info/layout/record surfaces, and generic lookup hydration must require
the full-User-read tier, except for explicit self-read and explicitly scoped
primitives defined below.

The full-User-read tier is `ViewAllUsers` plus Salesforce-compatible
implications such as `ManageUsers`. If NexusCRM does not yet have a generic
system-permission implication engine, the `User` read gate must express this as
an OR-list.

Direct arbitrary reads must fail as either 403 or sanitized 404, using the
existing Salesforce v63 error-envelope rules.

### R2. Self User Read

A user may read a bounded projection of their own `User` record for profile,
navigation, and UI context.

The self-read surface must be explicit. It must not be implemented by a
general-purpose branch that lets `User/<self>` unlock arbitrary User lookup or
metadata APIs.

The projection should include only fields already needed by frontend profile
and session UX. Sensitive/admin fields should require the normal full User read
permission.

### R3. Lookup Label Hydration

When a caller can read a source record and can see a User-valued source field,
the platform may return a minimal display label for the exact User ID already
stored in that field.

This primitive:

- accepts source object, source record ID, and source field;
- derives the target User ID from the source record on the server;
- never accepts arbitrary target User IDs from the client unless the caller
  satisfies the full-User-read tier;
- returns a minimal label projection, not full User data;
- does not support directory search.

This covers read-only display labels for `OwnerId`, audit fields, and
metadata-defined User references without turning any readable source record into
a directory oracle.

For SOQL and UI relationship projection, this means callers who do not satisfy
the full-User-read tier may receive only minimal labels for the User IDs already
present on records they can read. They must not be able to request arbitrary
User subfields such as `Email`, `ProfileId`, `UserRoleId`, or `ManagerId`
through relationship selects unless they satisfy the full-User-read tier.

### R4. Candidate Search For User-Valued Writes

Searching for a replacement User is allowed only when a server-owned operation
has already proven that the caller may perform that operation and has derived
the candidate scope.

The candidate-search primitive must not accept a client-supplied source field
as sufficient proof. "Can edit a field whose type references User" does not
mean "can enumerate active users."

Candidate search must be tied to an operation contract such as:

- change owner for a specific record;
- assign share recipient for a specific record;
- reassign a specific approval work item;
- choose a run-as user for a connected app as a connected-app admin;
- choose users for approval-process metadata as an approval metadata admin.

For each operation, the server must derive the permission check, allowed target
population, projection, and pagination shape.

### R5. Setup/Admin User Management

Setup pages that are truly user-management pages can manage users under their
own setup permissions. For example:

- `ManageUsers` can list and administer users;
- `ManageSharing` can search users and groups for group/share administration;
- `ManageRoles` can list role membership;
- `ManageProfilesPermSets` can search users for permission analysis.

These are not generic `User` reads. They are setup/admin surfaces and must have
their own backend contracts, projections, and pagination. They must not reuse a
500-row capped lookup helper when the UI requires full admin-list semantics.

The setup contract must cover more than listing. A delegated user admin must be
able to open `/setup/users`, load create/edit form metadata, load the relevant
layout or form schema, create users, edit users, and save changes without
falling back to generic `User` describe/object-info/layout routes that are
gated as full User-record reads.

### R6. Server-Internal User Resolution

Internal runtimes may need to resolve a User row to execute metadata, not to
return directory data to the caller. Examples include dashboard running-user
sessions, approval runtime actor resolution, and metadata deploy references.

These reads may use privileged context only inside a named internal helper that:

- is not reachable as a generic public lookup endpoint;
- returns only the internal shape needed by the runtime;
- checks the caller's authority for the parent operation before loading users;
- does not pass arbitrary User rows back to the client.

### R7. L0 Boundary

L0 may define generic primitives such as:

- full User record read;
- self User projection;
- source-field label hydration;
- operation-scoped candidate search;
- setup/admin user directory list;
- internal actor/session resolution.

L0 must not define app/workflow-specific purpose literals like
`approvalMetadata`, `connectedApp`, or `recordShareCandidates` as a central
public enum. Those workflows should bind to generic primitives through typed
server methods or metadata-owned operation definitions.

### R8. Verification

The final gate flip must include real permission-system coverage, not only
handler tests backed by fake query services.

At minimum, coverage must prove:

- Standard User cannot read arbitrary User rows through SObject REST/SOQL/UI
  API/generic search.
- Standard User can read the approved self projection.
- A readable record can hydrate only the exact User label stored in a visible
  User-valued field.
- Field-scoped lookup cannot search the user directory merely because the field
  is editable.
- Operation-scoped candidate search works for authorized operations and fails
  for unauthorized operations.
- Setup/admin user listings honor their setup permission and paginate beyond
  500 users when the UI requires complete lists.
- Public endpoints cannot combine self-read or source-record-read proof into
  arbitrary User ID hydration.

## 7. Proposed Architecture

### 7.1 Name The Surfaces

Use these canonical terms in code and docs:

| Term | Meaning |
|---|---|
| User record read | Full or normal SObject read of a `User` row. Requires the full-User-read tier, except self projection. |
| Self User projection | Bounded current-user data for profile/session UI. Never authorizes other User lookups. |
| User reference label | Minimal display label for a User ID already present on a readable source record field. |
| User relationship projection | SOQL/UI relationship output for User-valued fields, constrained to labels unless full User read is granted. |
| User candidate search | Search result set for a specific server-authorized operation that needs a target user. |
| Setup user management | Admin/setup listing, search, metadata/form, create, and edit surfaces under setup permissions. |
| Internal user resolution | Privileged server-only load of User data needed to execute metadata/runtime behavior. |

### 7.2 Replace Public Purpose Routing With Typed Server Contracts

Delete the target shape of `/ui-api/user-lookup?purpose=...`.

Instead, expose separate server-owned handlers or service methods only where a
public UI flow needs one. The route name should describe the resource/operation,
not a caller-supplied authorization enum.

Examples:

- `GET /ui-api/user-reference-label`
  - Inputs: `sourceObject`, `recordId`, `sourceField`
  - Server derives target ID from the source record.
  - Projection: `Id`, `Name`.

- `GET /actions/standard/changeOwner/candidates`
  - Inputs: source object, record ID, search term.
  - Server loads the action metadata, checks the caller may invoke Change
    Owner on that record, derives eligible users/queues, then searches.

- `GET /approval/work-items/:id/reassign-candidates`
  - Server proves the caller is the actor or otherwise authorized to reassign
    that work item, then searches the candidate population.

- `GET /setup/users`
  - Gated by `ManageUsers`.
  - Server-side pagination and filters.
  - Projection suitable for user administration.

The exact route names can follow existing NexusCRM route conventions. The
important contract is that the operation authorizer runs before any privileged
User read, and that public clients do not choose a privileged `purpose`.

### 7.3 Centralize User Visibility Service

Add one backend service as the only owner for privileged User visibility
decisions, for example:

```go
type UserVisibilityService interface {
    CanReadFullUser(ctx, actor) bool
    GetSelfProjection(ctx, actor) (SObject, error)
    ResolveReferenceLabel(ctx, actor, source ObjectRecordField) (UserLabel, error)
    ResolveRelationshipProjection(ctx, actor, source RelationshipProjectionRequest) (RelationshipProjection, error)
    SearchCandidates(ctx, actor, operation CandidateOperation) (Page[UserCandidate], error)
    ListSetupUsers(ctx, actor, request SetupUserListRequest) (Page[SetupUser], error)
    GetSetupUserForm(ctx, actor, request SetupUserFormRequest) (SetupUserForm, error)
    LoadInternalUserSession(ctx, reason, userID) (*UserSession, error)
}
```

This service owns:

- all decisions to use caller context vs elevated/system context;
- output projections;
- pagination and caps;
- audit/log reason strings for privileged loads;
- anti-enumeration behavior.

Handlers, approval runtime, UI API, standard actions, and frontend-specific
adapters must call this service instead of issuing direct `SystemContext`
queries against `User`.

### 7.4 Gate Flip Sequencing

Do not merge the `User.requiredReadPermission = "ViewAllUsers"` line as the
first change in a broad migration PR.

Use a staged sequence:

1. Add the `UserVisibilityService` and tests while `User` is still generically
   readable. The service should still enforce the future target behavior.
2. Migrate one caller class at a time to the service:
   - self profile/UI;
   - source-record User label hydration;
   - SOQL/UI User lookup-name hydration and relationship projection;
   - Change Owner;
   - record share;
   - record comments;
   - approval runtime and approval metadata;
   - connected apps;
   - setup user admin list/search/form/create/edit, group admin, role admin,
     permission analyzer;
   - dashboard running user;
   - generic non-polymorphic User reference validation.
3. Add a mechanical audit that identifies remaining `User` reads that do not
   go through the service.
4. Flip `User.requiredReadPermission`.
5. Run real integration coverage proving the gate and legitimate flows.

If PR #1370 must stay one PR, it must still follow the same internal shape:
service first, callers switched, old public bypass endpoint removed, gate flip
last.

## 8. Caller Migration Contract

Each caller migration must answer these questions in code or tests:

1. What user-visible workflow is being preserved?
2. What exact permission or record access proves the caller may perform it?
3. Is the flow displaying an existing User reference, searching candidates, or
   loading internal runtime data?
4. Who derives the target User IDs: server or client?
5. What fields are returned?
6. Is pagination required?
7. What negative test proves the flow cannot enumerate arbitrary Users?

No migration is complete if it only changes the frontend to call a different
endpoint.

## 9. Rejected Designs

### 9.1 Keep Generic User Read Open

Rejected because it leaves the issue #1660 vulnerability open.

### 9.2 Flip The Gate And Patch Callers Opportunistically

Rejected because PR #1370 already demonstrated non-convergence: each review
round found another live caller or another bypass. This is exactly the
forbidden refactor shape for a source-of-truth switch.

### 9.3 One Public `/ui-api/user-lookup` Endpoint With `purpose`

Rejected because it turns authorization into a client-selected enum, creates
L1-shaped branching in L0, and encourages `SystemContext` reads that bypass
the new read gate.

### 9.4 Field-Scoped Search Based On Editable User Reference Fields

Rejected because edit access to a field is not proof that the caller may search
the User directory. It may justify validating one submitted target for a
specific mutation, but it does not justify directory enumeration.

### 9.5 Return Only Fewer Fields From A Broad Directory Search

Rejected as insufficient. A list of active names/usernames/emails is still a
directory reconnaissance surface. Projection minimization is necessary but not
sufficient; the candidate set must also be authorized.

## 10. Acceptance Criteria

The implementation is complete only when all of these are true:

- `User.requiredReadPermission` is pinned to the full-User-read tier in
  `system_tables.json` and spec tests. If permission implications are not
  modeled generically yet, that means an OR-list such as
  `ViewAllUsers,ManageUsers`.
- There is no public endpoint that accepts arbitrary User IDs and hydrates them
  under `SystemContext` for callers who do not satisfy the full-User-read tier.
- There is no public endpoint that lets a caller who does not satisfy the
  full-User-read tier search all active users unless the operation-specific
  authorizer has derived that candidate scope.
- Generic frontend lookup widgets refuse `User` lookup without a typed
  operation/source-label contract.
- SOQL/UI lookup-name hydration and relationship projection route User targets
  through the visibility service and cannot expose arbitrary User subfields to
  non-full-read callers.
- Setup/admin user pages use setup user-management contracts for list/search,
  form metadata, create, and edit; they do not depend on generic User SOQL,
  generic User describe/object-info/layout, or 500-row lookup helpers.
- Server-internal User resolution is centralized, named, and not reachable as
  a generic client endpoint.
- A mechanical audit of `TableUser`, `"User"`, `Retrieve`, `RetrieveByField`,
  `Query`, `SearchSingleObject`, `getRichRecord`, and `searchSingleObject`
  shows all live user-read paths are either migrated or explicitly justified.
- Real integration tests cover both denial of arbitrary User enumeration and
  survival of legitimate workflows.

## 11. Verification Plan

### Static Audit

Raw `rg` is useful for inventory, but it is not the acceptance gate. User reads
can be indirect through metadata-derived relationship targets, and many risky
call chains are multiline. The final implementation needs a scripted audit
that parses Go and TypeScript call sites or an equivalent CodeQL-style query.
It must fail CI when a public path introduces a `User` read outside the
approved visibility service/adapters.

The audit must catch at least:

- `WithSecurityContext(... SystemContext ...)` in the same call chain as
  `TableUser`, `"User"`, metadata-derived `ReferenceTo` targets, or
  `GetRecordsByIDs` relationship hydration.
- `hydrateLookupNames`, `projectRelationshipSelectFields`, `_Name` suffix
  hydration, and relationship selects such as `Owner.Name` / `CreatedBy.Name`.
- direct `Retrieve`, `RetrieveByField`, `Query`, `QueryWithFilter`,
  `SearchSingleObject`, `GetRaw`, and repository reads of `User`.
- frontend generic `User` lookup or hydration through `searchSingleObject`,
  `getRichRecord`, `useObjectMetadata`, `useLayout`, `hydrateUserLookup`, or
  `/User/` endpoints.

Use these as preliminary inventory commands only:

```bash
rg -t go 'TableUser|"User"' backend/internal \
  | rg 'Retrieve|RetrieveByField|Query|QueryWithFilter|SearchSingleObject|GetRaw|SystemContext'

rg -g '*.ts' -g '*.tsx' 'searchSingleObject\\(.User|getRichRecord\\(.User|useObjectMetadata\\(.User|useLayout\\(.User|hydrateUserLookup|listUserLookup|/User/' frontend/src
rg -n 'hydrateLookupNames|projectRelationshipSelectFields|GetRecordsByIDs|ReferenceTo' backend/internal/features/analytics
```

Expected result: no public, caller-controlled User read bypasses remain outside
the User visibility service and its operation-specific adapters.

### Targeted Backend Tests

Add tests for:

- permission-service gate matrix for `User.requiredReadPermission`;
- SObject direct retrieve/query/search denial for Standard User;
- self projection success and arbitrary self-derived bypass denial;
- source-field label hydration returns only the stored User ID's label;
- SOQL relationship projection returns only allowed User labels for
  non-full-read callers and rejects or omits arbitrary User subfields;
- field-scoped candidate search denial without an operation contract;
- operation-specific candidate searches for Change Owner, record sharing, and
  approval reassignment;
- setup user-management permissions, form metadata, create/edit flows, and
  pagination;
- approval metadata and connected app admin resolution;
- dashboard running user internal resolution.

### End-To-End Or Real-DB Tests

At least one real permission-stack test must use a Standard User without
the full-User-read tier and prove:

- direct `GET /sobjects/User/<admin>` fails;
- direct SOQL `SELECT Id, Email FROM User` fails;
- the user can render a record detail page whose `OwnerId` is another user,
  but receives only the minimal label authorized by the readable source record;
- SOQL such as `SELECT Owner.Name FROM Account` can return the allowed label,
  while `SELECT Owner.Email FROM Account` does not leak email without the
  full-User-read tier;
- the user cannot use the same source record to hydrate arbitrary User IDs or
  search all users;
- an authorized operation, such as Change Owner where allowed by action
  metadata, still works through its typed candidate endpoint.

### Baseline Commands

For a final implementation PR:

```bash
(cd backend && go test ./...)
(cd shared && go test ./...)
(cd frontend && npm run build)
git diff --check
```

If backend tests touch `backend/internal/controlplane/packs`, set:

```bash
export NEXUSCRM_OFFICIAL_APP_PACKS_ROOT=/abs/path/to/nexuscrm-app-packs
```

## 12. Open Decisions

1. Should NexusCRM model full Salesforce User Sharing now, or only the stricter
   `ViewAllUsers` plus explicit scoped primitives for this P1?
   - Recommended: only the stricter scoped-primitives design for this P1.

2. Should self-read return the normal `User` SObject shape or a bounded profile
   DTO?
   - Recommended: bounded DTO/projection. It avoids making self-read a special
     case in every generic SObject path.

3. Should lookup labels expose `Email`?
   - Recommended: no for generic label hydration. Use `Name` only unless an
     operation-specific candidate search proves email is needed.

4. Should generic editable User-reference fields ever search active users?
   - Recommended: no. They may validate submitted IDs at write time through a
     server-side target validator, but search requires an operation contract.

5. Should setup user lists be part of the same service as lookup labels?
   - Recommended: yes as one service owner for User visibility, but exposed as
     separate typed methods and routes.

6. Does `ManageUsers` imply full User read?
   - Recommended: yes. Match Salesforce by treating `ManageUsers` as satisfying
     the `ViewAllUsers` tier, either through permission implication or an OR
     read gate.

7. Should lookup labels wait for a full `ViewAllLookupRecordNames` permission?
   - Recommended: no for this P1. Allow only exact source-record labels as a
     documented temporary Nexus delta, and track the broader permission as a
     follow-up.

## 13. PR #1370 Disposition

The current PR should take the deferral path. It should not continue adding
purpose branches to `search_user_lookup.go`, and it should not land the
`User.requiredReadPermission` gate flip until the migration sequence above is
complete.

Recommended recovery path:

1. Preserve the one-line gate intent in this design record.
2. Revert the broad `/ui-api/user-lookup?purpose=...` design and the current
   gate flip from the implementation PR.
3. Land caller migrations in follow-up PRs by the contracts above.
4. Add the User visibility service and mechanical audit before the final gate
   flip.
5. Re-run review after the mechanical audit and real permission-stack tests are
   clean.

The final gate flip can then be a small, reviewable PR with strong verification
instead of a 50-file source-of-truth switch.
