Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ENH]Add Keycloak integration testing workflow #2

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/dockerfiles/Dockerfile.keycloak
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
FROM quay.io/keycloak/keycloak:latest as builder

# Enable health and metrics support
ENV KC_HEALTH_ENABLED=true
ENV KC_METRICS_ENABLED=true

# Configure a database vendor
ENV KC_DB=postgres

WORKDIR /opt/keycloak
# for demonstration purposes only, please make sure to use proper certificates in production instead
RUN keytool -genkeypair -storepass password -storetype PKCS12 -keyalg RSA -keysize 2048 -dname "CN=server" -alias server -ext "SAN:c=DNS:localhost,IP:127.0.0.1" -keystore conf/server.keystore
RUN /opt/keycloak/bin/kc.sh build

FROM quay.io/keycloak/keycloak:latest
COPY --from=builder /opt/keycloak/ /opt/keycloak/


ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]
44 changes: 44 additions & 0 deletions .github/dockerfiles/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
version: '3.8'

services:
postgres:
image: postgres:latest
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: keycloak
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- keycloak-network

keycloak:
build:
context: .
dockerfile: Dockerfile.keycloak
environment:
KC_HOSTNAME: localhost
KC_HOSTNAME_PORT: 8080
KC_HOSTNAME_STRICT_BACKCHANNEL: false
KC_HTTP_ENABLED: true
KC_HOSTNAME_STRICT_HTTPS: false
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://postgres/keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: keycloak
command:
- start-dev
ports:
- "8080:8080"
depends_on:
- postgres
networks:
- keycloak-network

volumes:
postgres_data:

networks:
keycloak-network:
117 changes: 117 additions & 0 deletions .github/scripts/setup_keycloak_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import json
import os
import time

from keycloak import KeycloakAdmin

# Environment variables
KEYCLOAK_SERVER_URL = os.environ.get("KEYCLOAK_SERVER_URL", "http://localhost:8080/")
KEYCLOAK_ADMIN_USERNAME = os.environ.get("KEYCLOAK_ADMIN_USERNAME", "admin")
KEYCLOAK_ADMIN_PASSWORD = os.environ.get("KEYCLOAK_ADMIN_PASSWORD", "admin")

KEYCLOAK_REALM = os.environ.get("KEYCLOAK_REALM", "test")
KEYCLOAK_CLIENT_ID = os.environ.get("KEYCLOAK_CLIENT_ID", "test-client")
KEYCLOAK_CLIENT_SECRET = os.environ.get("KEYCLOAK_CLIENT_SECRET", "test-client-secret")

# Initialize Keycloak Admin client
keycloak_admin = KeycloakAdmin(
server_url=KEYCLOAK_SERVER_URL,
username=KEYCLOAK_ADMIN_USERNAME,
password=KEYCLOAK_ADMIN_PASSWORD,
realm_name="master",
verify=False,
)

# Wait for Keycloak server to be available
for _ in range(10):
try:
keycloak_admin.get_realms()
break
except:
time.sleep(5)
else:
raise Exception("Keycloak server is not responding.")

# Check if the realm already exists
realms = keycloak_admin.get_realms()
if KEYCLOAK_REALM not in [realm["realm"] for realm in realms]:
# Create a new realm
keycloak_admin.create_realm(payload={"realm": KEYCLOAK_REALM, "enabled": True})
print(f"Realm '{KEYCLOAK_REALM}' created.")

# Switch to the new realm
keycloak_admin.connection.realm_name = KEYCLOAK_REALM

# Check if the client exists
clients = keycloak_admin.get_clients()
client = next((c for c in clients if c["clientId"] == KEYCLOAK_CLIENT_ID), None)

if not client:
# Create the client
client_id = keycloak_admin.create_client(
payload={
"clientId": KEYCLOAK_CLIENT_ID,
"name": KEYCLOAK_CLIENT_ID,
"enabled": True,
"clientAuthenticatorType": "client-secret",
"secret": KEYCLOAK_CLIENT_SECRET,
"protocol": "openid-connect",
"publicClient": False,
"serviceAccountsEnabled": True,
"standardFlowEnabled": False,
"directAccessGrantsEnabled": False,
}
)
print(f"Client '{KEYCLOAK_CLIENT_ID}' created.")
else:
client_id = client["id"]
print(f"Client '{KEYCLOAK_CLIENT_ID}' already exists.")

