Files
claudetools/.claude/skills/remediation-tool/scripts/patch-tenant-admin-manifest.sh
Mike Swanson cd50117aaf fix: remediation tool onboarding — add RoleManagement.ReadWrite.Directory + auto role assignment
Root cause: app-only Graph operations (password reset, Exchange REST) require
directory roles on each SP in the customer tenant, not just admin consent.
RoleManagement.ReadWrite.Directory was missing from all app manifests, making
role assignment impossible without manual portal work that was never being done.

Changes:
- patch-tenant-admin-manifest.sh: adds RoleManagement.ReadWrite.Directory to
  Tenant Admin app manifest via Management app, grants home-tenant consent
- onboard-tenant.sh: new script — resolves tenant, acquires Tenant Admin token,
  assigns Exchange Administrator to Security Investigator SP and User/Auth
  Administrator to User Manager SP; --dry-run supported; idempotent
- get-token.sh: detects AADSTS7000229, emits consent URL + onboard-tenant.sh
  reminder instead of silent failure
- gotchas.md: onboarding steps at top, tenant table expanded with role columns,
  all known tenants updated including martylryan.com (first fully onboarded)

Verified: martylryan.com fully onboarded, password reset to MLR2026!! succeeded

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 16:56:47 -07:00

173 lines
7.7 KiB
Bash

