feat: add keycloak and postgres

This commit is contained in:
Masaki Yatsu
2025-08-15 11:21:54 +09:00
parent 81016267af
commit ed1ba74797
20 changed files with 1656 additions and 0 deletions

1
keycloak/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
keycloak-values.yaml

408
keycloak/justfile Normal file
View File

@@ -0,0 +1,408 @@
set fallback := true
export KEYCLOAK_NAMESPACE := env("KEYCLOAK_NAMESPACE", "keycloak")
export KEYCLOAK_CHART_VERSION := env("KEYCLOAK_CHART_VERSION", "25.0.2")
export KEYCLOAK_REALM := env("KEYCLOAK_REALM", "")
export KEYCLOAK_HOST := env("KEYCLOAK_HOST", "")
export K8S_OIDC_CLIENT_ID := env('K8S_OIDC_CLIENT_ID', "k8s")
export KEYCLOAK_ADMIN_USER := env("KEYCLOAK_ADMIN_USER", "")
export KEYCLOAK_ADMIN_PASSWORD := env("KEYCLOAK_ADMIN_PASSWORD", "")
export VAULT_ENABLED := env("VAULT_ENABLED", "true")
[private]
default:
@just --list --unsorted --list-submodules
# Create Keycloak namespace
create-namespace:
@kubectl get namespace ${KEYCLOAK_NAMESPACE} &>/dev/null || \
kubectl create namespace ${KEYCLOAK_NAMESPACE}
# Delete Keycloak namespace
delete-namespace:
@kubectl delete namespace ${KEYCLOAK_NAMESPACE} --ignore-not-found
# Create Keycloak secret
create-credentials:
#!/bin/bash
set -euo pipefail
admin_user=$(gum input --prompt="Initial Keycloak admin username: " --width=100 --value="admin")
password=$(
gum input --prompt="Initial Keycloak admin password: " --password --width=100 \
--placeholder="Empty to generate a random password"
)
if [ -z "${password}" ]; then
password=$(just utils::random-password)
fi
just create-namespace
if kubectl get secret keycloak-credentials -n ${KEYCLOAK_NAMESPACE} &>/dev/null; then
kubectl delete --ignore-not-found secret keycloak-credentials -n ${KEYCLOAK_NAMESPACE}
fi
kubectl create secret generic keycloak-credentials -n ${KEYCLOAK_NAMESPACE} \
--from-literal=admin-user="${admin_user}" \
--from-literal=password="${password}"
if [ "${VAULT_ENABLED}" != "false" ]; then
just put-admin-credentials-to-vault "${admin_user}" "${password}"
fi
# Delete Keycloak secret
delete-credentials:
@kubectl delete secret keycloak-credentials -n ${KEYCLOAK_NAMESPACE} --ignore-not-found
# Create Keycloak database secret
create-database-secret:
#!/bin/bash
set -euo pipefail
if kubectl get secret database-config -n ${KEYCLOAK_NAMESPACE} &>/dev/null; then
kubectl delete secret database-config -n ${KEYCLOAK_NAMESPACE}
fi
kubectl create secret generic database-config -n ${KEYCLOAK_NAMESPACE} \
--from-literal=host=postgres-cluster-rw.postgres \
--from-literal=port=5432 \
--from-literal=user=$(just postgres::admin-username) \
--from-literal=password=$(just postgres::admin-password) \
--from-literal=database=keycloak
# Delete Keycloak database secret
delete-database-secret:
@kubectl delete secret database-config -n ${KEYCLOAK_NAMESPACE} --ignore-not-found
# Install Keycloak
install:
#!/bin/bash
set -euxo pipefail
just create-credentials
just postgres::create-db keycloak
just create-database-secret
KEYCLOAK_ADMIN_USER=$(just admin-username) \
gomplate -f keycloak-values.gomplate.yaml -o keycloak-values.yaml
helm upgrade --cleanup-on-fail --install \
keycloak oci://registry-1.docker.io/bitnamicharts/keycloak \
--version ${KEYCLOAK_CHART_VERSION} -n ${KEYCLOAK_NAMESPACE} --wait \
-f keycloak-values.yaml
# Uninstall Keycloak
uninstall delete-db='true':
#!/bin/bash
set -euo pipefail
helm uninstall keycloak -n ${KEYCLOAK_NAMESPACE} --ignore-not-found --wait
just delete-namespace
if [ "{{ delete-db }}" = "true" ]; then
just postgres::delete-db keycloak
fi
# Create Keycloak realm
create-realm create-client-for-k8s='true':
#!/bin/bash
set -euo pipefail
export KEYCLOAK_ADMIN_USER=$(just admin-username)
export KEYCLOAK_ADMIN_PASSWORD=$(just admin-password)
dotenvx run -f ../.env.local -- tsx ./scripts/create-realm.ts
if [ "{{ create-client-for-k8s }}" = "true" ]; then
just create-client ${KEYCLOAK_REALM} ${K8S_OIDC_CLIENT_ID} "http://localhost:8000"
fi
# Delete Keycloak realm
delete-realm realm:
#!/bin/bash
set -euo pipefail
if [ -z "{{ realm }}" ]; then
echo "Error: Realm name to delete must be provided as an argument." >&2
echo "Usage: just delete-realm <realm-to-delete>" >&2
exit 1
fi
if [ "{{ realm }}" = "master" ]; then
echo "Error: Deleting the 'master' realm is not allowed via this script." >&2
exit 1
fi
echo "WARNING: You are about to delete the Keycloak realm named '{{ realm }}'."
echo "This action is irreversible and will remove all users, clients, and configurations within this realm."
if ! gum confirm "Are you absolutely sure you want to delete realm '{{ realm }}'?"; then
echo "Realm deletion cancelled."
exit 0
fi
export KEYCLOAK_ADMIN_USER=$(just admin-username)
export KEYCLOAK_ADMIN_PASSWORD=$(just admin-password)
export KEYCLOAK_REALM_TO_DELETE={{ realm }}
dotenvx run -f ../.env.local -- tsx ./scripts/delete-realm.ts
# Create Keycloak client
create-client realm client_id redirect_url client_secret='':
#!/bin/bash
set -euo pipefail
export KEYCLOAK_ADMIN_USER=$(just admin-username)
export KEYCLOAK_ADMIN_PASSWORD=$(just admin-password)
export KEYCLOAK_REALM={{ realm }}
export KEYCLOAK_CLIENT_ID={{ client_id }}
export KEYCLOAK_CLIENT_SECRET={{ client_secret }}
export KEYCLOAK_REDIRECT_URL={{ redirect_url }}
dotenvx run -f ../.env.local -- tsx ./scripts/create-client.ts
# Delete Keycloak client
delete-client realm client_id:
#!/bin/bash
set -euo pipefail
export KEYCLOAK_ADMIN_USER=$(just admin-username)
export KEYCLOAK_ADMIN_PASSWORD=$(just admin-password)
export KEYCLOAK_REALM={{ realm }}
export KEYCLOAK_CLIENT_ID={{ client_id }}
dotenvx run -f ../.env.local -- tsx ./scripts/delete-client.ts
# Add Keycloak client audience mapper
add-audience-mapper client_id:
#!/bin/bash
set -euo pipefail
export KEYCLOAK_ADMIN_USER=$(just admin-username)
export KEYCLOAK_ADMIN_PASSWORD=$(just admin-password)
export KEYCLOAK_REALM=${KEYCLOAK_REALM}
export KEYCLOAK_CLIENT_ID={{ client_id }}
dotenvx run -f ../.env.local -- tsx ./scripts/add-audience-mapper.ts
# Create Keycloak group
create-group group_name parent_group='' description='':
#!/bin/bash
set -euo pipefail
export KEYCLOAK_ADMIN_USER=$(just admin-username)
export KEYCLOAK_ADMIN_PASSWORD=$(just admin-password)
export GROUP_NAME="{{ group_name }}"
export PARENT_GROUP_NAME="{{ parent_group }}"
export GROUP_DESCRIPTION="{{ description }}"
dotenvx run -f ../.env.local -- tsx ./scripts/create-group.ts
# Create Keycloak user
create-user username='' password='' email='' first_name='' last_name='' vault_admin='false':
#!/bin/bash
set -euo pipefail
export KEYCLOAK_ADMIN_USER=$(just admin-username)
export KEYCLOAK_ADMIN_PASSWORD=$(just admin-password)
export USERNAME="{{ username }}"
export PASSWORD="{{ password }}"
while [ -z "${USERNAME}" ]; do
USERNAME=$(gum input --prompt="Username: " --width=100)
done
while [ -z "${PASSWORD}" ]; do
PASSWORD=$(gum input --prompt="Password: " --password --width=100)
done
export EMAIL="{{ email }}"
while [ -z "${EMAIL}" ]; do
EMAIL=$(gum input --prompt="Email: " --width=100)
done
export FIRST_NAME="{{ first_name }}"
while [ -z "${FIRST_NAME}" ]; do
FIRST_NAME=$(gum input --prompt="First name: " --width=100)
done
export LAST_NAME="{{ last_name }}"
while [ -z "${LAST_NAME}" ]; do
LAST_NAME=$(gum input --prompt="Last name: " --width=100)
done
# Ask if user should be vault admin
VAULT_ADMIN="{{ vault_admin }}"
if [ "${VAULT_ADMIN}" = "false" ]; then
if gum confirm "Should this user have Vault admin access?"; then
VAULT_ADMIN="true"
fi
fi
# Create user
dotenvx run -f ../.env.local -- tsx ./scripts/create-user.ts
# Set up Kubernetes RBAC
kubectl delete clusterrolebinding oidc-${USERNAME} --ignore-not-found
kubectl create clusterrolebinding oidc-${USERNAME} --clusterrole=cluster-admin \
--user="https://${KEYCLOAK_HOST}/realms/${KEYCLOAK_REALM}#${USERNAME}"
# Add to vault-admins group if requested
if [ "${VAULT_ADMIN}" = "true" ]; then
echo "Setting up vault-admins group..."
# Create vault-admins group if it doesn't exist
just create-group "vault-admins" "" "Vault administrators group" || true
# Add user to vault-admins group
export GROUP_NAME="vault-admins"
dotenvx run -f ../.env.local -- tsx ./scripts/add-user-to-group.ts
echo "✓ User '${USERNAME}' added to vault-admins group"
fi
# Add user to group
add-user-to-group username group_name:
#!/bin/bash
set -euo pipefail
export KEYCLOAK_ADMIN_USER=$(just admin-username)
export KEYCLOAK_ADMIN_PASSWORD=$(just admin-password)
export USERNAME="{{ username }}"
export GROUP_NAME="{{ group_name }}"
dotenvx run -f ../.env.local -- tsx ./scripts/add-user-to-group.ts
# Remove user from group
remove-user-from-group username group_name:
#!/bin/bash
set -euo pipefail
export KEYCLOAK_ADMIN_USER=$(just admin-username)
export KEYCLOAK_ADMIN_PASSWORD=$(just admin-password)
export USERNAME="{{ username }}"
export GROUP_NAME="{{ group_name }}"
dotenvx run -f ../.env.local -- tsx ./scripts/delete-user-from-group.ts
# Delete Keycloak group
delete-group group_name:
#!/bin/bash
set -euo pipefail
if [ -z "{{ group_name }}" ]; then
echo "Error: Group name to delete must be provided as an argument." >&2
echo "Usage: just delete-group <group-name>" >&2
exit 1
fi
if [ "{{ group_name }}" = "vault-admins" ]; then
echo "WARNING: You are about to delete the 'vault-admins' group."
echo "This will remove Vault admin access for all users in this group."
if ! gum confirm "Are you sure you want to delete the '{{ group_name }}' group?"; then
echo "Group deletion cancelled."
exit 0
fi
else
echo "You are about to delete the Keycloak group '{{ group_name }}'."
if ! gum confirm "Are you sure you want to delete the '{{ group_name }}' group?"; then
echo "Group deletion cancelled."
exit 0
fi
fi
export KEYCLOAK_ADMIN_USER=$(just admin-username)
export KEYCLOAK_ADMIN_PASSWORD=$(just admin-password)
export GROUP_NAME="{{ group_name }}"
dotenvx run -f ../.env.local -- tsx ./scripts/delete-group.ts
# Delete a user
delete-user username='':
#!/bin/bash
set -euo pipefail
export KEYCLOAK_ADMIN_USER=$(just admin-username)
export KEYCLOAK_ADMIN_PASSWORD=$(just admin-password)
export USERNAME="{{ username }}"
while [ -z "${USERNAME}" ]; do
USERNAME=$(gum input --prompt="Username: " --width=100)
done
dotenvx run -f ../.env.local -- tsx ./scripts/delete-user.ts
# Create an admin user
# create-admin-user username='' password='':
# #!/bin/bash
# set -euo pipefail
# echo "Creating a new admin user in Keycloak"
# export KEYCLOAK_ADMIN_USER=$(just admin-user)
# export KEYCLOAK_ADMIN_PASSWORD=$(just admin-password)
# export USERNAME="{{ username }}"
# export PASSWORD="{{ password }}"
# while [ -z "${USERNAME}" ]; do
# USERNAME=$(gum input --prompt="Admin username: " --width=100)
# done
# if [ -z "${PASSWORD}" ]; then
# PASSWORD=$(
# gum input --prompt="Admin assword: " --password --width=100 \
# --placeholder="Empty to generate a random password"
# )
# fi
# if [ -z "${PASSWORD}" ]; then
# PASSWORD=$(just utils::random-password)
# fi
# export EMAIL=""
# export FIRST_NAME=""
# export LAST_NAME=""
# export CREATE_AS_ADMIN=true
# dotenvx run -f ../.env.local -- tsx ./scripts/create-user.ts
# if [ "${VAULT_ENABLED}" != "false" ]; then
# just put-admin-credentials-to-vault "${USERNAME}" "${PASSWORD}"
# fi
# Put admin credentials to Vault
put-admin-credentials-to-vault username password:
@just vault::put keycloak/admin username={{ username }} password={{ password }}
@echo "Admin credentials stored in Vault under 'keycloak/admin'."
# Delete admin credentials from Vault
delete-admin-credentials-from-vault:
@just vault::delete keycloak/admin
@echo "Admin credentials deleted from Vault."
# Create system user {w/o email, first name and last name}
create-system-user username='' password='':
#!/bin/bash
set -euo pipefail
export KEYCLOAK_ADMIN_USER=$(just admin-user)
export KEYCLOAK_ADMIN_PASSWORD=$(just admin-password)
export USERNAME="{{ username }}"
export PASSWORD="{{ password }}"
while [ -z "${USERNAME}" ]; do
USERNAME=$(gum input --prompt="Keycloak username: " --width=100)
done
while [ -z "${PASSWORD}" ]; do
PASSWORD=$(gum input --prompt="Keycloak password: " --password --width=100)
done
export EMAIL=""
export FIRST_NAME=""
export LAST_NAME=""
dotenvx run -f ../.env.local -- tsx ./scripts/create-user.ts
kubectl delete clusterrolebinding oidc-${USERNAME} --ignore-not-found
kubectl create clusterrolebinding oidc-${USERNAME} --clusterrole=cluster-admin \
--user="https://${KEYCLOAK_HOST}/realms/${KEYCLOAK_REALM}#${USERNAME}"
# Check if user exists
user-exists username='':
#!/bin/bash
set -euo pipefail
export KEYCLOAK_ADMIN_USER=$(just admin-user)
export KEYCLOAK_ADMIN_PASSWORD=$(just admin-password)
export USERNAME="{{ username }}"
while [ -z "${USERNAME}" ]; do
USERNAME=$(gum input --prompt="Username: " --width=100)
done
dotenvx run -f ../.env.local -- tsx ./scripts/user-exists.ts
# Print Keycloak admin username
admin-username:
#!/bin/bash
set -euxo pipefail
if [ -n "${KEYCLOAK_ADMIN_USER}" ]; then
echo "${KEYCLOAK_ADMIN_USER}"
exit 0
fi
if [ "${VAULT_ENABLED}" != "false" ]; then
just vault::setup-env
if just vault::exist keycloak/admin; then
just vault::get keycloak/admin username
echo
exit 0
fi
fi
just default-admin-username
# Print Keycloak admin password
admin-password:
#!/bin/bash
set -euo pipefail
if [ -n "${KEYCLOAK_ADMIN_PASSWORD}" ]; then
echo "${KEYCLOAK_ADMIN_PASSWORD}"
exit 0
fi
if [ "${VAULT_ENABLED}" != "false" ]; then
just vault::setup-env
if just vault::exist keycloak/admin; then
just vault::get keycloak/admin password
echo
exit 0
fi
fi
just default-admin-password
# Print default Keycloak admin username
default-admin-username:
@kubectl get secret keycloak-credentials -n ${KEYCLOAK_NAMESPACE} \
-o jsonpath="{.data.admin-user}" | base64 --decode
@echo
# Print default Keycloak admin password
default-admin-password:
@kubectl get secret keycloak-credentials -n ${KEYCLOAK_NAMESPACE} \
-o jsonpath="{.data.password}" | base64 --decode
@echo

