sync: auto-sync from GURU-5070 at 2026-06-10 16:02:59

Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-10 16:02:59
This commit is contained in:
2026-06-10 16:03:11 -07:00
parent 4c436743d1
commit 15d582845f
7 changed files with 427 additions and 0 deletions

View File

@@ -0,0 +1,120 @@
---
name: onboard365
description: "Single-consent onboarding of a customer Microsoft 365 tenant to the ComputerGuru remediation app suite (Security Investigator / Exchange Operator / User Manager / Tenant Admin / Defender). The customer Global Admin clicks ONE admin-consent link (Tenant Admin); everything else — service principals, Graph/EXO/Defender permissions, and Entra directory roles — is provisioned automatically, no further clicks. Triggers: onboard 365, onboard a tenant, add tenant to remediation tools, single consent, consent link for new client, provision tenant apps, new M365 client onboarding, get a tenant ready for breach checks."
---
# Onboard365 — Single-Consent M365 Tenant Onboarding
Gets a customer M365 tenant ready for the `remediation-tool` suite with **one** customer
action: a single admin-consent click on the **ComputerGuru Tenant Admin** app. After that,
ACG provisions every other app and role programmatically using the Tenant Admin token — the
customer never sees five separate consent prompts.
This skill is a thin orchestrator. The provisioning logic, role GUIDs, and app IDs live in the
`remediation-tool` skill (`onboard-tenant.sh`) and are reused here so they never drift. Do NOT
duplicate that script — call it.
## Why "single consent"
Microsoft admin consent is per-application, so naively onboarding the 5-app suite would mean
5 customer clicks. We avoid that: the **Tenant Admin** app holds `Application.ReadWrite.All` +
`AppRoleAssignment.ReadWrite.All` + `RoleManagement.ReadWrite.Directory`. Once the customer
consents to Tenant Admin, our automation can, on their behalf:
1. Create the service principal for each other app (this IS admin consent for that app).
2. Grant every required Graph / Exchange Online / Defender app-role assignment.
3. Assign the required Entra directory roles to each SP.
Net: **one** customer click; the rest is `onboard-tenant.sh`.
## The flow
```
onboard365.sh <domain> # smart: prints the consent link if not yet consented,
# or provisions the whole suite if it is
onboard365.sh link <domain> # just print the single consent URL + customer instructions
onboard365.sh status <domain> # dry-run: show current consent/role state, change nothing
onboard365.sh provision <domain> # after the customer consents: provision all apps + roles
```
Script lives at `scripts/onboard365.sh` in this skill. It auto-locates the `remediation-tool`
scripts at `$HOME/.claude/skills/remediation-tool/scripts` (repo fallback via
`identity.json.claudetools_root`).
### Step-by-step (what to actually do)
1. **Identify the tenant.** Accept a domain (e.g. `acme.com`), an `.onmicrosoft.com`, or a
tenant GUID. Run `onboard365.sh link <domain>` to resolve it and produce the consent URL.
2. **Send the single link to the customer's Global Admin.** Use the template at
`references/customer-consent-instructions.md`. They sign in and click **Accept**. That is
the only thing they do. The app shown will be **"ComputerGuru Tenant Admin"**.
3. **Provision.** Once they confirm they accepted, run `onboard365.sh provision <domain>`
(or just `onboard365.sh <domain>` — it detects consent and proceeds). This runs
`onboard-tenant.sh`, which creates the other SPs, grants all permissions, and assigns the
directory roles. Watch the final status table — every row should be `OK` / `ASSIGNED`.
4. **Verify.** Re-run `onboard365.sh status <domain>` (dry-run). All roles should read
`PRESENT`. Optionally confirm an Exchange path with
`remediation-tool/scripts/assign-exchange-role.sh <domain> --verify`.
5. **Record it** (see Recording below).
## What gets provisioned (handled by onboard-tenant.sh — do not re-implement)
| App | Graph/EXO/Defender perms | Directory role assigned |
|---|---|---|
| Tenant Admin (consented by customer) | high-privilege Graph (incl. `Policy.Read.All` backfill) | Conditional Access Administrator |
| Security Investigator | Graph read + EXO read | Exchange Administrator |
| Exchange Operator | Graph + EXO write | Exchange Administrator |
| User Manager | Graph user/group/auth write | User Administrator + Authentication Administrator |
| Defender Add-on | Graph + Defender ATP | (Defender API; no directory role) — **MDE-licensed tenants only** |
The script auto-detects MDE licensing: if the Defender ATP resource SP isn't present, it
skips Defender cleanly (not an error).
## Recording (durable — do this after a successful provision)
The script provisions but does NOT write your records. After a clean run:
1. **Tenant registry — edit the REPO copy** (so it persists + syncs to the fleet), not the
applied global copy. Path: `$CLAUDETOOLS_ROOT/.claude/skills/remediation-tool/references/tenants.md`
(resolve `$CLAUDETOOLS_ROOT` from `identity.json`). Set the tenant's **Onboarded** column to
`YES` and add a dated Notes line listing what was consented + roles assigned. If the tenant
isn't in the table yet, add a row (Display Name | Domain | Tenant ID | Onboarded | Notes).
2. **Client wiki / CONTEXT.** If `wiki/clients/<slug>.md` exists, note tenant onboarding under
Cloud/M365 (tenant ID, "remediation suite onboarded YYYY-MM-DD, all apps + roles").
3. Use UTC dates.
## Conventions & guardrails
- **Outward-facing action.** Sending a consent link to a customer and provisioning apps in
their tenant is customer-facing. Confirm the target tenant with the user before generating
the link, and again before running `provision` (the Tenant Admin grant is high-privilege).
- **One link only.** Do not send the customer the per-app consent URLs unless `provision`
reports that a specific app failed programmatic consent — then `onboard-tenant.sh` prints the
fallback per-app URLs. The whole point is a single click.
- **Idempotent.** Re-running `provision` on an already-onboarded tenant is safe — every grant
and role assignment checks-before-creating and treats "already exists" as success.
- **Vault.** Token acquisition uses the SOPS vault via `identity.json.vault_path`. On a machine
where the skill resolves the wrong identity.json (no `vault_path`), export
`VAULT_ROOT_ENV=<vault path>` before running (known remediation-tool quirk).
- **Break-glass / least privilege.** This skill only provisions the standing app suite. It does
NOT touch customer user accounts, CA policies, or break-glass accounts — that's
`remediation-tool` territory.
## Common results / troubleshooting
- `[WARNING] Tenant Admin app not yet consented` (exit 2): the customer hasn't accepted yet, or
accepted as a non-Global-Admin. Re-send the link; confirm they're a Global Administrator.
- `AADSTS7000229`: the SP isn't in the tenant — same as not-consented; resend the link.
- A role row shows `ERROR`: usually transient Graph replication. Re-run `provision` once; if it
persists, the customer may need to re-accept the Tenant Admin consent (the script prints the
re-consent URL).
- `vault_path not set`: export `VAULT_ROOT_ENV` (see Vault above).
- Exchange tasks 403 later despite onboarding: run
`remediation-tool/scripts/assign-exchange-role.sh <domain>` — the Exchange Administrator role
on the Exchange Operator SP is the recurring gap; onboarding assigns it, but verify.
## Relationship to remediation-tool
Onboard365 = the front door (get a tenant consented + provisioned). `remediation-tool` = the
work (breach checks, sweeps, mailbox/user/CA remediation) once the tenant is onboarded. After a
successful onboard, a breach check is `remediation-tool` territory.

