fix(keycloak): update user club attributes with real database UUIDs
- Replaced placeholder UUIDs (club-1-uuid, club-2-uuid) with real database UUIDs - Updated all 5 test users via Keycloak database - Restarted Keycloak to clear caches and apply changes Impact: - JWT tokens now contain real UUIDs in clubs claim - API endpoints accept X-Tenant-Id with real UUIDs (returns 200 OK) - Unblocks 46 remaining QA scenarios Documentation: - Created update-keycloak-club-uuids.py script for automation - Added KEYCLOAK_UPDATE_GUIDE.md with step-by-step instructions - Recorded learnings in notepad Ref: .sisyphus/evidence/final-f3-manual-qa.md lines 465-512
This commit is contained in:
233
.sisyphus/scripts/update-keycloak-club-uuids.py
Executable file
233
.sisyphus/scripts/update-keycloak-club-uuids.py
Executable file
@@ -0,0 +1,233 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Update Keycloak user club attributes to use real database UUIDs instead of placeholders.
|
||||
|
||||
This script updates the 'clubs' attribute for all test users in the workclub realm,
|
||||
replacing placeholder UUIDs with real database UUIDs and restarting Keycloak to
|
||||
ensure tokens reflect the updated attributes.
|
||||
|
||||
Real UUIDs:
|
||||
- Sunrise Tennis Club: afa8daf3-5cfa-4589-9200-b39a538a12de
|
||||
- Valley Cycling Club: a1952a72-2e13-4a4e-87dd-821847b58698
|
||||
|
||||
Usage:
|
||||
python3 update-keycloak-club-uuids.py
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
KEYCLOAK_ADMIN_URL = "http://localhost:8080"
|
||||
KEYCLOAK_REALM = "workclub"
|
||||
DB_HOST = "localhost"
|
||||
DB_PORT = 5432
|
||||
DB_NAME = "keycloak"
|
||||
DB_USER = "postgres"
|
||||
|
||||
SUNRISE_UUID = "afa8daf3-5cfa-4589-9200-b39a538a12de"
|
||||
VALLEY_UUID = "a1952a72-2e13-4a4e-87dd-821847b58698"
|
||||
|
||||
# User ID mappings from Keycloak
|
||||
USERS = {
|
||||
"bf5adcfb-0978-4beb-8e02-7577f0ded47f": {
|
||||
"username": "admin@test.com",
|
||||
"clubs": {SUNRISE_UUID: "admin", VALLEY_UUID: "member"}
|
||||
},
|
||||
"aa5270a3-633a-4d89-a3b4-a467b08cbb55": {
|
||||
"username": "manager@test.com",
|
||||
"clubs": {SUNRISE_UUID: "manager", VALLEY_UUID: "member"}
|
||||
},
|
||||
"60c0d8b9-6354-4ad3-bfac-9547c68c069b": {
|
||||
"username": "member1@test.com",
|
||||
"clubs": {SUNRISE_UUID: "member", VALLEY_UUID: "member"}
|
||||
},
|
||||
"294a2086-cf2f-43cc-9bc6-2a8a7d325b9a": {
|
||||
"username": "member2@test.com",
|
||||
"clubs": {VALLEY_UUID: "member"}
|
||||
},
|
||||
"f4890d47-ba6c-4691-9d7b-4f656c60f232": {
|
||||
"username": "viewer@test.com",
|
||||
"clubs": {SUNRISE_UUID: "viewer"}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def update_via_database():
|
||||
"""Update clubs attributes directly in PostgreSQL database."""
|
||||
print("\n[1/4] Updating clubs attributes in database...")
|
||||
|
||||
for user_id, user_data in USERS.items():
|
||||
username = user_data["username"]
|
||||
clubs = user_data["clubs"]
|
||||
clubs_json = json.dumps(clubs).replace("'", "''")
|
||||
|
||||
sql = f"UPDATE user_attribute SET value = '{clubs_json}' WHERE user_id = '{user_id}' AND name = 'clubs';"
|
||||
|
||||
cmd = [
|
||||
"docker", "exec", "workclub_postgres",
|
||||
"psql", "-U", DB_USER, "-d", DB_NAME, "-c", sql
|
||||
]
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
print(f" ✗ {username}: {result.stderr}")
|
||||
return False
|
||||
print(f" ✓ {username}")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def restart_keycloak():
|
||||
"""Restart Keycloak to clear caches."""
|
||||
print("\n[2/4] Restarting Keycloak to clear caches...")
|
||||
|
||||
cmd = ["docker", "restart", "workclub_keycloak"]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
print(f" ✗ Failed to restart: {result.stderr}")
|
||||
return False
|
||||
|
||||
print(" ✓ Keycloak restarted")
|
||||
|
||||
# Wait for Keycloak to be ready
|
||||
print(" ⏳ Waiting for Keycloak to be ready...")
|
||||
for i in range(30):
|
||||
try:
|
||||
cmd = [
|
||||
"curl", "-s", "-f",
|
||||
f"{KEYCLOAK_ADMIN_URL}/health"
|
||||
]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=2)
|
||||
if result.returncode == 0:
|
||||
print(" ✓ Keycloak is ready")
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
|
||||
if (i + 1) % 5 == 0:
|
||||
print(f" Waiting... ({i + 1}/30)")
|
||||
|
||||
print(" ✗ Keycloak did not become ready in time")
|
||||
return False
|
||||
|
||||
|
||||
def verify_jwt_tokens():
|
||||
"""Verify JWT tokens contain real UUIDs."""
|
||||
import base64
|
||||
|
||||
print("\n[3/4] Verifying JWT tokens contain real UUIDs...")
|
||||
|
||||
for user_id, user_data in USERS.items():
|
||||
username = user_data["username"]
|
||||
|
||||
# Get token
|
||||
cmd = [
|
||||
"curl", "-s", "-X", "POST",
|
||||
f"{KEYCLOAK_ADMIN_URL}/realms/{KEYCLOAK_REALM}/protocol/openid-connect/token",
|
||||
"-d", "client_id=workclub-app",
|
||||
"-d", "grant_type=password",
|
||||
"-d", f"username={username}",
|
||||
"-d", "password=testpass123"
|
||||
]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
|
||||
try:
|
||||
data = json.loads(result.stdout)
|
||||
if "access_token" not in data:
|
||||
print(f" ✗ {username}: {data.get('error', 'No token')}")
|
||||
return False
|
||||
|
||||
token = data["access_token"]
|
||||
parts = token.split('.')
|
||||
|
||||
# Decode payload
|
||||
payload = parts[1]
|
||||
padding = 4 - len(payload) % 4
|
||||
if padding != 4:
|
||||
payload += '=' * padding
|
||||
|
||||
decoded = json.loads(base64.urlsafe_b64decode(payload))
|
||||
clubs = decoded.get("clubs", {})
|
||||
|
||||
# Check if real UUIDs are present
|
||||
expected_clubs = user_data["clubs"]
|
||||
if clubs == expected_clubs:
|
||||
print(f" ✓ {username}: {json.dumps(clubs)}")
|
||||
else:
|
||||
print(f" ✗ {username}: Expected {expected_clubs}, got {clubs}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f" ✗ {username}: {str(e)}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def test_api_endpoint():
|
||||
"""Test API endpoint with real UUIDs."""
|
||||
print("\n[4/4] Testing API endpoints with real UUIDs...")
|
||||
|
||||
# Test with admin user and first club
|
||||
cmd = [
|
||||
"curl", "-s", "-X", "POST",
|
||||
f"{KEYCLOAK_ADMIN_URL}/realms/{KEYCLOAK_REALM}/protocol/openid-connect/token",
|
||||
"-d", "client_id=workclub-app",
|
||||
"-d", "grant_type=password",
|
||||
"-d", "username=admin@test.com",
|
||||
"-d", "password=testpass123"
|
||||
]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
token = json.loads(result.stdout)["access_token"]
|
||||
|
||||
# Test API
|
||||
cmd = [
|
||||
"curl", "-s", "-i", "-X", "GET",
|
||||
"-H", f"Authorization: Bearer {token}",
|
||||
"-H", f"X-Tenant-Id: {SUNRISE_UUID}",
|
||||
"http://localhost:5001/api/clubs/me"
|
||||
]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
|
||||
if "200 OK" in result.stdout:
|
||||
print(f" ✓ GET /api/clubs/me: 200 OK with real UUID in X-Tenant-Id")
|
||||
return True
|
||||
else:
|
||||
print(f" ✗ API endpoint returned: {result.stdout[:100]}")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""Run all update steps."""
|
||||
print("=" * 80)
|
||||
print("KEYCLOAK CLUB UUID UPDATE SCRIPT")
|
||||
print("=" * 80)
|
||||
|
||||
steps = [
|
||||
("Update database", update_via_database),
|
||||
("Restart Keycloak", restart_keycloak),
|
||||
("Verify JWT tokens", verify_jwt_tokens),
|
||||
("Test API endpoint", test_api_endpoint),
|
||||
]
|
||||
|
||||
for step_name, step_func in steps:
|
||||
if not step_func():
|
||||
print(f"\n✗ FAILED at: {step_name}")
|
||||
return 1
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("✓ ALL UPDATES SUCCESSFUL")
|
||||
print("=" * 80)
|
||||
print("\nSummary:")
|
||||
print(f" - Updated 5 test users with real club UUIDs")
|
||||
print(f" - Sunrise Tennis Club: {SUNRISE_UUID}")
|
||||
print(f" - Valley Cycling Club: {VALLEY_UUID}")
|
||||
print(f" - JWT tokens now contain real UUIDs instead of placeholders")
|
||||
print(f" - API endpoints accept requests with real X-Tenant-Id headers")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user