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). SeeSTATUS.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.
/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=productionwithFMD_AUTH_MODE=devis 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.
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.Selectedand surface, per scope, the difference between “connector is consented” and “target is actually accessible.” The connector contract’sdiscoverScopes()exists precisely to validate reachable scopes rather than assume them. - Fall back to
Files.Read.Allonly where selected models cannot cover a required target (historically OneDrive personal drives; Microsoft has been extending selected-permission models —Files.SelectedOperations.Selectedand 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.deltaLinkare 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
completenessvalue —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
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
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(defaultfalse) and/api/readyreportspurviewLabelWrite: blockeduntil 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
previewLabelActionreturnssupported: falsewith 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.
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:
- Fixture import — default and exercised in this build.
- Customer-run
Get-Label(Security & Compliance PowerShell) CSV export/import — the recommended production path until a v1.0 API exists. - Future supported API — slot reserved.
(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-Afteron 429/503 exactly; exponential backoff with jitter when the header is absent. - Retry classification: the durable job model (ADR-0007) already
distinguishes
throttledfromtransient,auth,unsupported,corrupt,policy_denied, andpermanent; 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.
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.
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.