import urllib.request, urllib.parse, json, sys CLAUDE_APP = "fabb3421-8b34-484b-bc17-e46de9703418" CLAUDE_SECRET = "~QJ8Q~NyQSs4OcGqHZyPrA2CVnq9KBfKiimntbMO" TENANT_ID = "dd4a82e8-85a3-44ac-8800-07945ab4d95f" TENANT = "bardach.net" def get_token(tid, cid, secret, scope): data = urllib.parse.urlencode({ 'client_id': cid, 'client_secret': secret, 'scope': scope, 'grant_type': 'client_credentials' }).encode() req = urllib.request.Request( f"https://login.microsoftonline.com/{tid}/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 Graph token print("[STEP 1] Getting Graph token...") token = get_token(TENANT_ID, CLAUDE_APP, CLAUDE_SECRET, "https://graph.microsoft.com/.default") print("[OK] Graph token acquired") # Step 2: Find Claude SP print("\n[STEP 2] 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] Claude SP not found") sys.exit(1) # Step 3: Check granted app role assignments print("\n[STEP 3] Checking granted permissions...") try: grants = graph_get(token, f"https://graph.microsoft.com/v1.0/servicePrincipals/{sp_id}/appRoleAssignments") roles = grants.get('value', []) print(f"[INFO] {len(roles)} app role assignments") # Get unique resource names resources = set() for r in roles: resources.add(r.get('resourceDisplayName', '?')) for res in sorted(resources): count = sum(1 for r in roles if r.get('resourceDisplayName') == res) print(f" {res}: {count} permissions") except urllib.error.HTTPError as e: print(f"[INFO] Cannot read appRoleAssignments: HTTP {e.code}") # Step 4: Find Exchange Admin role print("\n[STEP 4] Finding Exchange Administrator role...") try: roles_result = graph_get(token, "https://graph.microsoft.com/v1.0/directoryRoles?$select=id,displayName,roleTemplateId") 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("[INFO] Exchange Admin not activated, activating from template...") try: activate = graph_post(token, "https://graph.microsoft.com/v1.0/directoryRoles", {"roleTemplateId": "29232cdf-9323-42fd-ade2-1d097af3e4de"}) exo_role = activate print(f"[OK] Activated: {activate.get('id')}") except urllib.error.HTTPError as e: body = e.read().decode() print(f"[ERROR] Activation failed: HTTP {e.code} - {body[:200]}") sys.exit(1) exo_role_id = exo_role['id'] print(f"[OK] Exchange Admin Role ID: {exo_role_id}") except urllib.error.HTTPError as e: print(f"[ERROR] Cannot list directory roles: HTTP {e.code}") sys.exit(1) # Step 5: Assign Exchange Admin to Claude SP print("\n[STEP 5] Assigning Exchange Admin role to Claude SP...") try: assign_body = {"@odata.id": f"https://graph.microsoft.com/v1.0/servicePrincipals/{sp_id}"} graph_post(token, f"https://graph.microsoft.com/v1.0/directoryRoles/{exo_role_id}/members/$ref", assign_body) print("[OK] Exchange Administrator assigned!") except urllib.error.HTTPError as e: body = e.read().decode() if 'already exist' in body.lower(): print("[OK] Exchange Administrator already assigned") else: print(f"[ERROR] HTTP {e.code}: {body[:300]}") # Step 6: Test Exchange REST API print("\n[STEP 6] Testing Exchange REST API...") exo_token = get_token(TENANT_ID, CLAUDE_APP, CLAUDE_SECRET, "https://outlook.office365.com/.default") invoke_url = f"https://outlook.office365.com/adminapi/beta/{TENANT_ID}/InvokeCommand" headers = {'Authorization': f'Bearer {exo_token}', 'Content-Type': 'application/json'} cmd = json.dumps({ "CmdletInput": { "CmdletName": "Get-Mailbox", "Parameters": {"Identity": "barbara@bardach.net", "ResultSize": "1"} } }).encode() try: req = urllib.request.Request(invoke_url, data=cmd, headers=headers, method='POST') with urllib.request.urlopen(req) as resp: result = json.loads(resp.read()) if result.get('value'): mb = result['value'][0] print(f"[OK] Exchange access works - {mb.get('DisplayName', '?')}") else: print(f"[OK] Exchange responded: {json.dumps(result)[:200]}") except urllib.error.HTTPError as e: body = e.read().decode() print(f"[ERROR] Exchange REST: HTTP {e.code} - {body[:300]}")