> ## Documentation Index
> Fetch the complete documentation index at: https://docs.findmydata.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Permissions manifest

# Microsoft permissions & capability manifest

**Scope:** Find My Data (this repository, release 26.7.15.0, revised 2026-07-15).
**Honesty rule:** this document separates what is *implemented and exercised* (the
deterministic mock M365 connector, plus the live Graph connector's recorded
read-only CDX validation), what is *built but feature-/config-gated on the live
path* (live AI providers, Purview label read/write, change notifications, Teams,
Exchange), and what is *a seam only* (Entra sign-in — a 501 endpoint with no OIDC
implementation). See `STATUS.md` and ADR-0012 for the gating decision, and
`docs/cdx-test-runbook.md` for the CDX validation plan and the recorded run (§0).

**Update (live run):** a follow-up session validated the live Graph connector
**read-only** against the Contoso CDX tenant using application permissions
**`Sites.Read.All` + `Files.Read.All`** (admin-consented on app `FMD-CDX-Reader`).
`Sites.Selected` (below) remains the least-privilege production recommendation; the
broader Read.All pair was used only to stand up a working read-only pilot quickly.
Reading assigned sensitivity labels was **not** attempted (needs the protected
`extractSensitivityLabels` / `InformationProtectionContent.Read.All`), so the
connector reports label state `unknown` — honestly, never guessing. Run record:
`docs/cdx-test-runbook.md` §0 and ADR-0013.

All Microsoft Learn references were last checked during design on 2026-07-13
(handoff reference architecture §18). Re-verify API status, licensing,
protected/metered requirements, and national-cloud support before any live
deployment.

***

## 1. Identity model

### 1.1 Separate app registrations: read/scan vs. remediation

Find My Data's live path uses (at minimum) **two Entra ID app registrations**
with independent credentials:

| Identity                 | Purpose                                                                                                                        | Config keys                                                                  |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------- |
| **Read/scan identity**   | Scope discovery, `driveItem` delta enumeration, metadata, permission observation, label observation, bounded content retrieval | `FMD_GRAPH_TENANT_ID`, `FMD_GRAPH_CLIENT_ID`, `FMD_GRAPH_CLIENT_SECRET_REF`  |
| **Remediation identity** | Purview label assignment/removal (`assignSensitivityLabel`) and future permission remediation                                  | `FMD_GRAPH_REMEDIATION_CLIENT_ID`, `FMD_GRAPH_REMEDIATION_CLIENT_SECRET_REF` |

Why two identities (handoff security doc §7.1):

* **Blast-radius containment.** A compromise of the scanner — the component
  that touches untrusted content at volume — must not inherit any write
  capability against the customer tenant. The scan identity holds read-only
  permissions; only the remediation identity can mutate source state.
* **Independent rotation and revocation.** A customer can disable remediation
  (revoke one credential) without losing inventory/scan continuity, and vice
  versa.
* **Auditable consent.** Each registration carries a small, explainable
  permission set; the consent screens map 1:1 to product capabilities.

The reference architecture recommends further splitting notifications/
subscriptions into a third identity. Change notifications **are** built in this
slice: Graph `/subscriptions` on the read resource (no permission beyond the
resource read the connector already holds), a public HTTPS `notificationUrl`
(`FMD_WEBHOOK_PUBLIC_URL`) that Graph must reach, and a per-subscription
`clientState` HMAC the receiver verifies before any side effect. Without the
public URL the live path is honestly `blocked`; a dev simulator exercises the
receiver + reconcile path. See `packages/server/src/notifications/`.

Secrets are referenced by opaque IDs (`*_SECRET_REF`), never stored as values
in configuration. The slice's config layer (`packages/server/src/kernel/config.ts`)
validates presence; the intended production store is Azure Key Vault or
equivalent, with managed/workload identities preferred over client secrets.

### 1.2 Development-mode note

This build runs in **dev identity mode** by default (`FMD_AUTH_MODE=dev`,
ADR-0004): sign-in is a persona picker over seeded fixture principals, every
response carries `x-fmd-auth-mode: dev`, and the UI shows a persistent
development banner. No Microsoft identity is involved on the exercised path.