# Get the service account user ID
service_account_user = keycloak_admin.get_client_service_account_user(client_id)
service_account_user_id = service_account_user["id"]

# Get realm-management client ID
realm_management_client_id = keycloak_admin.get_client_id("realm-management")
realm_admin_role_id = keycloak_admin.get_client_role_id(
client_id=realm_management_client_id, role_name="realm-admin"
)

realm_admin_role = keycloak_admin.get_role_by_id(role_id=realm_admin_role_id)

# Check if the role is already assigned to the service account user
assigned_roles = keycloak_admin.get_client_role_members(
client_id=realm_management_client_id,
role_name="realm-admin",
)
role_already_assigned = any(
role["id"] == service_account_user_id for role in assigned_roles
)

if not role_already_assigned:
# Assign the realm-admin role to the service account user
keycloak_admin.assign_client_role(
user_id=service_account_user_id,
client_id=realm_management_client_id,
roles=[realm_admin_role],
)
print("Assigned 'realm-admin' role to the service account user.")
else:
print("'realm-admin' role is already assigned to the service account user.")

# Test client connection
try:
keycloak_admin_test = KeycloakAdmin(
server_url=KEYCLOAK_SERVER_URL,
realm_name=KEYCLOAK_REALM,
client_id=KEYCLOAK_CLIENT_ID,
client_secret_key=KEYCLOAK_CLIENT_SECRET,
verify=False,
)
print("Connected to the client using service account.")
except Exception as e:
raise Exception("Could not connect to the client.") from e

# Get users info from the realm
users = keycloak_admin.get_users()
print(json.dumps(users, indent=2))
207 changes: 207 additions & 0 deletions .github/scripts/setup_keycloak_mock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import os
from keycloak import KeycloakAdmin

# Environment variables
KEYCLOAK_SERVER_URL = os.environ.get("KEYCLOAK_SERVER_URL", "http://localhost:8080/")
KEYCLOAK_ADMIN_USERNAME = os.environ.get("KEYCLOAK_ADMIN_USERNAME", "admin")
KEYCLOAK_ADMIN_PASSWORD = os.environ.get("KEYCLOAK_ADMIN_PASSWORD", "admin")

KEYCLOAK_REALM = os.environ.get("KEYCLOAK_REALM", "test")
KEYCLOAK_CLIENT_ID = os.environ.get("KEYCLOAK_CLIENT_ID", "test-client")
KEYCLOAK_CLIENT_SECRET = os.environ.get("KEYCLOAK_CLIENT_SECRET", "test-client-secret")

# Initialize Keycloak Admin client
keycloak_admin = KeycloakAdmin(
server_url=KEYCLOAK_SERVER_URL,
username=KEYCLOAK_ADMIN_USERNAME,
password=KEYCLOAK_ADMIN_PASSWORD,
realm_name=KEYCLOAK_REALM,
verify=False,
)


# Create users with additional attributes
def create_user(username: str, first_name: str, last_name: str, email: str):
keycloak_admin.create_user(
{
"username": username,
"firstName": first_name,
"lastName": last_name,
"email": email,
"enabled": True,
"credentials": [
{"type": "password", "value": "password", "temporary": False}
],
}
)


USERS = [
{
"username": "user1",
"first_name": "Alice",
"last_name": "Smith",
"email": "alice@example.com",
},
{
"username": "user2",
"first_name": "Bob",
"last_name": "Brown",
"email": "bob@example.com",
},
{
"username": "user3",
"first_name": "Carol",
"last_name": "Jones",
"email": "carol@example.com",
},
]

for user in USERS:
create_user(**user)


# Create groups
def create_group(name: str):
keycloak_admin.create_group({"name": name})


GROUPS = ["developers", "testers", "admins"]
for group in GROUPS:
create_group(group)


