# Gotchas — Permissions, Roles, Consent

## Onboarding a new tenant

Run `onboard-tenant.sh` after admin consents each app. This assigns required directory roles automatically.

Quick steps:
1. Send consent URLs (Tenant Admin FIRST, then others)
2. After admin accepts: `bash scripts/onboard-tenant.sh <domain>`
3. Verify output shows all roles [OK]
4. Update tenant table below

If Tenant Admin is not yet consented, onboard-tenant.sh will output all needed consent URLs.

## App Suite (tiered architecture)

Five multi-tenant apps replace the old single over-permissioned app. Use minimum necessary tier.

| Tier | Display name in customer tenant | App ID | Vault file |
|---|---|---|---|
| `investigator` / `investigator-exo` | ComputerGuru Security Investigator | `bfbc12a4-f0dd-4e12-b06d-997e7271e10c` | `computerguru-security-investigator.sops.yaml` |
| `exchange-op` | ComputerGuru Exchange Operator | `b43e7342-5b4b-492f-890f-bb5a4f7f40e9` | `computerguru-exchange-operator.sops.yaml` |
| `user-manager` | ComputerGuru User Manager | `64fac46b-8b44-41ad-93ee-7da03927576c` | `computerguru-user-manager.sops.yaml` |
| `tenant-admin` | ComputerGuru Tenant Admin | `709e6eed-0711-4875-9c44-2d3518c47063` | `computerguru-tenant-admin.sops.yaml` |
| `defender` | ComputerGuru Defender Add-on | `dbf8ad1a-54f4-4bb8-8a9e-ea5b9634635b` | `computerguru-defender-addon.sops.yaml` |

**DELETED from the azcomputerguru.com tenant 2026-06-14** (was *ComputerGuru - AI Remediation* / *Claude-MSP-Access* / *Cloud MSP Access*, `fabb3421-8b34-484b-bc17-e46de9703418`) — old single-app with 159 permissions including Defender ATP. Any token request now returns **AADSTS700016** (app/SP gone). Do NOT reference it as a live app anywhere. Consequence: the legacy "old app only" tenants below (Valleywide, Dataforth, Cascades) have NO working remediation app anymore — migration to the new suite is REQUIRED, not optional.

**Mail.Send — already in the suite (DONE, not an open decision).** The **Exchange Operator** tier (`exchange-op`, `b43e7342-5b4b-492f-890f-bb5a4f7f40e9`) holds Graph **`Mail.Send` + `Mail.ReadWrite` + `MailboxSettings.ReadWrite`**. The suite CAN send mail in any consented tenant via Graph `POST /users/<upn>/sendMail` — this is the IR victim-notification path (notifying victims during a mailbox takeover is a remediation action). There is NO separate mail app to provision and NO pending decision. Token-audience gotcha: `get-token.sh exchange-op` returns an **Exchange-Online**-audience token whose `roles` claim does NOT list Graph scopes; to call Graph, mint a **Graph**-audience token (`scope=https://graph.microsoft.com/.default`) — never conclude Mail.Send is "missing" from the wrong-audience token.

**ACG own-mail (`/mailbox`) is separate and working.** It uses the dedicated single-tenant **ComputerGuru Mailbox** app `1873b1b0-3377-485c-a848-bae9b2f8f1f5` (vault `msp-tools/computerguru-mailbox.sops.yaml`; `Mail.ReadWrite` + `Mail.Send` + `Contacts.ReadWrite`, azcomputerguru.com only), via `get-token.sh azcomputerguru.com mailbox`. Repointed off the dead `fabb3421` on 2026-06-17. Its SP is disabled when idle → a token 401 "account is disabled" means enable the SP first.

When searching customer admin portals for a service principal (role assignments, app role assignments, CA exclusions), search by the display name for that tier (e.g., "ComputerGuru Security Investigator").

## Per-tenant prerequisites

Graph API permissions alone are not enough. Most privileged operations require directory roles on the specific service principal *in that tenant*:

| Operation | App tier | Required directory role on that SP |
|---|---|---|
| Exchange REST read (Get-InboxRule, Get-Mailbox, Get-MailboxPermission, Get-RecipientPermission) | **`exchange-op`** (NOT `investigator-exo` — see callout) | Exchange Administrator |
| Exchange REST write (Set-Mailbox, Remove-InboxRule) | `exchange-op` | Exchange Administrator |
| Password reset, user property updates | `user-manager` | User Administrator |
| MFA method reset | `user-manager` | Authentication Administrator |
| Conditional Access reads/writes | `tenant-admin` | Conditional Access Administrator OR Security Administrator |
| Teams policies | `tenant-admin` | Teams Administrator |

