1#!/usr/bin/env python3
2import argparse
3import hashlib
4import json
5import os
6from pathlib import Path
7import subprocess
8
9
10parser = argparse.ArgumentParser(description="Copy Keycloak accounts into a private dashboard import")
11parser.add_argument("--host", required=True)
12parser.add_argument("--output", required=True, type=Path)
13args = parser.parse_args()
14if args.output.exists():
15 parser.error("output already exists")
16
17remote = r'''
18import json, subprocess, urllib.request
19request = urllib.request.Request("http://127.0.0.1:4646/v1/job/postgres/allocations", headers={"X-Nomad-Token":open("/var/lib/studio/nomad.token").read().strip()})
20allocations = [a["ID"] for a in json.load(urllib.request.urlopen(request)) if a["ClientStatus"] == "running" and a["DesiredStatus"] == "run"]
21assert len(allocations) == 1
22containers = [line.split()[0] for line in subprocess.check_output(["podman", "ps", "--format", "{{.ID}} {{.Names}}"], text=True).splitlines() if line.split()[1].endswith(allocations[0])]
23assert len(containers) == 1
24sql = """
25BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY;
26SELECT json_build_object(
27 'issuer','https://auth.paperclover.net/realms/master',
28 'rpId','auth.paperclover.net',
29 'roles',(SELECT coalesce(json_agg(json_build_object('id',id,'name',name)), '[]') FROM keycloak_role WHERE realm_id=(SELECT id FROM realm WHERE name='master') AND client_role=false),
30 'users',(SELECT coalesce(json_agg(json_build_object(
31 'id',u.id,'username',u.username,'email',u.email,'firstName',u.first_name,'lastName',u.last_name,
32 'enabled',u.enabled,'emailVerified',u.email_verified,'createdTimestamp',u.created_timestamp,
33 'requiredActions',(SELECT coalesce(json_agg(required_action),'[]') FROM user_required_action WHERE user_id=u.id),
34 'attributes',(SELECT coalesce(json_object_agg(name,vals),'{}') FROM (SELECT name,json_agg(value) AS vals FROM user_attribute WHERE user_id=u.id GROUP BY name)a),
35 'roles',(SELECT coalesce(json_agg(role_id),'[]') FROM user_role_mapping WHERE user_id=u.id),
36 'credentials',(SELECT coalesce(json_agg(json_build_object('id',id,'type',type,'userLabel',user_label,'createdDate',created_date,'credentialData',credential_data::json,'secretData',secret_data::json)),'[]') FROM credential WHERE user_id=u.id)
37 )),'[]') FROM user_entity u WHERE realm_id=(SELECT id FROM realm WHERE name='master'))
38);
39COMMIT;
40"""
41result = subprocess.run(["podman", "exec", "-i", containers[0], "psql", "-U", "postgres", "-d", "keycloak_next", "-tA", "-v", "ON_ERROR_STOP=1"], input=sql, text=True, capture_output=True, check=True)
42print(next(line for line in result.stdout.splitlines() if line.startswith('{')))
43'''
44result = subprocess.run(["ssh", "-o", "BatchMode=yes", args.host, "python3", "-"], input=remote, text=True, capture_output=True, check=True)
45data = json.loads(result.stdout)
46if not data["users"]:
47 raise ValueError("source realm has no accounts")
48content = (json.dumps(data, separators=(",", ":")) + "\n").encode()
49args.output.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
50with os.fdopen(os.open(args.output, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600), "wb") as output:
51 output.write(content)
52 output.flush()
53 os.fsync(output.fileno())
54counts = {}
55for user in data["users"]:
56 for credential in user["credentials"]:
57 kind = credential["type"]
58 counts[kind] = counts.get(kind, 0) + 1
59print(json.dumps({"accounts": len(data["users"]), "credentials": counts, "sha256": hashlib.sha256(content).hexdigest()}))