Keycloak admin REST API を叩いて realm と user を新規作成する

## step1: keycloakをlocalhost:8080で起動する。

環境変数で管理者である"admin"のパスワードを"admin"に設定している

```sh
docker run -t --rm -p 8080:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:21.0.2 start-dev --http-port=8080
```

## step2: 以下のPythonを実行

1. 最初にアクセストークンを取得
2. 新しいrealmであるnewrealmを作成
3. newrealmにuser1,user2,user3を追加、各ユーザーのパスワードとして"password"を設定

```python
from typing import Dict, Optional
from urllib import parse, request, error
import os
import json

baseurl="http://localhost:8080"
email_domain="example.com"

def main():
    code, res = req(
        "/realms/master/protocol/openid-connect/token",
        data_urlencoded={"username":"admin", "password":"admin", "grant_type":"password", "client_id":"admin-cli"}
    )
    token=res["access_token"]
    realm="newrealm"
    add_realm(realm, token)
    for username in ["user1", "user2", "user3"]:
        add_user(username, "password", realm, token)

def add_realm(realm, token):
    return req(
        "/admin/realms",
        data_json={
            "id": realm,
            "realm": realm,
            "displayName": "New Realm",
            "enabled": True,
            "sslRequired": "external",
            "registrationAllowed": False,
            "loginWithEmailAllowed": True,
            "duplicateEmailsAllowed": False,
            "resetPasswordAllowed": False,
            "editUsernameAllowed": False,
            "bruteForceProtected": True
        },
        headers=get_headers(token)
    )
def add_user(username, password, realm, token):
    return req(
        "/admin/realms/{}/users".format(realm),
        data_json={
            "enabled":True,
            "username":username,
            "email":"{}@{}".format(username, email_domain),
            "emailVerified":True,
            "credentials":[{"type":"password","value":password,"temporary":False}]
        },
        headers=get_headers(token)
    )
def get_headers(token):
    return {
        "Content-Type":"application/json",
        "Authorization":"Bearer {}".format(token)
    }
def req(url, data_json:Optional[Dict]=None, data_urlencoded:Optional[Dict]=None, headers:Optional[Dict]=None):
    headers=headers or {}
    data=None
    if data_json:
        data=json.dumps(data_json)
    if data_urlencoded:
        data=parse.urlencode(data_urlencoded)
    url=baseurl+url
    print('{} {} \n--data "{}"'.format(("POST" if data else "GET"), url, data), end=" ")
    req = request.Request(url,data.encode('utf-8'),headers)
    def defer(res):
        code, data=res.getcode(), json.loads(res.read().decode("utf-8") or "{}")
        print(code, data)
        return code, data
    try:
        with request.urlopen(req) as res:
            return defer(res)
    except error.HTTPError as e:
        return defer(e)

if __name__=="__main__":
    main()
```

実行結果

