# ComputerGuru M365 App Suite — Authoritative Reference

**This is the single source of truth for the ComputerGuru multi-tenant app suite: every app,
its App ID, its ACTUAL granted permissions, the get-token.sh tiers, and — the part that keeps
biting us — how per-tenant consent differs and how to audit + fix a tenant that only has
PARTIAL consent.**

Live-verified 2026-07-02 against Birth Biologic (fully-consented reference tenant). Anything
here is re-checkable with the commands below; when a call fails, verify against reality before
concluding "the tool can't do it."

---

## The apps

All are **multi-tenant** app registrations owned in the ACG home tenant (azcomputerguru.com).
A customer tenant gets a **service principal** for each app when it consents. The App ID is the
same everywhere; the SP object id differs per tenant.

| App (display name) | App ID | Auth | Vault file (in `msp-tools/`) |
|---|---|---|---|
| ComputerGuru - Security Investigator | `bfbc12a4-f0dd-4e12-b06d-997e7271e10c` | secret | `computerguru-security-investigator.sops.yaml` |
| ComputerGuru - Exchange Operator | `b43e7342-5b4b-492f-890f-bb5a4f7f40e9` | secret | `computerguru-exchange-operator.sops.yaml` |
| ComputerGuru - User Manager | `64fac46b-8b44-41ad-93ee-7da03927576c` | secret | `computerguru-user-manager.sops.yaml` |
| ComputerGuru - Tenant Admin | `709e6eed-0711-4875-9c44-2d3518c47063` | secret (Graph/EXO) **+ CERT (SharePoint)** | `computerguru-tenant-admin.sops.yaml` |
| ComputerGuru - Defender Add-on | `dbf8ad1a-54f4-4bb8-8a9e-ea5b9634635b` | secret | `computerguru-defender-addon.sops.yaml` |
| ComputerGuru - Intune Manager | `46986910-aa47-4e5e-b596-f65c6b485abb` | secret | `computerguru-intune-manager.sops.yaml` |
| ComputerGuru - Mailbox (ACG-INTERNAL ONLY) | `1873b1b0-3377-485c-a848-bae9b2f8f1f5` | CERT | `computerguru-mailbox.sops.yaml` |

The **Mailbox** app is single-tenant (azcomputerguru.com only) — it is NOT part of the customer
suite; it 401s on any other tenant.

---

## get-token.sh tiers (tier -> app -> resource)

`bash scripts/get-token.sh <tenant> <tier>` -> a bearer token on stdout (cached 55 min in
`/tmp/remediation-tool/<tenant>/<tier>.jwt`; delete that file to force a fresh mint after a
consent change).

| Tier | App | Audience / resource | Auth |
|---|---|---|---|
| `investigator` | Security Investigator | Graph (`https://graph.microsoft.com`) | secret |
| `investigator-exo` | Security Investigator | Exchange Online (`https://outlook.office365.com`) | secret |
| `exchange-op` | Exchange Operator | Exchange Online | secret |
| `exchange-op-graph` | Exchange Operator | Graph (added 2026-07-01) | secret |
| `user-manager` | User Manager | Graph | secret |
| `tenant-admin` | Tenant Admin | Graph | secret |
| `tenant-admin-onboard` | Tenant Admin | Graph (onboarding alias) | secret |
| `sharepoint` | Tenant Admin | SharePoint content (`<name>.sharepoint.com`) | **CERT** |
| `sharepoint-admin` | Tenant Admin | SharePoint admin (`<name>-admin.sharepoint.com`) | **CERT** |
| `defender` | Defender Add-on | Defender ATP (`https://api.securitycenter.microsoft.com`) | secret |
| `intune-manager` | Intune Manager | Graph | secret |
| `mailbox` | Mailbox (internal) | Graph | CERT |

SharePoint host is auto-resolved via Graph `/sites/root`; override with
`SP_RESOURCE_ENV=<name>-admin.sharepoint.com` (needed when the app lacks the Graph Sites scope
to self-resolve — e.g. mid-onboarding).

---

## Live-verified permissions (application/app-only roles)

Decoded from real tokens on Birth Biologic (full consent). These are the roles each app is
BUILT to hold. **A given tenant may hold a SUBSET** — see the audit section.

### Graph roles

