Synced files: - Quote wizard frontend (all components, hooks, types, config) - API updates (config, models, routers, schemas, services) - Client work (bg-builders, gurushow) - Scripts (BGB Lesley termination, CIPP, Datto, migration) - Temp files (Bardach contacts, VWP investigation, misc) - Credentials and session logs - Email service, PHP API, session logs Machine: ACG-M-L5090 Timestamp: 2026-03-10 19:11:00 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
189 lines
8.7 KiB
PowerShell
189 lines
8.7 KiB
PowerShell
# Claude-MSP-Access - Automated Tenant Onboarding
|
|
# Onboards a customer tenant with full Claude + CIPP permissions
|
|
# No manual intervention required after initial admin consent
|
|
#
|
|
# Usage: .\claude-msp-onboard-tenant.ps1 -TenantDomain "sonorangreenllc.com"
|
|
#
|
|
# Prerequisites: Admin consent URL must be clicked first by customer/sysadmin:
|
|
# https://login.microsoftonline.com/common/adminconsent?client_id=fabb3421-8b34-484b-bc17-e46de9703418&redirect_uri=https://login.microsoftonline.com/common/oauth2/nativeclient
|
|
#
|
|
# What this script does after consent:
|
|
# 1. Finds the Claude-MSP-Access service principal in the customer tenant
|
|
# 2. Activates Exchange Administrator directory role (if not active)
|
|
# 3. Assigns Exchange Administrator to Claude's SP (via CIPP Graph proxy)
|
|
# 4. Verifies all access: Graph, Exchange, Mail, Security, Intune
|
|
|
|
param(
|
|
[Parameter(Mandatory=$true)]
|
|
[string]$TenantDomain
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
# --- Credentials ---
|
|
$cippUrl = "https://cippcanvb.azurewebsites.net"
|
|
$cippTenantId = "ce61461e-81a0-4c84-bb4a-7b354a9a356d"
|
|
$cippClientId = "420cb849-542d-4374-9cb2-3d8ae0e1835b"
|
|
$cippSecret = "MOn8Q~otmxJPLvmL~_aCVTV8Va4t4~SrYrukGbJT"
|
|
|
|
$claudeAppId = "fabb3421-8b34-484b-bc17-e46de9703418"
|
|
$claudeSecret = "~QJ8Q~NyQSs4OcGqHZyPrA2CVnq9KBfKiimntbMO"
|
|
|
|
Write-Output "========================================="
|
|
Write-Output " Claude-MSP-Access - Tenant Onboarding"
|
|
Write-Output " Tenant: $TenantDomain"
|
|
Write-Output " $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
|
|
Write-Output "========================================="
|
|
|
|
# --- STEP 1: Get CIPP API token ---
|
|
Write-Output "`n[STEP 1] Getting CIPP API token..."
|
|
$tokenBody = @{
|
|
client_id = $cippClientId
|
|
client_secret = $cippSecret
|
|
scope = "api://$cippClientId/.default"
|
|
grant_type = "client_credentials"
|
|
}
|
|
$cippToken = (Invoke-RestMethod -Uri "https://login.microsoftonline.com/$cippTenantId/oauth2/v2.0/token" -Method POST -Body $tokenBody).access_token
|
|
$cippHeaders = @{ "Authorization" = "Bearer $cippToken" }
|
|
Write-Output "[OK] CIPP token acquired"
|
|
|
|
# --- STEP 2: Find Claude SP in customer tenant via CIPP ---
|
|
Write-Output "`n[STEP 2] Finding Claude-MSP-Access service principal..."
|
|
$spFilter = [uri]::EscapeDataString("appId eq '$claudeAppId'")
|
|
$spResult = Invoke-RestMethod -Uri "$cippUrl/api/ListGraphRequest?TenantFilter=$TenantDomain&Endpoint=servicePrincipals&`$filter=$spFilter" -Headers $cippHeaders
|
|
$sp = $spResult.Results | Select-Object -First 1
|
|
|
|
if (-not $sp) {
|
|
Write-Output "[ERROR] Claude-MSP-Access SP not found in $TenantDomain"
|
|
Write-Output "[INFO] Has admin consent been completed? Use this URL:"
|
|
Write-Output " https://login.microsoftonline.com/common/adminconsent?client_id=$claudeAppId&redirect_uri=https://login.microsoftonline.com/common/oauth2/nativeclient"
|
|
exit 1
|
|
}
|
|
$spId = $sp.id
|
|
Write-Output "[OK] Found SP: $($sp.displayName) (ID: $spId)"
|
|
|
|
# --- STEP 3: Get Exchange Administrator role ID ---
|
|
Write-Output "`n[STEP 3] Finding Exchange Administrator role..."
|
|
$rolesResult = Invoke-RestMethod -Uri "$cippUrl/api/ListGraphRequest?TenantFilter=$TenantDomain&Endpoint=directoryRoles" -Headers $cippHeaders
|
|
$exoRole = $rolesResult.Results | Where-Object { $_.displayName -eq "Exchange Administrator" }
|
|
|
|
if (-not $exoRole) {
|
|
Write-Output "[INFO] Exchange Admin role not activated, activating from template..."
|
|
# Exchange Administrator role template ID is always 29232cdf-9323-42fd-ade2-1d097af3e4de
|
|
$activateBody = [uri]::EscapeDataString((@{ roleTemplateId = "29232cdf-9323-42fd-ade2-1d097af3e4de" } | ConvertTo-Json -Compress))
|
|
$activateResult = Invoke-RestMethod -Uri "$cippUrl/api/ListGraphRequest?TenantFilter=$TenantDomain&Endpoint=directoryRoles&type=POST&body=$activateBody" -Headers $cippHeaders
|
|
|
|
# Re-fetch roles
|
|
$rolesResult = Invoke-RestMethod -Uri "$cippUrl/api/ListGraphRequest?TenantFilter=$TenantDomain&Endpoint=directoryRoles" -Headers $cippHeaders
|
|
$exoRole = $rolesResult.Results | Where-Object { $_.displayName -eq "Exchange Administrator" }
|
|
}
|
|
|
|
if (-not $exoRole) {
|
|
Write-Output "[ERROR] Could not find or activate Exchange Administrator role"
|
|
exit 1
|
|
}
|
|
$exoRoleId = $exoRole.id
|
|
Write-Output "[OK] Exchange Admin role: $exoRoleId"
|
|
|
|
# --- STEP 4: Assign Exchange Administrator to Claude SP ---
|
|
Write-Output "`n[STEP 4] Assigning Exchange Administrator role..."
|
|
$assignEndpoint = [uri]::EscapeDataString("directoryRoles/$exoRoleId/members/`$ref")
|
|
$assignBody = [uri]::EscapeDataString((@{ "@odata.id" = "https://graph.microsoft.com/v1.0/servicePrincipals/$spId" } | ConvertTo-Json -Compress))
|
|
try {
|
|
$assignResult = Invoke-RestMethod -Uri "$cippUrl/api/ListGraphRequest?TenantFilter=$TenantDomain&Endpoint=$assignEndpoint&type=POST&body=$assignBody" -Headers $cippHeaders
|
|
if ($assignResult.Results.CippStatus -eq "Good") {
|
|
Write-Output "[OK] Exchange Administrator assigned to Claude-MSP-Access"
|
|
} else {
|
|
Write-Output "[INFO] Assignment result: $($assignResult.Results | ConvertTo-Json -Compress)"
|
|
}
|
|
} catch {
|
|
$errMsg = $_.Exception.Message
|
|
if ($errMsg -match "already exist") {
|
|
Write-Output "[OK] Exchange Administrator already assigned"
|
|
} else {
|
|
Write-Output "[WARNING] Role assignment: $errMsg"
|
|
}
|
|
}
|
|
|
|
# --- STEP 5: Verify Claude API access ---
|
|
Write-Output "`n[STEP 5] Verifying Claude-MSP-Access API connectivity..."
|
|
|
|
# Get tenant ID from CIPP
|
|
$selectFields = [uri]::EscapeDataString("id,displayName")
|
|
$orgResult = Invoke-RestMethod -Uri "$cippUrl/api/ListGraphRequest?TenantFilter=$TenantDomain&Endpoint=organization&`$select=$selectFields" -Headers $cippHeaders
|
|
$customerTenantId = $orgResult.Results[0].id
|
|
Write-Output "[INFO] Tenant ID: $customerTenantId"
|
|
|
|
# Get Claude token for this tenant
|
|
$claudeTokenBody = @{
|
|
client_id = $claudeAppId
|
|
client_secret = $claudeSecret
|
|
scope = "https://graph.microsoft.com/.default"
|
|
grant_type = "client_credentials"
|
|
}
|
|
try {
|
|
$claudeToken = (Invoke-RestMethod -Uri "https://login.microsoftonline.com/$customerTenantId/oauth2/v2.0/token" -Method POST -Body $claudeTokenBody).access_token
|
|
Write-Output "[OK] Claude Graph token acquired"
|
|
} catch {
|
|
Write-Output "[ERROR] Could not get Claude token - admin consent may not be complete"
|
|
Write-Output " $($_.Exception.Message)"
|
|
exit 1
|
|
}
|
|
$claudeHeaders = @{ "Authorization" = "Bearer $claudeToken"; "Content-Type" = "application/json" }
|
|
|
|
# Test endpoints
|
|
$tests = @(
|
|
@{ Name = "Users"; Uri = "https://graph.microsoft.com/v1.0/users?`$top=1&`$select=displayName" },
|
|
@{ Name = "Security"; Uri = "https://graph.microsoft.com/v1.0/security/alerts?`$top=1" },
|
|
@{ Name = "AuditLogs"; Uri = "https://graph.microsoft.com/v1.0/auditLogs/signIns?`$top=1" },
|
|
@{ Name = "Policies"; Uri = "https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies" },
|
|
@{ Name = "Devices"; Uri = "https://graph.microsoft.com/v1.0/deviceManagement/managedDevices?`$top=1" }
|
|
)
|
|
|
|
foreach ($test in $tests) {
|
|
try {
|
|
$r = Invoke-RestMethod -Uri $test.Uri -Headers $claudeHeaders -ErrorAction Stop
|
|
Write-Output " [OK] $($test.Name)"
|
|
} catch {
|
|
$code = $_.Exception.Response.StatusCode.value__
|
|
Write-Output " [FAIL] $($test.Name): HTTP $code"
|
|
}
|
|
}
|
|
|
|
# Test Exchange Online REST
|
|
Write-Output "`n Testing Exchange Online REST API..."
|
|
try {
|
|
$exoTokenBody = @{
|
|
client_id = $claudeAppId
|
|
client_secret = $claudeSecret
|
|
scope = "https://outlook.office365.com/.default"
|
|
grant_type = "client_credentials"
|
|
}
|
|
$exoToken = (Invoke-RestMethod -Uri "https://login.microsoftonline.com/$customerTenantId/oauth2/v2.0/token" -Method POST -Body $exoTokenBody).access_token
|
|
$exoHeaders = @{ "Authorization" = "Bearer $exoToken"; "Content-Type" = "application/json" }
|
|
|
|
$invokeUrl = "https://outlook.office365.com/adminapi/beta/$customerTenantId/InvokeCommand"
|
|
$getMailbox = @{
|
|
CmdletInput = @{
|
|
CmdletName = "Get-Mailbox"
|
|
Parameters = @{ ResultSize = "1" }
|
|
}
|
|
} | ConvertTo-Json -Depth 5
|
|
|
|
$r = Invoke-RestMethod -Uri $invokeUrl -Headers $exoHeaders -Method POST -Body $getMailbox -ErrorAction Stop
|
|
Write-Output " [OK] Exchange Online (Get-Mailbox)"
|
|
} catch {
|
|
Write-Output " [FAIL] Exchange Online: $($_.Exception.Message)"
|
|
}
|
|
|
|
# --- DONE ---
|
|
Write-Output "`n========================================="
|
|
Write-Output " ONBOARDING COMPLETE: $TenantDomain"
|
|
Write-Output " $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
|
|
Write-Output "========================================="
|
|
Write-Output ""
|
|
Write-Output "Claude-MSP-Access is fully operational for this tenant."
|
|
Write-Output "Capabilities: User mgmt, mail access, security alerts,"
|
|
Write-Output "audit logs, conditional access, Intune, Exchange admin,"
|
|
Write-Output "litigation hold, and all CIPP SAM operations."
|