View File

@@ -0,0 +1,43 @@
auth:
adminUser: {{ .Env.KEYCLOAK_ADMIN_USER }}
existingSecret: keycloak-credentials
passwordSecretKey: password
postgresql:
enabled: false
externalDatabase:
host: postgres-cluster-rw.postgres
port: 5432
database: keycloak
existingSecret: database-config
existingSecretUserKey: user
existingSecretPasswordKey: password
tls:
enabled: true
autoGenerated:
enabled: true
engine: helm
# Keycloak pod may not start with the default memory limits
resources:
limits:
memory: 1.5Gi
requests:
memory: 1Gi
# image:
# debug: true
# logging:
# level: DEBUG
ingress:
enabled: true
ingressClassName: traefik
hostname: {{ .Env.KEYCLOAK_HOST }}
annotations:
kubernetes.io/ingress.class: traefik
traefik.ingress.kubernetes.io/router.entrypoints: websecure
tls: true

View File

@@ -0,0 +1,70 @@
import KcAdminClient from "@keycloak/keycloak-admin-client";
import invariant from "tiny-invariant";
const main = async () => {
const keycloakHost = process.env.KEYCLOAK_HOST;
invariant(keycloakHost, "KEYCLOAK_HOST environment variable is required.");
const adminUsername = process.env.KEYCLOAK_ADMIN_USER;
invariant(adminUsername, "KEYCLOAK_ADMIN_USER environment variable is required.");
const adminPassword = process.env.KEYCLOAK_ADMIN_PASSWORD;
invariant(adminPassword, "KEYCLOAK_ADMIN_PASSWORD environment variable is required");
const realmName = process.env.KEYCLOAK_REALM;
invariant(realmName, "KEYCLOAK_REALM environment variable is required");
const clientId = process.env.KEYCLOAK_CLIENT_ID;
invariant(clientId, "KEYCLOAK_CLIENT_ID environment variable is required");
const kcAdminClient = new KcAdminClient({
baseUrl: `https://${keycloakHost}`,
realmName: "master",
});
try {
await kcAdminClient.auth({
username: adminUsername,
password: adminPassword,
grantType: "password",
clientId: "admin-cli",
});
console.log("Authentication successful.");
kcAdminClient.setConfig({ realmName });
const clients = await kcAdminClient.clients.find({ clientId });
if (clients.length === 0) {
throw new Error(`Client '${clientId}' not found.`);
}
const client = clients[0];
invariant(client.id, "Client ID is not set");
const mapperName = `aud-mapper-${clientId}`;
const audienceMapper = {
name: mapperName,
protocol: "openid-connect",
protocolMapper: "oidc-audience-mapper",
config: {
"included.client.audience": clientId,
"id.token.claim": "true",
"access.token.claim": "true",
},
};
const existingMappers = await kcAdminClient.clients.listProtocolMappers({ id: client.id });
if (existingMappers.some((mapper) => mapper.name === mapperName)) {
console.warn(`Audience Mapper '${mapperName}' already exists for the client.`);
} else {
await kcAdminClient.clients.addProtocolMapper({ id: client.id }, audienceMapper);
console.log(`Audience Mapper '${mapperName}' added directly to the client.`);
}
} catch (error) {
console.error("An error occurred:", error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}
};
main();

View File

@@ -0,0 +1,121 @@
import KcAdminClient from "@keycloak/keycloak-admin-client";
import invariant from "tiny-invariant";
const main = async () => {
const keycloakHost = process.env.KEYCLOAK_HOST;
invariant(keycloakHost, "KEYCLOAK_HOST environment variable is required.");
const adminUsername = process.env.KEYCLOAK_ADMIN_USER;
invariant(adminUsername, "KEYCLOAK_ADMIN_USER environment variable is required.");
const adminPassword = process.env.KEYCLOAK_ADMIN_PASSWORD;
invariant(adminPassword, "KEYCLOAK_ADMIN_PASSWORD environment variable is required");
const realmName = process.env.KEYCLOAK_REALM;
invariant(realmName, "KEYCLOAK_REALM environment variable is required");
const minioClientId = process.env.MINIO_OIDC_CLIENT_ID;
invariant(minioClientId, "MINIO_OIDC_CLIENT_ID environment variable is required");
const policyValue = process.env.MINIO_POLICY || "readwrite";
console.log(`Setting minioPolicy attribute with value: ${policyValue}`);
const kcAdminClient = new KcAdminClient({
baseUrl: `https://${keycloakHost}`,
realmName: "master",
});
try {
await kcAdminClient.auth({
username: adminUsername,
password: adminPassword,
grantType: "password",
clientId: "admin-cli",
});
console.log("Authentication successful.");
// Set realm to work with
kcAdminClient.setConfig({
realmName,
});
// Get current User Profile configuration
const userProfile = await kcAdminClient.users.getProfile();
// Check if minioPolicy attribute already exists
const existingAttribute = userProfile.attributes?.find(
(attr: any) => attr.name === "minioPolicy"
);
if (existingAttribute) {
console.log("minioPolicy attribute already exists in User Profile.");
} else {
// Add minioPolicy attribute to User Profile with proper permissions
if (!userProfile.attributes) {
userProfile.attributes = [];
}
userProfile.attributes.push({
name: "minioPolicy",
displayName: "MinIO Policy",
permissions: {
view: ["admin", "user"],
edit: ["admin"],
},
validations: {
options: { options: ["readwrite", "readonly", "writeonly"] },
},
});
// Update User Profile
await kcAdminClient.users.updateProfile(userProfile);
console.log(
"minioPolicy attribute added to User Profile successfully with admin edit permissions."
);
}
// Create protocol mapper for the minioPolicy attribute if it doesn't exist
const minioClient = await kcAdminClient.clients.find({ clientId: minioClientId });
if (minioClient.length === 0) {
console.error(`Client '${minioClientId}' not found.`);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}
const clientId = minioClient[0].id;
invariant(clientId, "Client ID is required");
// Check if the mapper already exists
const mappers = await kcAdminClient.clients.listProtocolMappers({ id: clientId });
const existingMapper = mappers.find((mapper) => mapper.name === "MinIO Policy");
if (existingMapper) {
console.log("MinIO Policy mapper already exists.");
} else {
// Create the protocol mapper
await kcAdminClient.clients.addProtocolMapper(
{ id: clientId },
{
name: "MinIO Policy",
protocol: "openid-connect",
protocolMapper: "oidc-usermodel-attribute-mapper",
config: {
"userinfo.token.claim": "true",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "minioPolicy",
"jsonType.label": "String",
"user.attribute": "minioPolicy",
multivalued: "false",
},
}
);
console.log("MinIO Policy mapper created successfully.");
}
} catch (error) {
console.error("An error occurred:", error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}
};
main();

View File

@@ -0,0 +1,76 @@
import KcAdminClient from "@keycloak/keycloak-admin-client";
import invariant from "tiny-invariant";
const main = async () => {
const keycloakHost = process.env.KEYCLOAK_HOST;
invariant(keycloakHost, "KEYCLOAK_HOST environment variable is required.");
const adminUsername = process.env.KEYCLOAK_ADMIN_USER;
invariant(adminUsername, "KEYCLOAK_ADMIN_USER environment variable is required.");
const adminPassword = process.env.KEYCLOAK_ADMIN_PASSWORD;
invariant(adminPassword, "KEYCLOAK_ADMIN_PASSWORD environment variable is required");
const realmName = process.env.KEYCLOAK_REALM;
invariant(realmName, "KEYCLOAK_REALM environment variable is required");
const username = process.env.USERNAME;
invariant(username, "USERNAME environment variable is required");
const groupName = process.env.GROUP_NAME;
invariant(groupName, "GROUP_NAME environment variable is required");
const kcAdminClient = new KcAdminClient({
baseUrl: `https://${keycloakHost}`,
realmName: "master",
});
try {
await kcAdminClient.auth({
username: adminUsername,
password: adminPassword,
grantType: "password",
clientId: "admin-cli",
});
console.log("Authentication successful.");
kcAdminClient.setConfig({ realmName });
// Find user
const users = await kcAdminClient.users.find({ username });
const user = users.find(u => u.username === username);
if (!user) {
throw new Error(`User '${username}' not found`);
}
// Find group
const groups = await kcAdminClient.groups.find({ search: groupName });
const group = groups.find(g => g.name === groupName);
if (!group) {
throw new Error(`Group '${groupName}' not found`);
}
// Check if user is already in group
const userGroups = await kcAdminClient.users.listGroups({ id: user.id! });
const isAlreadyMember = userGroups.some(ug => ug.id === group.id);
if (isAlreadyMember) {
console.log(`User '${username}' is already a member of group '${groupName}'`);
return;
}
// Add user to group
await kcAdminClient.users.addToGroup({
id: user.id!,
groupId: group.id!,
});
console.log(`User '${username}' added to group '${groupName}' successfully`);
} catch (error) {
console.error("Error adding user to group:", error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}
};
main();

View File

@@ -0,0 +1,64 @@
import KcAdminClient from "@keycloak/keycloak-admin-client";
import invariant from "tiny-invariant";
const main = async () => {
const keycloakHost = process.env.KEYCLOAK_HOST;
invariant(keycloakHost, "KEYCLOAK_HOST environment variable is required.");
const adminUsername = process.env.KEYCLOAK_ADMIN_USER;
invariant(adminUsername, "KEYCLOAK_ADMIN_USER environment variable is required.");
const adminPassword = process.env.KEYCLOAK_ADMIN_PASSWORD;
invariant(adminPassword, "KEYCLOAK_ADMIN_PASSWORD environment variable is required");
const realmName = process.env.KEYCLOAK_REALM;
invariant(realmName, "KEYCLOAK_REALM environment variable is required");
const clientId = process.env.KEYCLOAK_CLIENT_ID;
invariant(clientId, "KEYCLOAK_CLIENT_ID environment variable is required");
const clientSecret = process.env.KEYCLOAK_CLIENT_SECRET;
const redirectUrl = process.env.KEYCLOAK_REDIRECT_URL;
invariant(redirectUrl, "KEYCLOAK_REDIRECT_URL environment variable is required");
const redirectUris = redirectUrl.split(',').map(url => url.trim());
const kcAdminClient = new KcAdminClient({
baseUrl: `https://${keycloakHost}`,
realmName: "master",
});
try {
await kcAdminClient.auth({
username: adminUsername,
password: adminPassword,
grantType: "password",
clientId: "admin-cli",
});
console.log("Authentication successful.");
kcAdminClient.setConfig({ realmName });
const existingClients = await kcAdminClient.clients.find({ clientId });
if (existingClients.length > 0) {
console.warn(`Client '${clientId}' already exists.`);
return;
}
const createdClient = await kcAdminClient.clients.create({
clientId: clientId,
secret: clientSecret,
enabled: true,
redirectUris: redirectUris,
publicClient: clientSecret && clientSecret !== '' ? false : true,
});
console.log(`Client created successfully with ID: ${createdClient.id}`);
} catch (error) {
console.error("An error occurred:", error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}
};
main();

View File

@@ -0,0 +1,82 @@
import KcAdminClient from "@keycloak/keycloak-admin-client";
import invariant from "tiny-invariant";
const main = async () => {
const keycloakHost = process.env.KEYCLOAK_HOST;
invariant(keycloakHost, "KEYCLOAK_HOST environment variable is required.");
const adminUsername = process.env.KEYCLOAK_ADMIN_USER;
invariant(adminUsername, "KEYCLOAK_ADMIN_USER environment variable is required.");
const adminPassword = process.env.KEYCLOAK_ADMIN_PASSWORD;
invariant(adminPassword, "KEYCLOAK_ADMIN_PASSWORD environment variable is required");
const realmName = process.env.KEYCLOAK_REALM;
invariant(realmName, "KEYCLOAK_REALM environment variable is required");
const groupName = process.env.GROUP_NAME;
invariant(groupName, "GROUP_NAME environment variable is required");
const parentGroupName = process.env.PARENT_GROUP_NAME || "";
const groupDescription = process.env.GROUP_DESCRIPTION || "";
const kcAdminClient = new KcAdminClient({
baseUrl: `https://${keycloakHost}`,
realmName: "master",
});
try {
await kcAdminClient.auth({
username: adminUsername,
password: adminPassword,
grantType: "password",
clientId: "admin-cli",
});
console.log("Authentication successful.");
kcAdminClient.setConfig({ realmName });
// Check if group already exists
const existingGroups = await kcAdminClient.groups.find({ search: groupName });
const existingGroup = existingGroups.find(group => group.name === groupName);
if (existingGroup) {
console.log(`Group '${groupName}' already exists with ID: ${existingGroup.id}`);
return;
}
// Find parent group if specified
let parentGroupId: string | undefined;
if (parentGroupName) {
const parentGroups = await kcAdminClient.groups.find({ search: parentGroupName });
const parentGroup = parentGroups.find(group => group.name === parentGroupName);
if (!parentGroup) {
throw new Error(`Parent group '${parentGroupName}' not found`);
}
parentGroupId = parentGroup.id;
}
// Create group payload
const groupPayload = {
name: groupName,
...(groupDescription && { attributes: { description: [groupDescription] } }),
};
// Create group
const group = parentGroupId
? await kcAdminClient.groups.createChildGroup({ id: parentGroupId }, groupPayload)
: await kcAdminClient.groups.create(groupPayload);
console.log(`Group '${groupName}' created successfully with ID: ${group.id}`);
if (parentGroupName) {
console.log(`Group '${groupName}' created as child of '${parentGroupName}'`);
}
} catch (error) {
console.error("Error creating group:", error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}
};
main();

View File

@@ -0,0 +1,50 @@
import KcAdminClient from "@keycloak/keycloak-admin-client";
import invariant from "tiny-invariant";
const main = async () => {
const keycloakHost = process.env.KEYCLOAK_HOST;
invariant(keycloakHost, "KEYCLOAK_HOST environment variable is required.");
const adminUsername = process.env.KEYCLOAK_ADMIN_USER;
invariant(adminUsername, "KEYCLOAK_ADMIN_USER environment variable is required.");
const adminPassword = process.env.KEYCLOAK_ADMIN_PASSWORD;
invariant(adminPassword, "KEYCLOAK_ADMIN_PASSWORD environment variable is required");
const realmName = process.env.KEYCLOAK_REALM;
invariant(realmName, "KEYCLOAK_REALM environment variable is required");
const kcAdminClient = new KcAdminClient({
baseUrl: `https://${keycloakHost}`,
realmName: "master",
});
try {
await kcAdminClient.auth({
username: adminUsername,
password: adminPassword,
grantType: "password",
clientId: "admin-cli",
});
console.log("Authentication successful.");
const existingRealms = await kcAdminClient.realms.find();
const realmExists = existingRealms.some((realm) => realm.realm === realmName);
if (realmExists) {
console.warn(`Realm '${realmName}' already exists.`);
return;
}
await kcAdminClient.realms.create({
realm: realmName,
enabled: true,
});
console.log(`Realm '${realmName}' created successfully.`);
} catch (error) {
console.error("An error occurred:", error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}
};
main();

View File

@@ -0,0 +1,99 @@
import KcAdminClient from "@keycloak/keycloak-admin-client";
import invariant from "tiny-invariant";
const main = async () => {
const keycloakHost = process.env.KEYCLOAK_HOST;
invariant(keycloakHost, "KEYCLOAK_HOST environment variable is required.");
const adminUsername = process.env.KEYCLOAK_ADMIN_USER;
invariant(adminUsername, "KEYCLOAK_ADMIN_USER environment variable is required.");
const adminPassword = process.env.KEYCLOAK_ADMIN_PASSWORD;
invariant(adminPassword, "KEYCLOAK_ADMIN_PASSWORD environment variable is required");
const realmName = process.env.KEYCLOAK_REALM;
invariant(realmName, "KEYCLOAK_REALM environment variable is required");
const username = process.env.USERNAME;
invariant(username, "USERNAME environment variable is required");
const email = process.env.EMAIL;
const firstName = process.env.FIRST_NAME;
const lastName = process.env.LAST_NAME;
const password = process.env.PASSWORD;
invariant(password, "PASSWORD environment variable is required");
const createAsAdmin = process.env.CREATE_AS_ADMIN === "true";
const kcAdminClient = new KcAdminClient({
baseUrl: `https://${keycloakHost}`,
realmName: "master",
});
try {
await kcAdminClient.auth({
username: adminUsername,
password: adminPassword,
grantType: "password",
clientId: "admin-cli",
});
console.log("Authentication successful.");
kcAdminClient.setConfig({ realmName });
const userPayload = {
username,
email,
emailVerified: true,
firstName,
lastName,
enabled: true,
};
const user = await kcAdminClient.users.create(userPayload);
console.log(`User created successfully with ID: ${user.id}`);
if (createAsAdmin && realmName === "master") {
const adminRole = await kcAdminClient.roles.findOneByName({
realm: "master",
name: "admin",
});
const createRealmRole = await kcAdminClient.roles.findOneByName({
realm: "master",
name: "create-realm",
});
await kcAdminClient.users.addRealmRoleMappings({
realm: "master",
id: user.id,
roles: [
{
id: adminRole!.id!,
name: adminRole!.name!,
},
{
id: createRealmRole!.id!,
name: createRealmRole!.name!,
},
],
});
}
await kcAdminClient.users.resetPassword({
id: user.id!,
credential: {
type: "password",
value: password,
temporary: false,
},
});
console.log(`Password set for user '${user.id}'.`);
} catch (error) {
console.error("Error creating user:", error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}
};
main();

View File

@@ -0,0 +1,54 @@
import KcAdminClient from "@keycloak/keycloak-admin-client";
import invariant from "tiny-invariant";
const main = async () => {
const keycloakHost = process.env.KEYCLOAK_HOST;
invariant(keycloakHost, "KEYCLOAK_HOST environment variable is required.");
const adminUsername = process.env.KEYCLOAK_ADMIN_USER;
invariant(adminUsername, "KEYCLOAK_ADMIN_USER environment variable is required.");
const adminPassword = process.env.KEYCLOAK_ADMIN_PASSWORD;
invariant(adminPassword, "KEYCLOAK_ADMIN_PASSWORD environment variable is required");
const realmName = process.env.KEYCLOAK_REALM;
invariant(realmName, "KEYCLOAK_REALM environment variable is required");
const clientId = process.env.KEYCLOAK_CLIENT_ID;
invariant(clientId, "KEYCLOAK_CLIENT_ID environment variable is required");
const kcAdminClient = new KcAdminClient({
baseUrl: `https://${keycloakHost}`,
realmName: 'master',
});
try {
await kcAdminClient.auth({
username: adminUsername,
password: adminPassword,
grantType: "password",
clientId: "admin-cli",
});
console.log("Authentication successful.");
kcAdminClient.setConfig({ realmName });
const existingClients = await kcAdminClient.clients.find({ clientId });
if (existingClients.length === 0) {
console.warn(`Client '${clientId}' does not exist.`);
return;
}
const client = existingClients[0];
invariant(client.id, "Client ID is not set");
await kcAdminClient.clients.del({ id: client.id });
console.log(`Client '${clientId}' successfully deleted.`);
} catch (error) {
console.error("An error occurred:", error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}
};
main();

View File

@@ -0,0 +1,76 @@
import KcAdminClient from "@keycloak/keycloak-admin-client";
import invariant from "tiny-invariant";
const main = async () => {
const keycloakHost = process.env.KEYCLOAK_HOST;
invariant(keycloakHost, "KEYCLOAK_HOST environment variable is required.");
const adminUsername = process.env.KEYCLOAK_ADMIN_USER;
invariant(adminUsername, "KEYCLOAK_ADMIN_USER environment variable is required.");
const adminPassword = process.env.KEYCLOAK_ADMIN_PASSWORD;
invariant(adminPassword, "KEYCLOAK_ADMIN_PASSWORD environment variable is required");
const realmName = process.env.KEYCLOAK_REALM;
invariant(realmName, "KEYCLOAK_REALM environment variable is required");
const groupName = process.env.GROUP_NAME;
invariant(groupName, "GROUP_NAME environment variable is required");
const kcAdminClient = new KcAdminClient({
baseUrl: `https://${keycloakHost}`,
realmName: "master",
});
try {
await kcAdminClient.auth({
username: adminUsername,
password: adminPassword,
grantType: "password",
clientId: "admin-cli",
});
console.log("Authentication successful.");
kcAdminClient.setConfig({ realmName });
// Find group to delete
const groups = await kcAdminClient.groups.find({ search: groupName });
const group = groups.find(g => g.name === groupName);
if (!group) {
console.log(`Group '${groupName}' not found`);
return;
}
// Check if group has members
const groupMembers = await kcAdminClient.groups.listMembers({ id: group.id! });
if (groupMembers.length > 0) {
console.log(`Warning: Group '${groupName}' has ${groupMembers.length} members:`);
groupMembers.forEach(member => {
console.log(` - ${member.username} (${member.firstName} ${member.lastName})`);
});
console.log("All members will be removed from the group when it's deleted.");
}
// Check for subgroups
const subGroups = await kcAdminClient.groups.listSubGroups({ id: group.id! });
if (subGroups.length > 0) {
console.log(`Warning: Group '${groupName}' has ${subGroups.length} subgroups:`);
subGroups.forEach(subGroup => {
console.log(` - ${subGroup.name}`);
});
console.log("All subgroups will be deleted as well.");
}
// Delete group
await kcAdminClient.groups.del({ id: group.id! });
console.log(`Group '${groupName}' deleted successfully`);
} catch (error) {
console.error("Error deleting group:", error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}
};
main();

View File

@@ -0,0 +1,61 @@
import KcAdminClient from "@keycloak/keycloak-admin-client";
import invariant from "tiny-invariant";
const main = async () => {
const keycloakHost = process.env.KEYCLOAK_HOST;
invariant(keycloakHost, "KEYCLOAK_HOST environment variable is required.");
const adminUsername = process.env.KEYCLOAK_ADMIN_USER;
invariant(adminUsername, "KEYCLOAK_ADMIN_USER environment variable is required.");
const adminPassword = process.env.KEYCLOAK_ADMIN_PASSWORD;
invariant(adminPassword, "KEYCLOAK_ADMIN_PASSWORD environment variable is required");
const realmNameToDelete = process.env.KEYCLOAK_REALM_TO_DELETE;
invariant(realmNameToDelete, "KEYCLOAK_REALM_TO_DELETE environment variable is required");
if (realmNameToDelete === "master") {
console.error("Error: Deleting the 'master' realm is a highly destructive operation and is not allowed by this script.");
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}
const kcAdminClient = new KcAdminClient({
baseUrl: `https://${keycloakHost}`,
realmName: "master", // Authenticate against master realm to delete other realms
});
try {
await kcAdminClient.auth({
username: adminUsername,
password: adminPassword,
grantType: "password",
clientId: "admin-cli",
});
console.log("Authentication successful with master realm.");
// Check if realm exists before attempting deletion
const realm = await kcAdminClient.realms.findOne({ realm: realmNameToDelete });
if (!realm) {
console.warn(`Realm '${realmNameToDelete}' not found. Nothing to delete.`);
return; // Exit gracefully if realm doesn't exist
}
console.log(`Attempting to delete realm: '${realmNameToDelete}'...`);
await kcAdminClient.realms.del({ realm: realmNameToDelete });
console.log(`Realm '${realmNameToDelete}' deleted successfully.`);
} catch (error) {
console.error(`An error occurred while trying to delete realm '${realmNameToDelete}':`, error);
const err = error as any;
if (err.response?.data) {
console.error("Error details:", JSON.stringify(err.response.data, undefined, 2));
}
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}
};
main();

View File

@@ -0,0 +1,76 @@
import KcAdminClient from "@keycloak/keycloak-admin-client";
import invariant from "tiny-invariant";
const main = async () => {
const keycloakHost = process.env.KEYCLOAK_HOST;
invariant(keycloakHost, "KEYCLOAK_HOST environment variable is required.");
const adminUsername = process.env.KEYCLOAK_ADMIN_USER;
invariant(adminUsername, "KEYCLOAK_ADMIN_USER environment variable is required.");
const adminPassword = process.env.KEYCLOAK_ADMIN_PASSWORD;
invariant(adminPassword, "KEYCLOAK_ADMIN_PASSWORD environment variable is required");
const realmName = process.env.KEYCLOAK_REALM;
invariant(realmName, "KEYCLOAK_REALM environment variable is required");
const username = process.env.USERNAME;
invariant(username, "USERNAME environment variable is required");
const groupName = process.env.GROUP_NAME;
invariant(groupName, "GROUP_NAME environment variable is required");
const kcAdminClient = new KcAdminClient({
baseUrl: `https://${keycloakHost}`,
realmName: "master",
});
try {
await kcAdminClient.auth({
username: adminUsername,
password: adminPassword,
grantType: "password",
clientId: "admin-cli",
});
console.log("Authentication successful.");
kcAdminClient.setConfig({ realmName });
// Find user
const users = await kcAdminClient.users.find({ username });
const user = users.find(u => u.username === username);
if (!user) {
throw new Error(`User '${username}' not found`);
}
// Find group
const groups = await kcAdminClient.groups.find({ search: groupName });
const group = groups.find(g => g.name === groupName);
if (!group) {
throw new Error(`Group '${groupName}' not found`);
}
// Check if user is in group
const userGroups = await kcAdminClient.users.listGroups({ id: user.id! });
const isMember = userGroups.some(ug => ug.id === group.id);
if (!isMember) {
console.log(`User '${username}' is not a member of group '${groupName}'`);
return;
}
// Remove user from group
await kcAdminClient.users.delFromGroup({
id: user.id!,
groupId: group.id!,
});
console.log(`User '${username}' removed from group '${groupName}' successfully`);
} catch (error) {
console.error("Error removing user from group:", error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}
};
main();

View File

@@ -0,0 +1,60 @@
import KcAdminClient from "@keycloak/keycloak-admin-client";
import invariant from "tiny-invariant";
const main = async () => {
const keycloakHost = process.env.KEYCLOAK_HOST;
invariant(keycloakHost, "KEYCLOAK_HOST environment variable is required.");
const adminUsername = process.env.KEYCLOAK_ADMIN_USER;
invariant(adminUsername, "KEYCLOAK_ADMIN_USER environment variable is required.");
const adminPassword = process.env.KEYCLOAK_ADMIN_PASSWORD;
invariant(adminPassword, "KEYCLOAK_ADMIN_PASSWORD environment variable is required");
const realmName = process.env.KEYCLOAK_REALM;
invariant(realmName, "KEYCLOAK_REALM environment variable is required");
const usernameToDelete = process.env.USERNAME;
invariant(
usernameToDelete,
"USERNAME environment variable (for the user to be deleted) is required"
);
const kcAdminClient = new KcAdminClient({
baseUrl: `https://${keycloakHost}`,
realmName: "master",
});
try {
await kcAdminClient.auth({
username: adminUsername,
password: adminPassword,
grantType: "password",
clientId: "admin-cli",
});
console.log("Authentication successful.");
kcAdminClient.setConfig({ realmName });
const users = await kcAdminClient.users.find({ username: usernameToDelete });
if (users.length === 0) {
console.warn(`User '${usernameToDelete}' not found in realm '${realmName}'.`);
return;
}
const user = users[0];
invariant(user.id, `User ID not found for user '${usernameToDelete}'.`);
await kcAdminClient.users.del({ id: user.id });
console.log(
`User '${usernameToDelete}' (ID: ${user.id}) successfully deleted from realm '${realmName}'.`
);
} catch (error) {
console.error("An error occurred during user deletion:", error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}
};
main();

View File

@@ -0,0 +1,57 @@
import KcAdminClient from "@keycloak/keycloak-admin-client";
import invariant from "tiny-invariant";
const main = async () => {
const keycloakHost = process.env.KEYCLOAK_HOST;
invariant(keycloakHost, "KEYCLOAK_HOST environment variable is required.");
const adminUsername = process.env.KEYCLOAK_ADMIN_USER;
invariant(adminUsername, "KEYCLOAK_ADMIN_USER environment variable is required.");
const adminPassword = process.env.KEYCLOAK_ADMIN_PASSWORD;
invariant(adminPassword, "KEYCLOAK_ADMIN_PASSWORD environment variable is required");
const realmName = process.env.KEYCLOAK_REALM;
invariant(realmName, "KEYCLOAK_REALM environment variable is required");
const username = process.env.USERNAME;
invariant(username, "USERNAME environment variable is required");
const kcAdminClient = new KcAdminClient({
baseUrl: `https://${keycloakHost}`,
realmName: "master",
});
try {
await kcAdminClient.auth({
username: adminUsername,
password: adminPassword,
grantType: "password",
clientId: "admin-cli",
});
console.log("Authentication successful.");
kcAdminClient.setConfig({ realmName });
const users = await kcAdminClient.users.find({
username,
exact: true,
});
if (users && users.length > 0) {
console.log(`User '${username}' exists with ID: ${users[0].id}`);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(0);
} else {
console.log(`User '${username}' does not exist.`);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}
} catch (error) {
console.error("Error checking user existence:", error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}
};
main();