# App permissions (authoritative) + SharePoint access

Purpose: stop the recurring "I can't do this / access denied" friction. The ComputerGuru
app suite has **broad, working access across M365 — Graph, Exchange Online, Defender, AND
SharePoint Online**. When a call fails, the cause is almost always one of: wrong tier, wrong
endpoint for the scope we hold, a not-consented tenant (AADSTS7000229), or the SharePoint
**secret-vs-cert** gotcha below — NOT a genuine lack of access.

**Discipline: before ever telling the user "the tool can't do X", verify against reality.**
Decode the token's `roles` claim and check it against this map:

```bash
TOK=$(bash scripts/get-token.sh <tenant> <tier>)
python - "$TOK" <<'PY'   # or: cut -d. -f2 | base64 -d | jq .roles
import sys,base64,json
p=sys.argv[1].split('.')[1]; p+='='*(-len(p)%4)
print(json.dumps(json.loads(base64.urlsafe_b64decode(p)).get('roles',[]),indent=2))
PY
```

## Live-verified application-permission map (decoded 2026-07-01, Birth Biologic tenant)

Application (app-only) roles actually present in each app's token. Fetch live to confirm for
a given tenant (consent can differ), but this is the baseline the apps are built to.

| Tier (get-token.sh) | App | Resource | Key application roles |
|---|---|---|---|
| `investigator` | Security Investigator | Graph | Directory.Read.All, User.Read.All, AuditLog.Read.All, Application.Read.All, Organization.Read.All, Policy.Read.All, **Sites.Read.All**, Mail.Read, MailboxSettings.Read, BitlockerKey.Read.All, IdentityRiskyUser.ReadWrite.All, IdentityRiskEvent.ReadWrite.All, IdentityRiskyServicePrincipal.ReadWrite.All, IdentityRiskyAgent.ReadWrite.All, UserAuthenticationMethod.Read.All |
| `investigator-exo` | Security Investigator | Exchange Online | EXO read (Get-InboxRule, Get-Mailbox, Get-*) |
| `exchange-op` | Exchange Operator | Exchange Online | EXO write (Set-Mailbox, Remove-InboxRule, revoke sessions) |
| `user-manager` | User Manager | Graph | **Directory.ReadWrite.All, Group.ReadWrite.All, User.ReadWrite.All**, Device.ReadWrite.All, User.RevokeSessions.All, UserAuthenticationMethod.ReadWrite.All, Organization.Read.All |
| `tenant-admin` | Tenant Admin | Graph | **Application.ReadWrite.All, AppRoleAssignment.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 |
| `tenant-admin` | Tenant Admin | **SharePoint Online** | **Sites.FullControl.All** (SharePoint resource `00000003-0000-0ff1-ce00-000000000000`) — the SharePoint tiers use this |
| `defender` | Defender Add-on | Defender ATP | MDE machine/alert actions (MDE-licensed tenants only) |
| `intune-manager` | Intune Manager | Graph | DeviceManagement* (Intune) |
| `mailbox` | ACG Mailbox | Graph | ACG-INTERNAL ONLY — Mail.ReadWrite/Send, Contacts.ReadWrite |

Notes:
- **CA policies ARE manageable** (tenant-admin holds `Policy.ReadWrite.ConditionalAccess` + the Conditional Access Administrator role) — report-only + break-glass exclusion first (see SKILL scope boundaries).
- **Directory settings** (M365 Group creation restriction, etc.) are writable via `user-manager` (Directory.ReadWrite.All) or `tenant-admin`.

## SharePoint Online — the two gotchas

1. **SharePoint app-only REQUIRES a certificate. A client_secret token is rejected** with
   `"Unsupported app only token"` on every SharePoint endpoint (REST `/_api/...` and CSOM
   `/_vti_bin/client.svc/ProcessQuery`). The Tenant Admin app has a cert in the vault
   (`cert_thumbprint_b64url` + `cert_private_key_pem_b64`); `get-token.sh` forces cert for the
   SharePoint tiers automatically. If you hand-roll a token, use the cert (client_assertion),
   not the secret.
2. **The Graph endpoint `GET /admin/sharepoint/settings` needs `SharePointTenantSettings.Read.All`,
   which NONE of the apps currently hold** — so that route returns `accessDenied`. That is NOT
   "no SharePoint access". Use the SharePoint **CSOM/REST admin API** (cert token, `Sites.FullControl.All`)
   instead — that path has full tenant-settings read/write. (If you'd rather use the Graph route,
   add `SharePointTenantSettings.Read.All`/`.ReadWrite.All` to the Tenant Admin app manifest and
   re-consent — `patch-tenant-admin-manifest.sh` + the tenant admin-consent URL.)

## SharePoint tiers (get-token.sh)

```bash
# Tenant admin resource ("<name>-admin.sharepoint.com") — tenant settings, site provisioning
TOK=$(bash scripts/get-token.sh <tenant> sharepoint-admin)
# Content resource ("<name>.sharepoint.com") — site/list/file operations
TOK=$(bash scripts/get-token.sh <tenant> sharepoint)
# Host is auto-resolved via Graph /sites/root; override if needed:
SP_RESOURCE_ENV=contoso.sharepoint.com bash scripts/get-token.sh <tenant> sharepoint-admin
```

### Read tenant settings (e.g. site-creation) via CSOM

```bash
ADMIN="https://<name>-admin.sharepoint.com"
TOK=$(bash scripts/get-token.sh <tenant> sharepoint-admin)
BODY='<Request xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="ACG"><Actions><ObjectPath Id="2" ObjectPathId="1"/><Query Id="3" ObjectPathId="1"><Query SelectAllProperties="true"><Properties/></Query></Query></Actions><ObjectPaths><Constructor Id="1" TypeId="{268004ae-ef6b-4e9b-8425-127220d84719}"/></ObjectPaths></Request>'
curl -s -X POST "$ADMIN/_vti_bin/client.svc/ProcessQuery" \
  -H "Authorization: Bearer $TOK" -H "Content-Type: text/xml" --data-binary "$BODY" \
  | jq -r '.[] | select(type=="object") | to_entries[]
           | select(.key|test("SiteCreation|SelfService|Sharing";"i")) | "\(.key) = \(.value)"'
```

Relevant tenant properties: `SelfServiceSiteCreationDisabled` (false = users CAN create sites),
`AllowClassicPublishingSiteCreation`, `SharingCapability`. Writing a setting is the same CSOM
surface with a `SetProperty` action (gate behind explicit YES, like any remediation write).

### Modern site creation = M365 Group creation

A modern team/communication site creates an M365 Group, gated by the Entra directory setting
`Group.Unified` -> `EnableGroupCreation` / `GroupCreationAllowedGroupId`. Read via Graph
`GET /groupSettings` (empty result = defaults = anyone can create). Write via `user-manager`
(Directory.ReadWrite.All): POST/PATCH `/groupSettings` using the `Group.Unified` template.
So fully restricting employee site creation = (a) `SelfServiceSiteCreationDisabled=true` (CSOM)
**and** (b) restrict `Group.Unified` group creation (Graph). Neither touches edit rights on an
existing site.

---
_First documented 2026-07-01 (Mike). Trigger: Syncro #32492 (Birth Biologic SharePoint site-creation
lockdown) surfaced the secret-vs-cert + no-SharePoint-tier gaps. Added `sharepoint`/`sharepoint-admin`
tiers to get-token.sh the same day._