#!/usr/bin/env bash
# Patch the Tenant Admin app manifest to add RoleManagement.ReadWrite.Directory,
# then grant admin consent for that role in the ACG home tenant.
#
# Usage: patch-tenant-admin-manifest.sh
#
# Requirements:
# - Management app token (ACG home tenant) via SOPS vault
# - jq on PATH
# - curl on PATH
set -euo pipefail
ACG_HOME_TENANT="ce61461e-81a0-4c84-bb4a-7b354a9a356d"
MANAGEMENT_CLIENT_ID="0df4e185-4cf2-478c-a490-cc4ef36c6118"
MANAGEMENT_VAULT_PATH="msp-tools/computerguru-management.sops.yaml"
TENANT_ADMIN_APP_ID="709e6eed-0711-4875-9c44-2d3518c47063"
GRAPH_RESOURCE_APP_ID="00000003-0000-0000-c000-000000000000"
ROLE_MGMT_PERMISSION_ID="9e3f62cf-ca93-4989-b6ce-bf83c28f9fe8"
VAULT_ROOT=""
for candidate in "D:/vault" "$HOME/vault" "/d/vault"; do
[[ -d "$candidate" ]] && VAULT_ROOT="$candidate" && break
done
[[ -z "$VAULT_ROOT" ]] && { echo "[ERROR] SOPS vault not found (tried D:/vault ~/vault /d/vault)" >&2; exit 3; }
# ── Step 1: Get Management app client secret ──────────────────────────────────
echo "[INFO] Reading Management app secret from vault..."
CLIENT_SECRET=""
CLIENT_SECRET=$(bash "$VAULT_ROOT/scripts/vault.sh" get-field "$MANAGEMENT_VAULT_PATH" credentials.client_secret 2>/dev/null | tr -d '\r\n' || true)
if [[ -z "$CLIENT_SECRET" ]]; then
CLIENT_SECRET=$(bash "$VAULT_ROOT/scripts/vault.sh" get-field "$MANAGEMENT_VAULT_PATH" credentials.credential 2>/dev/null | tr -d '\r\n' || true)
fi
[[ -z "$CLIENT_SECRET" ]] && { echo "[ERROR] Could not read secret from $MANAGEMENT_VAULT_PATH" >&2; exit 4; }
echo "[OK] Management app secret retrieved"
# ── Step 2: Get Management app token (home tenant) ───────────────────────────
echo "[INFO] Acquiring Management app token for ACG home tenant..."
TOKEN_RESP=$(curl -s --max-time 15 -X POST \
"https://login.microsoftonline.com/${ACG_HOME_TENANT}/oauth2/v2.0/token" \
--data-urlencode "client_id=${MANAGEMENT_CLIENT_ID}" \
--data-urlencode "client_secret=${CLIENT_SECRET}" \
--data-urlencode "scope=https://graph.microsoft.com/.default" \
--data-urlencode "grant_type=client_credentials")
MGMT_TOKEN=$(echo "$TOKEN_RESP" | jq -r '.access_token // empty')
if [[ -z "$MGMT_TOKEN" ]]; then
echo "[ERROR] Failed to acquire Management app token" >&2
echo "$TOKEN_RESP" >&2
exit 5
fi
echo "[OK] Management app token acquired"
# ── Step 3: Get current Tenant Admin app object + requiredResourceAccess ──────
echo "[INFO] Fetching Tenant Admin app registration (appId=$TENANT_ADMIN_APP_ID)..."
APP_RESP=$(curl -s --max-time 15 \
-H "Authorization: Bearer $MGMT_TOKEN" \
-G \
--data-urlencode "\$filter=appId eq '$TENANT_ADMIN_APP_ID'" \
--data-urlencode "\$select=id,displayName,requiredResourceAccess" \
"https://graph.microsoft.com/v1.0/applications")
APP_OBJ_ID=$(echo "$APP_RESP" | jq -r '.value[0].id // empty')
APP_DISPLAY=$(echo "$APP_RESP" | jq -r '.value[0].displayName // empty')
if [[ -z "$APP_OBJ_ID" ]]; then
echo "[ERROR] Tenant Admin application not found (appId=$TENANT_ADMIN_APP_ID)" >&2
echo "Response: $APP_RESP" >&2
exit 6
fi
echo "[OK] Found app: $APP_DISPLAY (objectId=$APP_OBJ_ID)"
# ── Step 4: Check whether RoleManagement.ReadWrite.Directory already present ──
ALREADY_PRESENT=$(echo "$APP_RESP" | jq --arg pid "$ROLE_MGMT_PERMISSION_ID" \
'[.value[0].requiredResourceAccess[].resourceAccess[].id] | map(select(. == $pid)) | length > 0')
if [[ "$ALREADY_PRESENT" == "true" ]]; then
echo "[OK] RoleManagement.ReadWrite.Directory already present in manifest — no patch needed"
else
echo "[INFO] RoleManagement.ReadWrite.Directory not in manifest — patching..."
# Build updated requiredResourceAccess: inject new permission into the Graph entry
UPDATED_RRA=$(echo "$APP_RESP" | jq --arg gid "$GRAPH_RESOURCE_APP_ID" \
--arg pid "$ROLE_MGMT_PERMISSION_ID" '
.value[0].requiredResourceAccess
| map(
if .resourceAppId == $gid
then .resourceAccess += [{"id": $pid, "type": "Role"}]
else .
end
)
')
# PATCH the application
PATCH_RESP=$(curl -s --max-time 15 -o /dev/null -w "%{http_code}" -X PATCH \
-H "Authorization: Bearer $MGMT_TOKEN" \
-H "Content-Type: application/json" \
"https://graph.microsoft.com/v1.0/applications/$APP_OBJ_ID" \
-d "{\"requiredResourceAccess\": $UPDATED_RRA}")
if [[ "$PATCH_RESP" == "204" ]]; then
echo "[OK] App manifest patched (HTTP 204)"
else
echo "[ERROR] PATCH returned HTTP $PATCH_RESP" >&2
exit 7
fi
fi
# ── Step 5: Locate Tenant Admin SP and Graph SP in the home tenant ─────────────
echo "[INFO] Locating Tenant Admin service principal in home tenant..."
TA_SP_RESP=$(curl -s --max-time 15 \
-H "Authorization: Bearer $MGMT_TOKEN" \
-G \
--data-urlencode "\$filter=appId eq '$TENANT_ADMIN_APP_ID'" \
--data-urlencode "\$select=id,displayName" \
"https://graph.microsoft.com/v1.0/servicePrincipals")
TA_SP_OID=$(echo "$TA_SP_RESP" | jq -r '.value[0].id // empty')
[[ -z "$TA_SP_OID" ]] && { echo "[ERROR] Tenant Admin SP not found in home tenant" >&2; exit 8; }
echo "[OK] Tenant Admin SP: $TA_SP_OID"
echo "[INFO] Locating Microsoft Graph SP in home tenant..."
GRAPH_SP_RESP=$(curl -s --max-time 15 \
-H "Authorization: Bearer $MGMT_TOKEN" \
-G \
--data-urlencode "\$filter=appId eq '00000003-0000-0000-c000-000000000000'" \
--data-urlencode "\$select=id" \
"https://graph.microsoft.com/v1.0/servicePrincipals")
GRAPH_SP_OID=$(echo "$GRAPH_SP_RESP" | jq -r '.value[0].id // empty')
[[ -z "$GRAPH_SP_OID" ]] && { echo "[ERROR] Microsoft Graph SP not found in home tenant" >&2; exit 8; }
echo "[OK] Microsoft Graph SP: $GRAPH_SP_OID"
# ── Step 6: Check if appRoleAssignment already granted ────────────────────────
echo "[INFO] Checking existing appRoleAssignments for Tenant Admin SP..."
EXISTING_RESP=$(curl -s --max-time 15 \
-H "Authorization: Bearer $MGMT_TOKEN" \
"https://graph.microsoft.com/v1.0/servicePrincipals/$TA_SP_OID/appRoleAssignments")
ALREADY_GRANTED=$(echo "$EXISTING_RESP" | jq --arg rid "$ROLE_MGMT_PERMISSION_ID" \
'[.value[] | select(.appRoleId == $rid)] | length > 0')
if [[ "$ALREADY_GRANTED" == "true" ]]; then
echo "[OK] RoleManagement.ReadWrite.Directory appRoleAssignment already granted in home tenant — nothing to do"
else
echo "[INFO] Granting RoleManagement.ReadWrite.Directory appRoleAssignment in home tenant..."
GRANT_BODY=$(jq -n \
--arg principal "$TA_SP_OID" \
--arg resource "$GRAPH_SP_OID" \
--arg role "$ROLE_MGMT_PERMISSION_ID" \
'{"principalId": $principal, "resourceId": $resource, "appRoleId": $role}')
GRANT_RESP=$(curl -s --max-time 15 \
-H "Authorization: Bearer $MGMT_TOKEN" \
-H "Content-Type: application/json" \
-X POST \
"https://graph.microsoft.com/v1.0/servicePrincipals/$TA_SP_OID/appRoleAssignments" \
-d "$GRANT_BODY")
GRANT_ID=$(echo "$GRANT_RESP" | jq -r '.id // empty')
if [[ -z "$GRANT_ID" ]]; then
echo "[ERROR] Failed to grant appRoleAssignment" >&2
echo "$GRANT_RESP" >&2
exit 9
fi
echo "[OK] appRoleAssignment granted (id=$GRANT_ID)"
fi
echo ""
echo "[SUCCESS] Tenant Admin app manifest patched and home-tenant consent granted."
echo " RoleManagement.ReadWrite.Directory (id=$ROLE_MGMT_PERMISSION_ID) is now active."
echo ""
echo "[INFO] Next step: re-run admin consent in any customer tenants where Tenant Admin"
echo " is already consented, so the new permission is reflected in their service principal."
echo ""
echo " Consent URL pattern:"
echo " https://login.microsoftonline.com/{TENANT_ID}/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent"