cascades: CA unblock + Phase B buildout + onboard-tenant.sh CA Admin backfill

Day-long session unblocking the Cascades CA reconciliation that was paused on
the Tenant Admin SP directory-role gap. Discovered Microsoft also tightened
the OAuth scope for /identity/conditionalAccess/* reads (Policy.Read.All now
required, Policy.ReadWrite.ConditionalAccess no longer accepted for reads).
Patched Tenant Admin manifest accordingly and re-consented in Cascades.

Phase B Intune state turned out to be far more built than the 4/20 log
suggested -- compliance policy, Wi-Fi, device restrictions, both SDM app
configs (Authenticator + Teams), and 7 of 8 apps were already deployed and
assigned. PATCHed device restrictions to block camera/Bluetooth/roaming
and enabled Managed Home Screen multi-app kiosk (ALIS + Teams visible,
10-min auto-signout). PATCHed Cascades named location to add primary WAN
(184.191.143.62/32). Howard added Outlook from Managed Play; SMB encryption
enabled on \CS-SERVER\homes.

CA bypass design corrected -- original §5 plan in user-account-rollout-plan.md
called for "block off-site + MFA on-site" which doesn't match the actual goal
of bypass when network + device assurance present. Reshaped to three policies
that produce on-site-compliant = password only, anything else = MFA or block.

onboard-tenant.sh patched to:
  1. Backfill Policy.Read.All on Tenant Admin SP if missing (idempotent --
     for tenants consented before the 2026-04-29 manifest update).
  2. Assign Conditional Access Administrator directory role to Tenant Admin
     SP at onboard time. Mirrors the Exchange Operator fix Mike landed in
     16f95e8.

Validated with --dry-run against Cascades. Customer-facing tenants already
onboarded should be re-run with this script to backfill both items.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-29 07:32:14 -07:00
parent a1209b28bb
commit a2f38c1038
2 changed files with 387 additions and 0 deletions

View File

@@ -44,6 +44,7 @@ DEFENDER_APP_ID="fc780465-2017-40d4-a0c5-307022471b92"
ROLE_EXCHANGE_ADMIN="29232cdf-9323-42fd-ade2-1d097af3e4de"
ROLE_USER_ADMIN="fe930be7-5e62-47db-91af-98c3a49a38b1"
ROLE_AUTH_ADMIN="c4e39bd9-1100-46d3-8c65-fb160da0071f"
ROLE_CA_ADMIN="b1be1c3e-b65d-4f19-8427-f6fa0d97feb9"
# ── Graph appRole GUIDs per app (from requiredResourceAccess in home tenant) ──
# Security Investigator — Graph
@@ -435,6 +436,29 @@ if [[ -z "$GRAPH_SP_OID" ]]; then
exit 1
fi
# ── Step 3.5: Backfill Tenant Admin SP — Policy.Read.All ─────────────────────
# Microsoft tightened LIST /identity/conditionalAccess/* to require Policy.Read.All
# (Policy.ReadWrite.ConditionalAccess no longer accepted for reads). Added to manifest
# 2026-04-29; tenants consented before that need backfill.
POLICY_READ_ALL_ID="246dd0d5-5bd0-4def-940b-0421030a5b68"
HAS_POLICY_READ=$(curl -s --max-time 15 \
-H "Authorization: Bearer $TENANT_ADMIN_TOKEN" \
"https://graph.microsoft.com/v1.0/servicePrincipals/$TA_SP_OID/appRoleAssignments" \
| jq --arg rid "$POLICY_READ_ALL_ID" '[.value[]? | select(.appRoleId == $rid)] | length > 0')
if [[ "$HAS_POLICY_READ" != "true" ]]; then
echo ""
echo "[INFO] Tenant Admin SP missing Policy.Read.All — backfilling..."
if [[ "$DRY_RUN" == "true" ]]; then
echo " [DRY-RUN] Would grant Policy.Read.All ($POLICY_READ_ALL_ID) to Tenant Admin SP"
elif grant_app_role "$TENANT_ADMIN_TOKEN" "$TA_SP_OID" "$GRAPH_SP_OID" "$POLICY_READ_ALL_ID"; then
echo " [OK] Policy.Read.All granted to Tenant Admin SP"
else
echo " [WARNING] Could not grant Policy.Read.All programmatically — customer re-consent may be needed"
echo " Re-consent URL: ${CONSENT_BASE}/${TENANT_ID}/adminconsent?client_id=${APP_TENANT_ADMIN}&redirect_uri=${CONSENT_REDIRECT}&prompt=consent"
fi
fi
# ── Step 4: Programmatic consent — create SPs + grant appRoleAssignments ──────
echo ""
echo "[INFO] Consenting app suite in tenant (programmatic — no customer click needed)..."
@@ -522,6 +546,36 @@ else
fi
fi
# Tenant Admin -> Conditional Access Administrator
# Required because Microsoft Graph evaluates SP membership in CA-relevant directory
# roles in addition to OAuth scopes when calling /identity/conditionalAccess/* endpoints.
# Without this role, the Tenant Admin SP gets 403 even with Policy.ReadWrite.ConditionalAccess
# in its token. Discovered Cascades 2026-04-28; backfilled here so future tenants don't trip on it.
echo ""
echo "[CHECK] Tenant Admin SP: $TA_SP_OID"
# Activate CA Admin role in tenant first (idempotent — returns 400 if already activated)
ACTIVATE_RESP=$(curl -s --max-time 15 \
-H "Authorization: Bearer $TENANT_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-X POST \
"https://graph.microsoft.com/v1.0/directoryRoles" \
-d "{\"roleTemplateId\":\"$ROLE_CA_ADMIN\"}" 2>/dev/null || true)
# Don't fail on activation — directly-assigned role POST handles the case where already active
IS_CA=$(role_assigned "$TENANT_ADMIN_TOKEN" "$TA_SP_OID" "$ROLE_CA_ADMIN")
if [[ "$IS_CA" == "true" ]]; then
echo " Conditional Access Administrator: PRESENT"
STATUS_MAP["Tenant Admin:Conditional Access Administrator"]="OK"
else
echo " Conditional Access Administrator: MISSING -> ASSIGNING..."
if assign_role "$TENANT_ADMIN_TOKEN" "$TA_SP_OID" "$ROLE_CA_ADMIN" "Conditional Access Administrator"; then
STATUS_MAP["Tenant Admin:Conditional Access Administrator"]=$( [[ "$DRY_RUN" == "true" ]] && echo "DRY-RUN" || echo "ASSIGNED" )
else
STATUS_MAP["Tenant Admin:Conditional Access Administrator"]="ERROR"
PARTIAL_FAILURE=true
fi
fi
# User Manager -> User Administrator + Authentication Administrator
if [[ -z "$USER_MGR_OID" ]]; then
echo "[WARNING] User Manager SP still not found after consent attempt"
@@ -573,6 +627,10 @@ else
fi
echo "SP roles status:"
TA_CA="${STATUS_MAP["Tenant Admin:Conditional Access Administrator"]:-SKIPPED}"
echo " Tenant Admin:"
printf " Conditional Access Administrator: %s\n" "[$TA_CA]"
SEC_EXCH="${STATUS_MAP["Security Investigator:Exchange Administrator"]:-SKIPPED}"
echo " Security Investigator:"
printf " Exchange Administrator: %s\n" "[$SEC_EXCH]"