| App / tier | Graph application roles |
|---|---|
| `investigator` | Application.Read.All, AuditLog.Read.All, BitlockerKey.Read.All, Directory.Read.All, IdentityRiskEvent.ReadWrite.All, IdentityRiskyAgent.ReadWrite.All, IdentityRiskyServicePrincipal.ReadWrite.All, IdentityRiskyUser.Read.All, IdentityRiskyUser.ReadWrite.All, Mail.Read, MailboxSettings.Read, Organization.Read.All, Policy.Read.All, **Sites.Read.All**, User.Read.All, UserAuthenticationMethod.Read.All |
| `exchange-op-graph` | **Mail.ReadWrite, MailboxSettings.ReadWrite**, Organization.Read.All, User.Read.All, User.RevokeSessions.All |
| `user-manager` | Device.ReadWrite.All, **Directory.ReadWrite.All, Group.ReadWrite.All, User.ReadWrite.All**, Organization.Read.All, User.RevokeSessions.All, UserAuthenticationMethod.ReadWrite.All |
| `tenant-admin` | **AppRoleAssignment.ReadWrite.All, Application.ReadWrite.All, RoleManagement.ReadWrite.Directory, Directory.ReadWrite.All, Policy.ReadWrite.ConditionalAccess, Sites.FullControl.All, Sites.ReadWrite.All**, User.ReadWrite.All, SecurityEvents.Read.All, Policy.Read.All, UserAuthenticationMethod.ReadWrite.All |

### SharePoint Online roles (resource `00000003-0000-0ff1-ce00-000000000000`, CERT auth)

| Tier | SharePoint application roles |
|---|---|
| `sharepoint` / `sharepoint-admin` | **Sites.FullControl.All** |

### Exchange Online (app-only, EXO audience — role, not a Graph `roles` claim)

EXO app-only access is via the **Exchange Administrator directory role** assigned to the SP PLUS
`full_access_as_app` + `Exchange.ManageAsApp` app permissions. It does NOT show in a Graph
`roles` decode — it's an EXO RBAC assignment.

| Tier | EXO capability |
|---|---|
| `investigator-exo` | EXO **read** (Get-Mailbox, Get-InboxRule, Get-*) — needs Exchange Admin role on the Investigator SP |
| `exchange-op` | EXO **write / all-access** (Set-Mailbox, New/Remove-InboxRule, Add-MailboxPermission, InvokeCommand, move mail) — Exchange Admin role + full_access_as_app + Exchange.ManageAsApp on the Exchange Operator SP. This is the all-access mail tier; do NOT claim "no tier can write mail." |

---

## THE THING THAT KEEPS BITING US: per-tenant consent is not uniform

Every failure of the form "we can't do X on tenant Y" has been one of these, NOT a missing
capability:

1. **The app is consented but with an OLD/PARTIAL permission set.** A tenant onboarded before a
   scope was added to the app manifest keeps the old grant until re-consented. Example: **VWP
   (Valley Wide Plastering, `5c53ae9f-7071-4248-b834-8685b646450f`) had the Tenant Admin app but
   NO SharePoint `Sites.FullControl.All`** — every SharePoint call 401'd with a valid-looking
   token (`aud` = SharePoint, `roles` = empty). Fixed 2026-07-02 (see below).
2. **The SharePoint secret-vs-cert gotcha.** SharePoint app-only REJECTS a client_secret token
   ("Unsupported app only token"). `get-token.sh` forces cert for the `sharepoint*` tiers; if you
   hand-roll, use the cert.
3. **Not consented at all** (AADSTS7000229 / AADSTS700016) — the tenant never onboarded the app.
   Use the admin-consent URL.
4. **EXO role not assigned** — the app is consented but the SP lacks the Exchange Admin directory
   role (EXO calls 403). Fix with `assign-exchange-role.sh`.

**Discipline: before telling the user "the tool can't do it", AUDIT the tenant.**

### Audit a tenant's real consent state

**Just run the audit command** — it decodes every app's actual grants, diffs against this
baseline, grades GREEN/AMBER/RED, and prints the exact fix for each gap:

```bash
bash scripts/consent-audit.sh <domain|tenant-guid>     # one tenant, full detail + fixes
bash scripts/consent-audit.sh --all                    # every tenant in references/tenants.md
```

Run it at the START of any tenant task (or on a schedule) so a partial-consent gap surfaces
up front, not as a 401 mid-task. It checks: investigator / exchange-op / user-manager /
tenant-admin Graph roles, the SharePoint app-only `Sites.FullControl.All` (cert), and the
EXO Exchange Administrator role — the full set that has ever caused a "can't do X" failure.

Manual equivalent (what the script does), if you want to eyeball one tier:

```bash
TEN=<tenant-guid>
for tier in investigator exchange-op-graph user-manager tenant-admin; do
  echo "== $tier =="
  rm -f /tmp/remediation-tool/$TEN/$tier.jwt
  T=$(bash scripts/get-token.sh $TEN $tier 2>/dev/null | tail -1)
  python - "$T" <<'PY'
import sys,base64,json
c=json.loads(base64.urlsafe_b64decode((lambda p:p+'='*(-len(p)%4))(sys.argv[1].split('.')[1])))
print("  roles:", ", ".join(sorted(c.get("roles",[]))) or "(NONE — not consented / partial)")
PY
done
# SharePoint (cert): roles should include Sites.FullControl.All
SP_RESOURCE_ENV=<name>-admin.sharepoint.com bash scripts/get-token.sh $TEN sharepoint-admin \
  | tail -1 | cut -d. -f2 | ...decode roles...
```