# Add users to groups
def add_users_to_group(group_name: str, usernames: list):
group_id = keycloak_admin.get_group_by_path(f"/{group_name}")["id"]
for username in usernames:
user_id = keycloak_admin.get_user_id(username)
keycloak_admin.group_user_add(user_id=user_id, group_id=group_id)


add_users_to_group("developers", ["user1", "user2"])
add_users_to_group("testers", ["user2", "user3"])
add_users_to_group("admins", ["user3", "user1"])


# Create realm roles
def create_realm_role(role_name: str):
keycloak_admin.create_realm_role({"name": role_name})


REALM_ROLES = ["manage-users", "view-realm", "manage-clients"]
for role in REALM_ROLES:
create_realm_role(role)


# Assign realm roles to groups
def assign_realm_roles_to_group(group_name: str, role_names: list):
group_id = keycloak_admin.get_group_by_path(f"/{group_name}")["id"]
roles = [keycloak_admin.get_realm_role(role_name) for role_name in role_names]
keycloak_admin.assign_group_realm_roles(group_id=group_id, roles=roles)


assign_realm_roles_to_group("developers", ["view-realm"])
assign_realm_roles_to_group("admins", ["manage-users", "manage-clients"])


# Create client roles
def create_client_role(client_id: str, role_name: str):
client_uuid = keycloak_admin.get_client_id(client_id)
keycloak_admin.create_client_role(client_uuid, {"name": role_name})


CLIENT_ROLES = {
"read-data": ["scope:read"],
"write-data": ["scope:write"],
"delete-data": ["scope:delete"],
}

for role_name, scopes in CLIENT_ROLES.items():
create_client_role(KEYCLOAK_CLIENT_ID, role_name)


# Assign client roles to groups
def assign_client_roles_to_group(group_name: str, client_id: str, role_names: list):
group_id = keycloak_admin.get_group_by_path(f"/{group_name}")["id"]
client_uuid = keycloak_admin.get_client_id(client_id)
roles = [
keycloak_admin.get_client_role(client_uuid, role_name)
for role_name in role_names
]
keycloak_admin.assign_group_client_roles(
group_id=group_id, client_id=client_uuid, roles=roles
)


assign_client_roles_to_group(
"developers", KEYCLOAK_CLIENT_ID, ["read-data", "write-data"]
)
assign_client_roles_to_group("testers", KEYCLOAK_CLIENT_ID, ["read-data"])
assign_client_roles_to_group(
"admins", KEYCLOAK_CLIENT_ID, ["read-data", "write-data", "delete-data"]
)


# Configure authentication flows
def create_auth_flow(flow_alias: str):
keycloak_admin.create_authentication_flow(
{
"alias": flow_alias,
"description": "Custom authentication flow for development",
"providerId": "basic-flow",
"topLevel": True,
"builtIn": False,
}
)


create_auth_flow("dev-auth-flow")

# Set the custom flow as the realm's browser flow
keycloak_admin.update_realm(KEYCLOAK_REALM, {"browserFlow": "dev-auth-flow"})


# Add identity provider (e.g., GitHub)
def create_identity_provider(alias: str, provider_id: str, config: dict):
payload = {
"alias": alias,
"displayName": alias.capitalize(),
"providerId": provider_id,
"enabled": True,
"config": config,
}
keycloak_admin.create_identity_provider(payload)


create_identity_provider(
alias="github",
provider_id="github",
config={
"clientId": "your-github-client-id",
"clientSecret": "your-github-client-secret",
"useJwksUrl": "true",
"jwksUrl": "https://github.com/login/oauth/access_token",
"authorizationUrl": "https://github.com/login/oauth/authorize",
"tokenUrl": "https://github.com/login/oauth/access_token",
"defaultScope": "user:email",
},
)

# Update realm-level settings (e.g., password policy)
keycloak_admin.update_realm(
KEYCLOAK_REALM,
{
"passwordPolicy": "length(8) and notUsername(#2) and regexPattern('^[a-zA-Z0-9]+$')",
"smtpServer": {
"host": "smtp.example.com",
"port": "587",
"from": "noreply@example.com",
"auth": "true",
"user": "smtp-user",
"password": "smtp-password",
},
},
)

print("Development realm setup is complete.")
Loading
Loading