Two production protections are implemented and tested:

* **Production lockout:** `FMD_ENV=production` with `FMD_AUTH_MODE=dev` is a
  hard startup failure (`ConfigError`), and dev-session cookies are rejected
  in production even if present.
* **Immutable identity mapping:** external identities map to internal
  principals by `(issuer, subject)`, never by email or display name.

Entra OIDC user sign-in is the intended production path but exists **only as a
seam** in this build: `GET /api/auth/entra/login` returns an explicit **501
`not_configured`** (never a silent fallback to dev), and there is **no** OIDC
code-flow or token/JWT-validation implementation yet (ADR-0004;
`packages/server/src/identity/auth-routes.ts`). `/api/ready` reports
`entraSignIn` as `blocked` (mode selected, flow unimplemented) or `disabled`
(dev mode) — never "live."

***

## 2. Microsoft Graph permission manifest (live path)

The **read/scan** permissions below (`Sites.Read.All` + `Files.Read.All`) were
consented on the `FMD-CDX-Reader` app registration and exercised **read-only**
against the live Contoso CDX tenant (ADR-0013, `docs/cdx-test-runbook.md` §0).
Everything else remains unconsented and gated. The table is the
deployment-generated manifest the handoff requires (security doc §7.2):
every permission, why it is needed, and its least-privilege alternative.

**Type key:** A = application permission, D = delegated permission.
**Status key:** *mock* = capability exercised via the deterministic mock
connector; *gated* = code path exists behind configuration, not validated
live; *future* = designed, not built.

| Permission                                         | Type | Identity             | Why needed                                                                                                        | Feature                   | Least-privilege alternative                                                                                                                                             | Status in this build                                                                                            |
| -------------------------------------------------- | ---- | -------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `Sites.Selected`                                   | A    | read/scan            | Scoped read access to explicitly granted SharePoint sites for delta enumeration, metadata, permissions, content   | Inventory + delta scan    | This *is* the least-privilege option for SharePoint                                                                                                                     | gated (mock connector exercises the equivalent behavior)                                                        |
| `Files.Read.All`                                   | A    | read/scan            | Tenant-wide drive read where `Sites.Selected` cannot cover required targets (notably OneDrive personal drives)    | Inventory + delta scan    | Prefer `Sites.Selected` per site; for OneDrive evaluate the newer Selected-permission models (see 2.1)                                                                  | gated                                                                                                           |
| `Files.ReadWrite.All` **or** `Sites.ReadWrite.All` | A    | remediation          | Required by `assignSensitivityLabel` (write path)                                                                 | Purview label remediation | Scope with `Sites.Selected` write role where the API supports it; verify against tenant                                                                                 | gated + blocked on protected-API enablement (see 2.4)                                                           |
| `InformationProtectionPolicy.Read.All`             | A    | read/scan            | Beta label-catalog listing (`/beta` `security/informationProtection/sensitivityLabels`)                           | Label catalog sync        | Customer-run `Get-Label` export (default production path, ADR-0008)                                                                                                     | gated; fixture catalog is the exercised path (see 2.5)                                                          |
| `User.Read.All`                                    | A    | read/scan            | Directory properties for evidence-ranked owner suggestion (title, department, manager chain)                      | Ownership intelligence    | Restrict acquired properties by policy; do not request `Directory.Read.All` unless group ownership requires it                                                          | gated behind `FMD_FEATURE_ENTRA_DIRECTORY_SYNC` (default off); fixture principals are the exercised path        |
| `GroupMember.Read.All`                             | A    | read/scan            | Group membership expansion for exposure analysis                                                                  | Permission normalization  | Compact transitive-membership projections; acquire only groups observed in grants                                                                                       | future                                                                                                          |
| `openid`, `profile`, `email`                       | D    | sign-in registration | Entra OIDC user sign-in                                                                                           | Authentication            | Standard minimal OIDC scopes                                                                                                                                            | gated (`FMD_AUTH_MODE=entra`), dev identity is the exercised path                                               |
| `ChannelMessage.Read.All`                          | A    | read/scan            | Teams channel message acquisition (`getAllMessages` — itself a protected/metered API with licensing requirements) | Teams connector           | Canonicalize Teams *files* to their backing SharePoint `driveItem` first (no extra permission — the connector does this); messages are a separately approved capability | **built**; gated behind `FMD_FEATURE_TEAMS_CONNECTOR` + this consent (mock fixture is the exercised path)       |
| `Mail.Read`                                        | A    | read/scan            | Selected-mailbox message delta (`mailFolders/{id}/messages/delta`) + attachment bytes                             | Exchange connector        | Scope to the explicit selected mailboxes (`FMD_EXCHANGE_MAILBOXES`) via Exchange Online *Application Access Policy*, never tenant-wide                                  | **built**; gated behind `FMD_FEATURE_EXCHANGE_CONNECTOR` + a scoped policy (mock fixture is the exercised path) |