> **[AUTHORITATIVE — stop re-deriving this] Exchange REST goes through `exchange-op`, NEVER `investigator-exo`.**
> The Exchange adminapi (`https://outlook.office365.com/adminapi/beta/{tenant}/InvokeCommand`) requires the
> calling app to hold the **`Exchange.ManageAsApp`** application permission (Office 365 Exchange Online app role
> `dc50a0fb-09a3-484d-be87-e023b12c6440`) **in addition to** the Exchange Administrator directory role. Only the
> **Exchange Operator** app (`exchange-op`, `b43e7342-5b4b-492f-890f-bb5a4f7f40e9`) has BOTH. The **Security
> Investigator** app (`investigator`/`investigator-exo`, `bfbc12a4-...`) has the Exchange Admin *role* but NOT
> `Exchange.ManageAsApp`, so **every** `investigator-exo` adminapi call returns **401** — including read-only
> `Get-*` cmdlets. Verified live on ACG (`azcomputerguru.com`) 2026-07-16: Investigator SP `Exchange.ManageAsApp`=**NO**,
> Operator SP=**YES**. **So use `exchange-op` for ALL Exchange cmdlets, read and write.**
>
> **401 vs 403:** `401` on adminapi = wrong app (missing `Exchange.ManageAsApp`) → you are on `investigator-exo`,
> switch to `exchange-op`. `403` = the `exchange-op` SP is missing the **Exchange Administrator** directory role on
> that specific tenant → assign it (steps below). Assigning Exchange Admin to the *Security Investigator* SP does
> **not** help — the missing piece there is the app-permission, which is baked into the app registration, not a
> per-tenant role. Authoritative ACG SP→role map (verified 2026-07-16): `exchange-op` SP `83c225f1-b38d-4063-9fdd-642b6b09ae8b`
> = Exchange Administrator (+ `Exchange.ManageAsApp`); `investigator` SP `9d242c15-6cd3-46ec-96d3-bcafaaaca333` = Exchange
> Administrator only (no ManageAsApp); `user-manager` SP `04020a4e-...` = Authentication Administrator + User Administrator;
> `tenant-admin` SP `046a7f70-...` = Conditional Access Administrator.

### How to assign a role to an SP in a customer tenant

1. Sign into the customer's Entra admin center as Global Admin:
   `https://entra.microsoft.com/#@{customer-domain}`
2. Identity -> Roles & admins -> All roles -> select the role (e.g., Exchange Administrator)
3. Add assignments -> search by the app display name (e.g., "ComputerGuru Security Investigator") -> Assign
   (Active, permanent — service principals cannot activate eligible assignments)

## Admin consent URLs

Each app must be individually consented in each customer tenant. Format:

```
https://login.microsoftonline.com/{tenant-id}/adminconsent?client_id={app-id}&redirect_uri=https://azcomputerguru.com&prompt=consent
```

**Security Investigator** (consent this first — needed for all breach checks):
```
https://login.microsoftonline.com/{TENANT_ID}/adminconsent?client_id=bfbc12a4-f0dd-4e12-b06d-997e7271e10c&redirect_uri=https://azcomputerguru.com&prompt=consent
```

**Exchange Operator** (consent when remediation scope is needed):
```
https://login.microsoftonline.com/{TENANT_ID}/adminconsent?client_id=b43e7342-5b4b-492f-890f-bb5a4f7f40e9&redirect_uri=https://azcomputerguru.com&prompt=consent
```

**User Manager**:
```
https://login.microsoftonline.com/{TENANT_ID}/adminconsent?client_id=64fac46b-8b44-41ad-93ee-7da03927576c&redirect_uri=https://azcomputerguru.com&prompt=consent
```

**Tenant Admin**:
```
https://login.microsoftonline.com/{TENANT_ID}/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
```

**Defender Add-on** (MDE-licensed tenants only — AADSTS650052 if no MDE license):
```
https://login.microsoftonline.com/{TENANT_ID}/adminconsent?client_id=dbf8ad1a-54f4-4bb8-8a9e-ea5b9634635b&redirect_uri=https://azcomputerguru.com&prompt=consent
```

The customer admin signs in as Global Admin, clicks Accept. Redirect lands on azcomputerguru.com — expected. Verify via `/servicePrincipals/{sp-id}/appRoleAssignments` (grants timestamped today confirm success).

## Diagnosing "required scopes are missing"