An **empty `roles`** on a token whose `aud` is correct = the app is present but that permission
set was never granted on this tenant. That is the signal to fix consent, NOT to give up.

### Fix partial consent — two methods

**Method A — re-consent the whole app manifest (preferred, grants everything the app requests).**
A tenant Global Admin clicks:
```
https://login.microsoftonline.com/<tenant>/adminconsent?client_id=<app-id>
```
Grants ALL of the app's requiredResourceAccess. NOTE (VWP lesson): this reliably grants the
**Graph** permissions but the **SharePoint** app-only role sometimes does NOT attach from the
consent flow — verify with the audit above and use Method B for the leftover.

**Method B — grant a specific missing app role directly (no user interaction; needs the
tenant-admin token, which holds AppRoleAssignment.ReadWrite.All).** This is how VWP's missing
SharePoint role was fixed:
```bash
TEN=<tenant>; TA=$(bash scripts/get-token.sh $TEN tenant-admin | tail -1); G=https://graph.microsoft.com/v1.0
# the SP receiving the role (e.g. Tenant Admin app):
RECIP=$(curl -s -G "$G/servicePrincipals" --data-urlencode "\$filter=appId eq '<app-id>'" -H "Authorization: Bearer $TA" | jq -r '.value[0].id')
# the resource SP that DEFINES the role (SharePoint = 00000003-0000-0ff1-ce00-...):
RES=$(curl -s -G "$G/servicePrincipals" --data-urlencode "\$filter=appId eq '00000003-0000-0ff1-ce00-000000000000'" --data-urlencode "\$select=id,appRoles" -H "Authorization: Bearer $TA")
RESID=$(echo "$RES" | jq -r '.value[0].id')
ROLE=$(echo "$RES" | jq -r '.value[0].appRoles[]|select(.value=="Sites.FullControl.All").id')
# grant it:
curl -s -X POST "$G/servicePrincipals/$RECIP/appRoleAssignments" -H "Authorization: Bearer $TA" \
  -H "Content-Type: application/json" \
  -d "{\"principalId\":\"$RECIP\",\"resourceId\":\"$RESID\",\"appRoleId\":\"$ROLE\"}"
```
Only use Method B to complete an intent the customer already consented to (they clicked the
consent link / authorized the access). It propagates to a fresh token within seconds.

**Fix a missing EXO role:** `bash scripts/assign-exchange-role.sh <domain>` (idempotent;
`--all --verify` to audit the fleet). EXO propagation is 15-60 min.

---

## Per-tenant onboarding status (living matrix — update as verified)

Legend: F = full consent verified · P = partial (note the gap) · ? = unverified.

| Tenant | Guid | investigator | exchange-op | user-manager | tenant-admin (Graph) | SharePoint | Notes |
|---|---|---|---|---|---|---|---|
| Birth Biologic | 19a568e8-… | F | F | F | F | F | reference/baseline |
| Valley Wide Plastering (VWP) | 5c53ae9f-… | ? | F (JIT-proven) | F | F (Graph Sites added 2026-07-02) | **F (fixed 2026-07-02 via Method B)** | was the "old app only" gap |
| Cascades of Tucson | (resolve) | ? | ? | ? | ? | ? | flagged "old app only" — audit |
| Dataforth | (resolve) | ? | ? | ? | ? | ? | flagged "old app only" — audit |

Keep this matrix current — it is the answer to "is tenant X onboarded?" that we keep not having.

---

## Gotchas index

- **SharePoint app-only = CERT, not secret** (Unsupported app only token). `get-token.sh`
  forces cert for `sharepoint*`.
- **Graph `GET /admin/sharepoint/settings` 403** — no app holds `SharePointTenantSettings.*`.
  Read/write SP tenant settings via the CSOM/REST admin API (`sharepoint-admin`, Sites.FullControl.All).
- **adminconsent grants Graph but may skip the SharePoint app role** — verify + Method-B the gap.
- **JIT Privileged Auth Admin can't self-remove** — reset-password leaves standing PAA on the
  Tenant Admin SP; a human GA removes it (or use PIM / a second principal).
- **EXO role propagation 15-60 min**; verify membership via
  `roleManagement/directory/roleAssignments` (not the laggy directoryRoles/members list).
- **Mailbox app is ACG-internal only** — never expect it on a customer tenant.

Adding a scope to an app manifest is a portal action (`patch-tenant-admin-manifest.sh` +
adminconsent); the multi-tenant app registrations themselves are managed manually, not via the tool.