View File

@@ -0,0 +1,40 @@
# Customer Consent Instructions (template)
Use this when handing the single consent link to a customer's Global Administrator.
Fill in `{{CUSTOMER}}`, `{{CONSENT_URL}}` (from `onboard365.sh link <domain>`), and your name.
---
**Subject:** Action needed: one-click approval to connect Arizona Computer Guru to your Microsoft 365
Hi {{ADMIN_NAME}},
To manage and protect your Microsoft 365 environment, we need a one-time approval from a
Microsoft 365 **Global Administrator** at {{CUSTOMER}}. This is a single click — you won't need
to approve anything else after this.
1. Open this link while signed in as a Global Administrator:
{{CONSENT_URL}}
2. Review the screen (it will show **"ComputerGuru Tenant Admin"**) and click **Accept**.
That's it. Once you've accepted, reply to let us know and we'll finish the setup on our end.
Thanks,
{{TECH_NAME}}
Arizona Computer Guru · 520.304.8300
---
## Notes for the tech (not for the customer)
- The approver MUST be a **Global Administrator**. A User Admin / other role cannot grant
application admin consent — the Accept will fail or be greyed out.
- The single grant is for **ComputerGuru Tenant Admin** only. After they accept, run
`onboard365.sh provision <domain>` — that creates the other app SPs and assigns roles with no
further customer interaction.
- If they report an error like "Need admin approval" / "AADSTS650056" / "AADSTS7000229" on the
link, they almost always signed in with a non-GA account. Have them retry as a GA.
- Don't send the per-app links. One link is the whole point. Per-app fallback URLs only come into
play if `onboard-tenant.sh` reports a specific app failed programmatic consent.

View File