Token returned 403 with `"required scopes are missing in the token"`:

1. Decode the JWT payload (2nd segment, base64url) and check the `roles` claim.
2. If the expected scope is missing from `roles`:
   - Confirm the scope is in the app manifest in the home tenant (saved, not just selected).
   - Grant admin consent in the home tenant.
   - Re-run the customer admin consent URL above for that specific app.
3. If the scope IS in `roles` but you still get 403: check for a missing directory role (see table above).

## Diagnosing Exchange REST 403

- Wrong token scope: must request `https://outlook.office365.com/.default` (use `investigator-exo` or `exchange-op` tier, NOT `investigator`).
- Missing Exchange Administrator role on the specific SP in that tenant.
- Propagation delay: newly assigned role can take up to 15 minutes to reach Exchange Online. If just assigned, wait and retry.

## AADSTS650052 — service not licensed

If token request or API call returns AADSTS650052 referencing `WindowsDefenderATP` (`fc780465`): the tenant does not have an MDE license. Do not use the `defender` tier for this tenant. Security investigation proceeds with `investigator` + `investigator-exo` only.

## Common, benign "failures" in sign-in logs

- `error 50140` "Keep me signed in interrupt" — KMSI prompt, not a real failure.
- `error 65001` "has not consented to use the application" — fires during onboarding and before consent granted. If `appDisplayName` matches any ComputerGuru app, those are our own consent attempts, not attacker activity.
- `error 50126` from the sysadmin account during onboarding is typo/retry noise — check `ipAddress` against Mike's known IPs before flagging.

## Tenants where apps are consented (as of 2026-04-20)

| Tenant | Tenant ID | Sec Inv | Exch Op | User Mgr | Tenant Admin | Defender | Exch Admin (Sec Inv) | User Admin (User Mgr) | Auth Admin (User Mgr) | Notes |
|---|---|---|---|---|---|---|---|---|---|---|
| Valleywide Plastering | 5c53ae9f... | old app only | — | — | — | — | — | old app only | — | Needs migration to new app suite |
| Dataforth | 7dfa3ce8... | old app only | — | — | — | — | old app only | old app only | — | Needs migration |
| Cascades Tucson | 207fa277-e9d8-4eb7-ada1-1064d2221498 | old app only | — | — | — | — | old app only | old app only | — | IdentityRiskyUser scope still not consented as of 2026-04-16 |
| Grabblaw | 032b383e-96e4-491b-880d-3fd3295672c3 | YES (2026-04-20) | — | YES (2026-04-20) | YES (2026-04-20) | — | ASSIGNED (2026-04-20) | ASSIGNED (2026-04-20) | ASSIGNED (2026-04-20) | Fully onboarded |
| martylryan.com | (resolve via script) | YES (2026-04-20) | — | YES (old app) | YES (2026-04-20) | — | ASSIGNED (2026-04-20) | ASSIGNED (2026-04-20) | ASSIGNED (2026-04-20) | Fully onboarded |
| mvaninc.com | 5affaf1e-de89-416b-a655-1b2cf615d5b1 | YES (2026-04-21) | — | YES (2026-04-21) | YES (2026-04-21) | — | — | — | — | Fully onboarded. Incident 2026-04-21: sysadmin GA account unauthorized sign-in from OKC via device PRT (MITCH-LAPTOP/JUNE). Remediated: pw reset, sessions revoked. CA policy (MFA all users) still pending — Mike to create. |
| Quantum Wealth Management | 2fd0092b-e9b7-474c-ad73-301f34dd6b64 | YES (2026-05-27) | YES (2026-05-27) | YES (2026-05-27) | YES (2026-05-27) | YES (2026-05-27) | ASSIGNED (2026-05-27) | ASSIGNED (2026-05-27) | ASSIGNED (2026-05-27) | Fully onboarded via onboard-tenant.sh. NEW tenant (not the dormant GoDaddy one ddf3d2c9); quantumwms.com verified+primary; john@/sheila@ licensed. Intermedia->M365 migration in progress (Syncro #32323). |

**Migration note (now URGENT):** Valleywide, Dataforth, and Cascades were on the old app (`fabb3421`), which was DELETED 2026-06-14 — they currently have NO working remediation access. Migrate each: consent Security Investigator (+ Exchange Operator if write is needed) and assign the Exchange Administrator role to the new SP in that tenant.

Keep this table updated when rolling out to new tenants or migrating existing ones. Run `onboard-tenant.sh` after each consent and update the role columns from the script's final status output.
