"""Onboard drelenaparra.com - verify access, assign Exchange Admin role.""" import urllib.request, urllib.parse, json, sys, base64 CLAUDE_APP = "fabb3421-8b34-484b-bc17-e46de9703418" CLAUDE_SECRET = "~QJ8Q~NyQSs4OcGqHZyPrA2CVnq9KBfKiimntbMO" TENANT_ID = "f06c26c7-a314-432c-a8e4-549574b6af74" TENANT = "drelenaparra.com" def get_token(scope): data = urllib.parse.urlencode({ 'client_id': CLAUDE_APP, 'client_secret': CLAUDE_SECRET, 'scope': scope, 'grant_type': 'client_credentials' }).encode() req = urllib.request.Request( f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token", data=data, method='POST') with urllib.request.urlopen(req) as resp: return json.loads(resp.read())['access_token'] def graph_get(token, url): req = urllib.request.Request(url, headers={'Authorization': f'Bearer {token}'}) with urllib.request.urlopen(req) as resp: return json.loads(resp.read()) def graph_post(token, url, body): data = json.dumps(body).encode() req = urllib.request.Request(url, data=data, method='POST', headers={'Authorization': f'Bearer {token}', 'Content-Type': 'application/json'}) with urllib.request.urlopen(req) as resp: return json.loads(resp.read()) # Step 1: Get token and check permissions print(f"[STEP 1] Getting Graph token for {TENANT}...") token = get_token("https://graph.microsoft.com/.default") # Decode JWT to check roles payload = token.split('.')[1] payload += '=' * (4 - len(payload) % 4) decoded = json.loads(base64.urlsafe_b64decode(payload)) roles = decoded.get('roles', []) print(f"[OK] Token acquired - {len(roles)} permissions granted") # Step 2: List users print(f"\n[STEP 2] Listing users...") users = graph_get(token, f"https://graph.microsoft.com/v1.0/users?$select=displayName,userPrincipalName,mail") for u in users.get('value', []): print(f" {u['displayName']} - {u['userPrincipalName']}") # Step 3: Find Claude SP print(f"\n[STEP 3] Finding Claude SP...") sp_filter = urllib.parse.quote(f"appId eq '{CLAUDE_APP}'") sp_result = graph_get(token, f"https://graph.microsoft.com/v1.0/servicePrincipals?$filter={sp_filter}&$select=id,displayName") if sp_result.get('value'): sp = sp_result['value'][0] sp_id = sp['id'] print(f"[OK] SP: {sp['displayName']} (ID: {sp_id})") else: print("[ERROR] SP not found") sys.exit(1) # Step 4: Check granted permissions print(f"\n[STEP 4] Checking granted permissions...") try: grants = graph_get(token, f"https://graph.microsoft.com/v1.0/servicePrincipals/{sp_id}/appRoleAssignments") roles_granted = grants.get('value', []) resources = {} for r in roles_granted: res = r.get('resourceDisplayName', '?') resources[res] = resources.get(res, 0) + 1 for res, count in sorted(resources.items()): print(f" {res}: {count} permissions") print(f" Total: {len(roles_granted)}") except urllib.error.HTTPError as e: print(f" Cannot read appRoleAssignments: HTTP {e.code}") # Step 5: Activate and assign Exchange Admin print(f"\n[STEP 5] Exchange Administrator role...") try: roles_result = graph_get(token, "https://graph.microsoft.com/v1.0/directoryRoles?$select=id,displayName") exo_role = None for role in roles_result.get('value', []): if role.get('displayName') == 'Exchange Administrator': exo_role = role break if not exo_role: print(" Activating from template...") try: exo_role = graph_post(token, "https://graph.microsoft.com/v1.0/directoryRoles", {"roleTemplateId": "29232cdf-9323-42fd-ade2-1d097af3e4de"}) print(f" [OK] Activated") except urllib.error.HTTPError as e: body = e.read().decode() if 'already' in body.lower(): # Re-fetch roles_result = graph_get(token, "https://graph.microsoft.com/v1.0/directoryRoles?$select=id,displayName") for role in roles_result.get('value', []): if role.get('displayName') == 'Exchange Administrator': exo_role = role break else: print(f" [ERROR] {e.code}: {body[:200]}") if exo_role: exo_role_id = exo_role['id'] print(f" Role ID: {exo_role_id}") # Assign try: graph_post(token, f"https://graph.microsoft.com/v1.0/directoryRoles/{exo_role_id}/members/$ref", {"@odata.id": f"https://graph.microsoft.com/v1.0/servicePrincipals/{sp_id}"}) print(f" [OK] Exchange Administrator assigned to Claude SP") except urllib.error.HTTPError as e: body = e.read().decode() if 'already exist' in body.lower(): print(f" [OK] Already assigned") else: print(f" [WARNING] {e.code}: {body[:200]}") except urllib.error.HTTPError as e: print(f" [ERROR] Cannot manage roles: HTTP {e.code}") # Step 6: Test access print(f"\n[STEP 6] Testing API access...") tests = [ ("Users", f"https://graph.microsoft.com/v1.0/users?$top=1&$select=displayName"), ("Security", "https://graph.microsoft.com/v1.0/security/alerts?$top=1"), ("AuditLogs", "https://graph.microsoft.com/v1.0/auditLogs/signIns?$top=1"), ("ConditionalAccess", "https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies"), ("Devices", "https://graph.microsoft.com/v1.0/deviceManagement/managedDevices?$top=1"), ] for name, url in tests: try: graph_get(token, url) print(f" [OK] {name}") except urllib.error.HTTPError as e: print(f" [FAIL] {name}: HTTP {e.code}") # Step 7: Test Exchange print(f"\n[STEP 7] Testing Exchange Online...") try: exo_token = get_token("https://outlook.office365.com/.default") invoke_url = f"https://outlook.office365.com/adminapi/beta/{TENANT_ID}/InvokeCommand" cmd = json.dumps({"CmdletInput": {"CmdletName": "Get-Mailbox", "Parameters": {"ResultSize": "1"}}}).encode() req = urllib.request.Request(invoke_url, data=cmd, method='POST', headers={'Authorization': f'Bearer {exo_token}', 'Content-Type': 'application/json'}) with urllib.request.urlopen(req) as resp: result = json.loads(resp.read()) if result.get('value'): print(f" [OK] Exchange Online - {result['value'][0].get('DisplayName','?')}") else: print(f" [OK] Exchange responded (no mailboxes yet)") except urllib.error.HTTPError as e: print(f" [FAIL] Exchange: HTTP {e.code}") except Exception as e: print(f" [FAIL] Exchange: {e}") print(f"\n{'='*50}") print(f" ONBOARDING COMPLETE: {TENANT}") print(f" Tenant ID: {TENANT_ID}") print(f"{'='*50}")