Post-Tyranny-Tech-Infrastru.../ansible/roles/zitadel/files/bootstrap_api_token.py
Pieter 48ef4da920 Fix Zitadel deployment by removing FirstInstance variables
- Remove all ZITADEL_FIRSTINSTANCE_* environment variables
- Fixes migration error: duplicate key constraint violation
- Root cause: Bug in Zitadel v2.63.7 FirstInstance migration
- Workaround: Complete initial setup via web UI
- Upstream issue: https://github.com/zitadel/zitadel/issues/8791

Changes:
- Clean up obsolete documentation (OIDC_AUTOMATION.md, SETUP_GUIDE.md, COLLABORA_SETUP.md)
- Add PROJECT_REFERENCE.md for essential configuration info
- Add force recreate functionality with clean database volumes
- Update bootstrap instructions for web UI setup
- Document one-time manual setup requirement for OIDC automation

Zitadel now deploys successfully and is accessible at:
https://zitadel.test.vrije.cloud

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-06 16:43:57 +01:00

171 lines
5 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
Automate creation of Zitadel API service user and Personal Access Token.
This script logs in as admin and creates a machine user with a PAT for API automation.
"""
import requests
import sys
import time
import re
from urllib.parse import urlparse, parse_qs
def login_and_get_session(domain, username, password):
"""Login to Zitadel and get authenticated session cookies."""
session = requests.Session()
session.headers.update({
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
})
# Start login flow
login_url = f"https://{domain}/ui/login/loginname"
print(f"📡 Initiating login to {domain}...")
# Get login page to establish session
resp = session.get(login_url, allow_redirects=True)
# Submit username
login_data = {
'loginName': username
}
resp = session.post(login_url, data=login_data, allow_redirects=True)
# Submit password
password_url = f"https://{domain}/ui/login/password"
password_data = {
'password': password
}
resp = session.post(password_url, data=password_data, allow_redirects=True)
if 'set-cookie' in resp.headers or len(session.cookies) > 0:
print("✅ Login successful!")
return session
else:
print(f"❌ Login failed. Status: {resp.status_code}")
print(f"Response: {resp.text[:500]}")
return None
def create_machine_user(session, domain):
"""Create a machine user via Management API."""
api_url = f"https://{domain}/management/v1/users/machine"
print("🤖 Creating API automation service user...")
payload = {
"userName": "api-automation",
"name": "API Automation Service",
"description": "Service account for automated OIDC app provisioning",
"accessTokenType": "ACCESS_TOKEN_TYPE_BEARER"
}
resp = session.post(api_url, json=payload)
if resp.status_code in [200, 201]:
data = resp.json()
user_id = data.get('userId')
print(f"✅ Machine user created: {user_id}")
return user_id
elif resp.status_code == 409:
print(" Machine user already exists")
# Try to get existing user
list_url = f"https://{domain}/management/v1/users/_search"
search_payload = {
"query": {
"userName": "api-automation"
}
}
resp = session.post(list_url, json=search_payload)
if resp.status_code == 200:
users = resp.json().get('result', [])
if users:
user_id = users[0].get('id')
print(f"✅ Found existing user: {user_id}")
return user_id
return None
else:
print(f"❌ Failed to create machine user. Status: {resp.status_code}")
print(f"Response: {resp.text}")
return None
def create_pat(session, domain, user_id):
"""Create a Personal Access Token for the machine user."""
pat_url = f"https://{domain}/management/v1/users/{user_id}/pats"
print("🔑 Creating Personal Access Token...")
payload = {
"expirationDate": "2099-12-31T23:59:59Z"
}
resp = session.post(pat_url, json=payload)
if resp.status_code in [200, 201]:
data = resp.json()
token = data.get('token')
if token:
print("✅ Personal Access Token created successfully!")
return token
else:
print("⚠️ PAT created but token not in response")
print(f"Response: {resp.text}")
return None
else:
print(f"❌ Failed to create PAT. Status: {resp.status_code}")
print(f"Response: {resp.text}")
return None
def main():
if len(sys.argv) != 4:
print("Usage: python3 bootstrap_api_token.py <domain> <admin_username> <admin_password>")
print("Example: python3 bootstrap_api_token.py zitadel.test.vrije.cloud 'admin@test.zitadel.test.vrije.cloud' 'password123'")
sys.exit(1)
domain = sys.argv[1]
username = sys.argv[2]
password = sys.argv[3]
print(f"""
🚀 Zitadel API Token Bootstrap
================================
Domain: {domain}
Admin: {username}
""")
# Step 1: Login
session = login_and_get_session(domain, username, password)
if not session:
print("\n❌ Failed to establish session")
sys.exit(1)
# Small delay to ensure session is established
time.sleep(2)
# Step 2: Create machine user
user_id = create_machine_user(session, domain)
if not user_id:
print("\n❌ Failed to create or find machine user")
sys.exit(1)
# Small delay
time.sleep(1)
# Step 3: Create PAT
token = create_pat(session, domain, user_id)
if not token:
print("\n❌ Failed to create Personal Access Token")
sys.exit(1)
print(f"""
✅ SUCCESS! API automation is ready.
📋 Personal Access Token:
{token}
🔐 Add this to your secrets file:
zitadel_api_token: {token}
Then re-run: ansible-playbook -i hcloud.yml playbooks/deploy.yml
""")
if __name__ == '__main__':
main()