### 2.1 `Files.Read.All` vs. `Sites.Selected`

`Files.Read.All` grants read on **every** drive in the tenant; it is simple
but maximally broad. `Sites.Selected` grants nothing by itself — it requires
both Entra admin consent **and** an explicit per-site grant (via the
`sites/{id}/permissions` API or admin tooling) before any site is readable.
See [Selected permissions in OneDrive and SharePoint](https://learn.microsoft.com/en-us/graph/permissions-selected-overview).

Find My Data's position:

* Default the read/scan identity to `Sites.Selected` and surface, per scope,
  the difference between "connector is consented" and "target is actually
  accessible." The connector contract's `discoverScopes()` exists precisely to
  validate reachable scopes rather than assume them.
* Fall back to `Files.Read.All` only where selected models cannot cover a
  required target (historically OneDrive personal drives; Microsoft has been
  extending selected-permission models — `Files.SelectedOperations.Selected`
  and related — so re-verify coverage at implementation time and document any
  endpoint that forces the broader grant).
* The mock connector models both SharePoint drives and a OneDrive scope so the
  scope-granting UX and per-scope accessibility states are exercised without a
  tenant.

### 2.2 `driveItem` delta

Initial inventory and incremental change tracking both use
[driveItem delta](https://learn.microsoft.com/en-us/graph/api/driveitem-delta?view=graph-rest-1.0).
Contract expectations implemented in this slice (against the mock connector):

* `@odata.nextLink` / `@odata.deltaLink` are persisted as **opaque** cursors,
  exactly as returned, never parsed (`connectors/types.ts`; ADR-0008).
* `410 Gone` → full resynchronization is a first-class result
  (`ChangesResult.kind = "resync_required"`), tested via the mock connector's
  simulated 410.
* Deletes, renames, and duplicate delivery are handled and tested via
  scripted delta sequences (`scan.test.ts`).
* The eTag/cTag distinction (any-change vs. content-change version) drives the
  stage-cache invalidation matrix, so a rename or permission change does not
  re-fetch content.

### 2.3 Permission observation caveats

[driveItem permissions](https://learn.microsoft.com/en-us/graph/api/driveitem-list-permissions?view=graph-rest-1.0)
returns direct and inherited grants, but **the detail returned depends on the
caller's access**. This slice therefore treats permission observations as
evidence with explicit completeness, not ground truth:

* Every permission observation carries a `completeness` value —
  `complete | partial | not_observable` (`packages/shared/src/domain-enums.ts`) —
  and the acquiring authorization context is recorded on the observation
  envelope.
* "No broad share observed" is never presented as "no broad share exists"
  (handoff security doc §7.3). Downstream exposure/risk scoring consumes the
  completeness state.
* Broad audiences (anonymous links, organization links, tenant-wide groups)
  are stored as categories, not expanded into per-user edges.

### 2.4 `extractSensitivityLabels` and `assignSensitivityLabel`

**Observation** —
[extractSensitivityLabels](https://learn.microsoft.com/en-us/graph/api/driveitem-extractsensitivitylabels?view=graph-rest-1.0)
refreshes label metadata from supported file content and has explicit failure
modes (notably encrypted content). The slice's label observation result
records the method used (`metadata | extract_api | fixture`) and the label
state including `unsupported` and `inaccessible`; the exercised path is
`fixture`.

**Remediation** —
[assignSensitivityLabel](https://learn.microsoft.com/en-us/graph/api/driveitem-assignsensitivitylabel?view=graph-rest-1.0)
is a **protected, metered** API. Practical consequences, all reflected in the
implementation:

* **Enablement process:** access must be requested from Microsoft (protected
  API request/approval flow) before the tenant will honor calls; usage is
  metered (billed). In this build the capability is behind
  `FMD_FEATURE_PURVIEW_LABEL_WRITE` (default `false`) and `/api/ready` reports
  `purviewLabelWrite: blocked` until enabled.
* **Justification requirements:** label downgrade or removal requires a
  justification. The action state machine (ADR-0010) enforces justification
  plus approval by policy default for downgrade/removal, independent of the
  API requirement.
* **Application-only limitations:** some protected (encrypted) files cannot be
  relabeled in application-only mode. The connector's `previewLabelAction`
  returns `supported: false` with limitations, and validation transitions the
  action to a blocked state with the reason (`actions/service.ts`).
* **Asynchrony and verification:** the live operation is long-running; the
  state machine polls and then **re-reads source state** to verify — executor
  success text alone never marks an action succeeded.
* **Marking caveat:** Office-at-rest labeling does not imply desktop-client
  visual markings (headers/footers) were rendered; verification records label
  state, not rendering.

In this build the full draft → validate → approve → execute → verify state
machine is real and tested **in mock mode only**; live execution additionally
requires the remediation app identity and protected-API enablement.

### 2.5 Label catalog listing (beta caveat)

The Graph label-list operation
([security/informationProtection sensitivityLabels](https://learn.microsoft.com/en-us/graph/api/security-informationprotection-list-sensitivitylabels?view=graph-rest-beta))
is documented under `/beta`, which Microsoft states is not supported for
production applications. Per ADR-0008, the catalog is therefore an isolated
adapter (`LabelCatalogSource`) with three paths:

1. **Fixture import** — default and exercised in this build.
2. **Customer-run `Get-Label`** ([Security & Compliance PowerShell](https://learn.microsoft.com/en-us/powershell/module/exchangepowershell/get-label?view=exchange-ps))
   CSV export/import — the recommended production path until a v1.0 API exists.
3. **Future supported API** — slot reserved.

Label identity everywhere is `(msTenantId, labelGuid)`. Names and hierarchy
are versioned display attributes; label names are never hard-coded and never
used as identity.

***

## 3. Throttling and `Retry-After` handling expectations

Per [Microsoft Graph throttling guidance](https://learn.microsoft.com/en-us/graph/throttling),
every live adapter must implement:

* **Honor `Retry-After`** on 429/503 exactly; exponential backoff with jitter
  when the header is absent.
* **Retry classification:** the durable job model (ADR-0007) already
  distinguishes `throttled` from `transient`, `auth`, `unsupported`,
  `corrupt`, `policy_denied`, and `permanent`; throttled work re-queues with
  the provider-specified delay and does not burn retry budget the way
  permanent failures do.
* **Per-provider concurrency budgets** and tenant/source fairness so one large
  scope cannot starve others; backpressure propagates upstream so acquisition
  never overwhelms extraction or persistence.
* **Prefer delta over polling** — the pipeline is delta-driven by design, which
  is the primary throttle-avoidance strategy.
* **Request correlation** — correlation IDs are part of the canonical
  observation envelope for diagnosing throttling with Microsoft support.

Status: the queue mechanics (leases, heartbeat, backoff, dead-letter, retry
classes, pause/resume) are implemented and tested against the mock connector,
which can script throttling responses. No live throttling behavior has been
observed this run. Microsoft recommends Graph Data Connect for bulk
extraction; it is explicitly out of scope for this slice (reference
architecture §5.8).

***

## 4. Change notifications

[Microsoft Graph change notifications](https://learn.microsoft.com/en-us/graph/change-notifications-overview)
are **built** in this slice (section 1.1; `/api/ready` reports `changeNotifications`
as `mock` in mock mode, and `disabled`/`blocked`/`live` in graph mode depending on
`FMD_FEATURE_CHANGE_NOTIFICATIONS`, resolvable Graph credentials, and
`FMD_WEBHOOK_PUBLIC_URL` — see section 5). Design position:

* **Delta remains authoritative.** Notifications are wake-up *hints* only; the
  pipeline processes changes exclusively from the delta feed. This means a
  deployment loses freshness, not correctness, when notifications are
  unavailable — every scope can still be reconciled by delta scans regardless.
* **Subscription lifecycle is real operational surface:** subscriptions
  expire and must be renewed ahead of expiry; lifecycle events must be
  consumed; duplicates must be deduplicated; silent/stale subscriptions and
  widening cursor lag need alarms; delivery latency varies by resource and can
  exceed the 15-minute processing target, which is why the internal SLO is
  defined from *receipt or delta observation*, not source event time.
* **Deployment prerequisite:** a publicly reachable, validated webhook
  endpoint (or supported Azure messaging delivery), which the local slice does
  not have.

Live notifications require no additional Graph permissions beyond the resource
permissions above, but should run under a distinct identity per section 1.1.

***

## 5. Capability status (mirrors `/api/ready`)

`GET /api/ready` (`packages/server/src/routes/health.ts`) reports one honest
mode per capability: `mock`, `live`, `blocked` (configured/required but
prerequisites unmet — never silently faked), or `disabled` (not configured /
not built). Current build state and exact prerequisites to advance each:

| Capability            | Current status                                                                    | Exercised path                                                                                                                                                                                                                                                                                | Prerequisites to go (or stay) live                                                                                                                                                                                                                                                                                                                                                                            |
| --------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `connector`           | **mock** by default; **live** in graph mode with resolvable credentials           | Deterministic Meridian Grove fixture tenant (full scan/delta/permissions/labels/content path, tested) **plus** a recorded **read-only live validation against the Contoso CDX tenant** (ADR-0013, `docs/cdx-test-runbook.md` §0)                                                              | `FMD_CONNECTOR_MODE=graph`; `FMD_GRAPH_TENANT_ID`, `FMD_GRAPH_CLIENT_ID`, and a `FMD_GRAPH_CLIENT_SECRET_REF` that **resolves** to a value; admin consent for read permissions (section 2). Missing/unresolvable credentials raise an explicit `BlockedError` — never a silent mock fallback (`connectors/registry.ts`). Recommended next: migrate `Sites.Read.All` → per-site `Sites.Selected` (section 2.1) |
| `aiProvider`          | **mock**                                                                          | Deterministic mock provider (only path exercised by tests; no credentials in repo)                                                                                                                                                                                                            | `FMD_AI_PROVIDER=azure-openai` + `FMD_AZURE_OPENAI_ENDPOINT`/`_DEPLOYMENT`/`_API_KEY_REF`, **or** `FMD_AI_PROVIDER=anthropic` + `FMD_ANTHROPIC_API_KEY_REF` (model defaults to `claude-sonnet-5`). Adapters exist; unvalidated against live endpoints                                                                                                                                                         |
| `entraSignIn`         | **disabled** in dev mode; **blocked** when `FMD_AUTH_MODE=entra`                  | Dev identity provider with production lockout (tested). The Entra path is a **501 seam with no OIDC implementation** (ADR-0004)                                                                                                                                                               | **Implement** the OIDC code flow on the seam (issuer/audience/nonce validation); Entra app registration for sign-in (`openid profile email`); redirect URI registration; live validation against a tenant. Production additionally requires a non-default `FMD_SESSION_SECRET`                                                                                                                                |
| `purviewLabelRead`    | **blocked**                                                                       | Fixture label observations (tested); the live `extractSensitivityLabels` read path is implemented and gated                                                                                                                                                                                   | `InformationProtectionContent.Read.All` consent + `FMD_FEATURE_PURVIEW_LABEL_READ=true` on a graph-configured deployment                                                                                                                                                                                                                                                                                      |
| `purviewLabelWrite`   | **blocked**                                                                       | Full action state machine in mock mode (draft/validate/approve/execute/verify, idempotency, drift — tested); the live `assignSensitivityLabel` write path is implemented behind the gates with a separate remediation identity                                                                | Microsoft protected-API enablement for `assignSensitivityLabel` (request/approval, metered billing accepted); remediation app registration (`FMD_GRAPH_REMEDIATION_CLIENT_ID`, `FMD_GRAPH_REMEDIATION_CLIENT_SECRET_REF`) with write permission (section 2.4); label catalog synchronized (`Get-Label` import for production); then `FMD_FEATURE_PURVIEW_LABEL_WRITE=true`                                    |
| `changeNotifications` | **mock** in mock mode; **blocked**/**disabled** in graph mode until prerequisites | **Built**: subscription lifecycle + clientState-HMAC-verified webhook receiver + delta reconciliation, exercised via the dev simulator (a simulated notification drove a real read-only delta reconcile against CDX data; section 1.1, `notifications.test.ts`)                               | `FMD_FEATURE_CHANGE_NOTIFICATIONS=true` + a publicly reachable `FMD_WEBHOOK_PUBLIC_URL` Graph can call; recommended dedicated notification identity                                                                                                                                                                                                                                                           |
| `teamsConnector`      | **mock** in mock mode; **blocked**/**disabled** in graph mode until prerequisites | **Built**: teams→channels→messages sync with file **canonicalization to the backing SharePoint `driveItem`** (never a second identity), exercised against the mock fixture (`teams.test.ts`)                                                                                                  | `FMD_FEATURE_TEAMS_CONNECTOR=true`; `ChannelMessage.Read.All` consent (protected API — approval + licensing requirements); live validation on a test tenant                                                                                                                                                                                                                                                   |
| `exchangeConnector`   | **mock** in mock mode; **blocked**/**disabled** in graph mode until prerequisites | **Built**: selected-mailbox folders + per-folder [message delta](https://learn.microsoft.com/en-us/graph/api/message-delta?view=graph-rest-1.0) with attachment↔SharePoint **similarity** matching (a copy, never an identity merge), exercised against the mock fixture (`exchange.test.ts`) | `FMD_FEATURE_EXCHANGE_CONNECTOR=true`; `Mail.Read` consent scoped to `FMD_EXCHANGE_MAILBOXES` via [Exchange Online RBAC for Applications](https://learn.microsoft.com/en-us/exchange/permissions-exo/application-rbac) (selected mailboxes, never indiscriminate tenant-wide capture); live validation on a test tenant                                                                                       |

The readiness payload never reports `live` on a feature flag alone: `live`
additionally requires the relevant credentials to **resolve** (the `*_REF` env
var names a variable that actually holds a value); an enabled capability whose
credentials do not resolve reports `blocked` (`routes/health.ts`, tested in
`routes/health.test.ts`). Even so, do not set feature flags ahead of their
consent prerequisites: Graph rejects unconsented calls at run time (403), which
surfaces as failed work units and probe failures — never silent success.

### Internal authorization (for contrast)

Independent of Microsoft permissions, all product APIs are governed by the
slice's own **capability-based authorization** (`kernel/authz.ts`): role
grants are `(capability, scope)` tuples where scope is tenant-wide or
domain-scoped (e.g., an HR domain owner holds `asset.read` only within owned
domains), evaluated per request against the tenant context, with enforcement
tested. Remediation additionally enforces separation of duties (requester
cannot approve, ADR-0010). Every privileged operation lands in the per-tenant
hash-chained audit log. Data handling follows the minimized-evidence retention
profile (ADR-0011): bounded redacted excerpts, purpose-flagged artifacts, and
default-deny log redaction.
