Skip to main content

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

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

ObservationextractSensitivityLabels 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. RemediationassignSensitivityLabel 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) 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) 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, 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 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: 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.