@@ -0,0 +1,113 @@
#!/usr/bin/env bash
# Onboard365 — single-consent onboarding of a customer M365 tenant to the
# ComputerGuru remediation app suite.
#
# The customer Global Admin consents ONCE to the "ComputerGuru Tenant Admin" app.
# Everything else (the other SPs, their Graph/EXO/Defender permissions, and the
# Entra directory roles) is provisioned programmatically by the reused
# remediation-tool/onboard-tenant.sh — no further customer clicks.
#
# Usage:
# onboard365.sh <domain|tenant-id> # smart: link if not consented, else provision
# onboard365.sh link <domain|tenant-id> # print the ONE consent URL + customer steps
# onboard365.sh status <domain|tenant-id> # dry-run: show current consent/role state
# onboard365.sh provision <domain|tenant-id> # after consent: provision all apps + roles
#
# Exit codes mirror onboard-tenant.sh for provision/status (0 ok, 2 not consented,
# 10 partial). link always exits 0.
set -euo pipefail
TENANT_ADMIN_APPID="709e6eed-0711-4875-9c44-2d3518c47063"
CONSENT_BASE="https://login.microsoftonline.com"
CONSENT_REDIRECT="https://azcomputerguru.com"
# ── Locate the reused remediation-tool scripts ────────────────────────────────
# Prefer the applied global copy (stable path on every fleet machine); fall back
# to the repo copy via identity.json.claudetools_root.
find_rtool() {
local cands=("$HOME/.claude/skills/remediation-tool/scripts")
local idf="$HOME/.claude/identity.json"
if [[ -f "$idf" ]] && command -v jq >/dev/null 2>&1; then
local root
root=$(jq -r '.claudetools_root // empty' "$idf" 2>/dev/null || true)
[[ -n "$root" ]] && cands+=("$root/.claude/skills/remediation-tool/scripts")
fi
local c
for c in "${cands[@]}"; do
[[ -f "$c/onboard-tenant.sh" ]] && { echo "$c"; return 0; }
done
return 1
}
RT="$(find_rtool)" || {
echo "[ERROR] remediation-tool scripts not found." >&2
echo " Expected: \$HOME/.claude/skills/remediation-tool/scripts/onboard-tenant.sh" >&2
echo " Run a repo sync, or check identity.json.claudetools_root." >&2
exit 3
}
# ── Parse args (allow a bare domain as smart mode) ────────────────────────────
SUB="${1:-}"
[[ -z "$SUB" ]] && { echo "usage: onboard365.sh <link|status|provision|auto> <domain|tenant-id>" >&2; exit 64; }
case "$SUB" in
link|status|provision|auto)
TARGET="${2:-}"
[[ -z "$TARGET" ]] && { echo "usage: onboard365.sh $SUB <domain|tenant-id>" >&2; exit 64; }
;;
*)
TARGET="$SUB"; SUB="auto"
;;
esac
resolve() { "$RT/resolve-tenant.sh" "$1" 2>/dev/null || echo "$1"; }
print_link() {
local t="$1"
cat <<EOF
============================================================
Onboard365 — Single-Consent Link
Customer: $TARGET
Tenant: $t
============================================================
Send the ONE link below to the customer's GLOBAL ADMIN. They sign
in and click Accept. That single consent is all they do — ACG
provisions everything else automatically.
${CONSENT_BASE}/${t}/adminconsent?client_id=${TENANT_ADMIN_APPID}&redirect_uri=${CONSENT_REDIRECT}&prompt=consent
App they will see: "ComputerGuru Tenant Admin"
(Customer instructions template: references/customer-consent-instructions.md)
After they confirm Accept, run:
onboard365.sh provision $TARGET
============================================================
EOF
}
# returns 0 if Tenant Admin token acquires (consented), 1 otherwise
is_consented() { "$RT/get-token.sh" "$1" tenant-admin >/dev/null 2>/tmp/onboard365-tok.err; }
case "$SUB" in
link)
print_link "$(resolve "$TARGET")"
;;
status)
echo "[INFO] Onboard365 status (dry-run) for $TARGET — no changes will be made"
"$RT/onboard-tenant.sh" "$TARGET" --dry-run
;;
provision)
echo "[INFO] Onboard365 provisioning suite for $TARGET (single-consent model)"
"$RT/onboard-tenant.sh" "$TARGET"
;;
auto)
T="$(resolve "$TARGET")"
if is_consented "$T"; then
echo "[INFO] Tenant Admin already consented for $TARGET — provisioning the suite..."
"$RT/onboard-tenant.sh" "$TARGET"
else
echo "[INFO] Tenant Admin not yet consented for $TARGET — generating the single consent link."
print_link "$T"
exit 2
fi
;;
esac