```
POST http://localhost:8080/realms/master/protocol/openid-connect/token 
--data "username=admin&password=admin&grant_type=password&client_id=admin-cli" 200 {'access_token': 'eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ0VE41WkdtSG5RTTBRd1FlQnhKR3JUMXN1dzN1QWdNNDduNUpKZ1FBQi1rIn0.eyJleHAiOjE2OTU
wOTMwNzAsImlhdCI6MTY5NTA5MzAxMCwianRpIjoiY2ZiMjAyNzEtZTQ4Mi00MDVkLWJmZDktMzdhNTYyZTE1YTYxIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo5MDgwL3JlYWxtcy9tYXN0ZXIiLCJzdWIiOiIxZWFkYzQ0OC1lMTQzLTQ1NGItOTM5ZS1iYmQwNjA2OWJlYjEiLCJ0eXAiOiJCZWFyZXI
iLCJhenAiOiJhZG1pbi1jbGkiLCJzZXNzaW9uX3N0YXRlIjoiMWE4MDI1NzktMGZhYi00OGQ1LThjM2UtMmI1ZjZiODJkYzM2IiwiYWNyIjoiMSIsInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsInNpZCI6IjFhODAyNTc5LTBmYWItNDhkNS04YzNlLTJiNWY2YjgyZGMzNiIsImVtYWlsX3ZlcmlmaWVkIjp
mYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWRtaW4ifQ.eP0VXJ7TRq2uyelLbl50hDQL4kKf_7wzE9L_61qOBq1OUh_9M7LaFiLaV7TO6vS0KVd0oIalz18lSqC0ti0Z9LsFtlXq8SlGDYAtJ0Vcdx-JfDEg52zPzKF1lF8CdwvbGegA3O8vxKur6X6qwPW8bmp6GMGrYRXWiZ01EfkwMdbyY0PQDU2s
kN3KObe0iwPObZV5j05uT19gcsqQ-PyKL9wWUP98ipK8Q90lYlpTLWS5erJ1SUd0qTJWkEOYSJ3U46G6D0LuORzgpb9NKDg7eVgQYT7gdyUWayim4pimVgOx1KjiO2oMCssEmHexuGxJ_li021atgegDAjPiUHSSZQ', 'expires_in': 60, 'refresh_expires_in': 1800, 'refresh_token': 
'eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJlZjg0YTkzOS1lODhmLTQ2M2EtOWJmMC1jMzhmM2Y5NzQxYmQifQ.eyJleHAiOjE2OTUwOTQ4MTAsImlhdCI6MTY5NTA5MzAxMCwianRpIjoiMDY0NzI2ZTktNjc5My00YjdkLTk3ZTYtMjU1ZTIzMWRlOWM4IiwiaXNzIjoiaHR0cDov
L2xvY2FsaG9zdDo5MDgwL3JlYWxtcy9tYXN0ZXIiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjkwODAvcmVhbG1zL21hc3RlciIsInN1YiI6IjFlYWRjNDQ4LWUxNDMtNDU0Yi05MzllLWJiZDA2MDY5YmViMSIsInR5cCI6IlJlZnJlc2giLCJhenAiOiJhZG1pbi1jbGkiLCJzZXNzaW9uX3N0YXRlIjoi
MWE4MDI1NzktMGZhYi00OGQ1LThjM2UtMmI1ZjZiODJkYzM2Iiwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwic2lkIjoiMWE4MDI1NzktMGZhYi00OGQ1LThjM2UtMmI1ZjZiODJkYzM2In0.KwswB_Adc887l3FN1pBmbYTumm6R1q6ManFjCTTfUis', 'token_type': 'Bearer', 'not-before-policy': 0, 'session_state': '1a802579-0fab-48d5-8c3e-2b5f6b82dc36', 'scope': 'email profile'}
POST http://localhost:8080/admin/realms
--data "{"id": "newrealm", "realm": "newrealm", "displayName": "New Realm", "enabled": true, "sslRequired": "external", "registrationAllowed": false, "loginWithEmailAllowed": true, "duplicateEmailsAllowed": false, "resetPasswordAllowed": false, "editUsernameAllowed": false, "bruteForceProtected": true}" 201 {}
POST http://localhost:8080/admin/realms/newrealm/users
--data "{"enabled": true, "username": "user1", "email": "user1@example.com", "emailVerified": true, "credentials": [{"type": "password", "value": "password", "temporary": false}]}" 201 {}
POST http://localhost:8080/admin/realms/newrealm/users
--data "{"enabled": true, "username": "user2", "email": "user2@example.com", "emailVerified": true, "credentials": [{"type": "password", "value": "password", "temporary": false}]}" 201 {}
POST http://localhost:8080/admin/realms/newrealm/users
--data "{"enabled": true, "username": "user3", "email": "user3@example.com", "emailVerified": true, "credentials": [{"type": "password", "value": "password", "temporary": false}]}" 201 {}

```