feat: add keycloak and postgres
This commit is contained in:
2
justfile
2
justfile
@@ -7,8 +7,10 @@ default:
|
|||||||
@just --list --unsorted --list-submodules
|
@just --list --unsorted --list-submodules
|
||||||
|
|
||||||
mod env
|
mod env
|
||||||
|
mod keycloak
|
||||||
mod k8s
|
mod k8s
|
||||||
mod longhorn
|
mod longhorn
|
||||||
|
mod postgres
|
||||||
mod utils
|
mod utils
|
||||||
mod vault
|
mod vault
|
||||||
|
|
||||||
|
|||||||
1
keycloak/.gitignore
vendored
Normal file
1
keycloak/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
keycloak-values.yaml
|
||||||
408
keycloak/justfile
Normal file
408
keycloak/justfile
Normal 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
|
||||||
43
keycloak/keycloak-values.gomplate.yaml
Normal file
43
keycloak/keycloak-values.gomplate.yaml
Normal 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
|
||||||
70
keycloak/scripts/add-audience-mapper.ts
Normal file
70
keycloak/scripts/add-audience-mapper.ts
Normal 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();
|
||||||
121
keycloak/scripts/add-minio-policy.ts
Normal file
121
keycloak/scripts/add-minio-policy.ts
Normal 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();
|
||||||
76
keycloak/scripts/add-user-to-group.ts
Normal file
76
keycloak/scripts/add-user-to-group.ts
Normal 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();
|
||||||
64
keycloak/scripts/create-client.ts
Normal file
64
keycloak/scripts/create-client.ts
Normal 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();
|
||||||
82
keycloak/scripts/create-group.ts
Normal file
82
keycloak/scripts/create-group.ts
Normal 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();
|
||||||
50
keycloak/scripts/create-realm.ts
Normal file
50
keycloak/scripts/create-realm.ts
Normal 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();
|
||||||
99
keycloak/scripts/create-user.ts
Normal file
99
keycloak/scripts/create-user.ts
Normal 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();
|
||||||
54
keycloak/scripts/delete-client.ts
Normal file
54
keycloak/scripts/delete-client.ts
Normal 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();
|
||||||
76
keycloak/scripts/delete-group.ts
Normal file
76
keycloak/scripts/delete-group.ts
Normal 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();
|
||||||
61
keycloak/scripts/delete-realm.ts
Normal file
61
keycloak/scripts/delete-realm.ts
Normal 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();
|
||||||
|
|
||||||
76
keycloak/scripts/delete-user-from-group.ts
Normal file
76
keycloak/scripts/delete-user-from-group.ts
Normal 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();
|
||||||
60
keycloak/scripts/delete-user.ts
Normal file
60
keycloak/scripts/delete-user.ts
Normal 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();
|
||||||
57
keycloak/scripts/user-exists.ts
Normal file
57
keycloak/scripts/user-exists.ts
Normal 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();
|
||||||
2
postgres/.gitignore
vendored
Normal file
2
postgres/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
create-database.yaml
|
||||||
|
delete-database.yaml
|
||||||
248
postgres/justfile
Normal file
248
postgres/justfile
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
set fallback := true
|
||||||
|
|
||||||
|
export CNPG_NAMESPACE := env("CNPG_NAMESPACE", "postgres")
|
||||||
|
export CNPG_CHART_VERSION := env("CNPG_CHART_VERSION", "0.26.0")
|
||||||
|
export CNPG_CLUSTER_CHART_VERSION := env("CNPG_CLUSTER_CHART_VERSION", "0.3.1")
|
||||||
|
export VAULT_ENABLED := env("VAULT_ENABLED", "true")
|
||||||
|
|
||||||
|
[private]
|
||||||
|
default:
|
||||||
|
@just --list --unsorted --list-submodules
|
||||||
|
|
||||||
|
# Add Helm repository
|
||||||
|
add-helm-repo:
|
||||||
|
@helm repo add cnpg https://cloudnative-pg.github.io/charts
|
||||||
|
@helm repo update
|
||||||
|
|
||||||
|
# Remove Helm repository
|
||||||
|
remove-helm-repo:
|
||||||
|
@helm repo remove cnpg
|
||||||
|
|
||||||
|
# Install CloudNativePG and create a cluster
|
||||||
|
install:
|
||||||
|
@just install-cnpg
|
||||||
|
@just create-cluster
|
||||||
|
|
||||||
|
# Uninstall CloudNativePG and delete the cluster
|
||||||
|
uninstall:
|
||||||
|
@just delete-cluster
|
||||||
|
@just uninstall-cnpg
|
||||||
|
|
||||||
|
# Install CloudNativePG
|
||||||
|
install-cnpg:
|
||||||
|
@just add-helm-repo
|
||||||
|
@helm upgrade --cleanup-on-fail --install cnpg cnpg/cloudnative-pg \
|
||||||
|
--version ${CNPG_CHART_VERSION} \
|
||||||
|
-n ${CNPG_NAMESPACE} --create-namespace --wait
|
||||||
|
|
||||||
|
# Uninstall CloudNativePG
|
||||||
|
uninstall-cnpg:
|
||||||
|
@helm uninstall cnpg -n ${CNPG_NAMESPACE} --wait
|
||||||
|
@kubectl delete namespace ${CNPG_NAMESPACE} --ignore-not-found
|
||||||
|
|
||||||
|
# Create Postgres cluster
|
||||||
|
create-cluster:
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
helm upgrade --install postgres-cluster cnpg/cluster \
|
||||||
|
--version ${CNPG_CLUSTER_CHART_VERSION} \
|
||||||
|
-n ${CNPG_NAMESPACE} --create-namespace --wait \
|
||||||
|
-f postgres-cluster-values.yaml
|
||||||
|
|
||||||
|
if [ "${VAULT_ENABLED}" != "false" ]; then
|
||||||
|
just put-admin-credentials-to-vault
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Delete Postgres cluster
|
||||||
|
delete-cluster:
|
||||||
|
@helm uninstall postgres-cluster -n ${CNPG_NAMESPACE} --ignore-not-found --wait
|
||||||
|
|
||||||
|
# Print Postgres username
|
||||||
|
admin-username:
|
||||||
|
@echo "postgres"
|
||||||
|
|
||||||
|
# Print Postgres password
|
||||||
|
admin-password:
|
||||||
|
@kubectl get -n ${CNPG_NAMESPACE} secret postgres-cluster-superuser \
|
||||||
|
-o jsonpath="{.data.password}" | base64 --decode
|
||||||
|
@echo
|
||||||
|
|
||||||
|
# Put admin credentials to Vault
|
||||||
|
put-admin-credentials-to-vault:
|
||||||
|
@echo vault::put postgres/admin username=$(just admin-username) password=$(just admin-password)
|
||||||
|
@just vault::put postgres/admin username=$(just admin-username) password=$(just admin-password)
|
||||||
|
@echo "Admin credentials stored in Vault under 'postgres/admin'."
|
||||||
|
|
||||||
|
# Create Postgres database
|
||||||
|
create-db db_name='':
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
DB_NAME=${DB_NAME:-{{ db_name }}}
|
||||||
|
while [ -z "${DB_NAME}" ]; do
|
||||||
|
DB_NAME=$(gum input --prompt="Database name: " --width=80)
|
||||||
|
done
|
||||||
|
if just db-exists ${DB_NAME} >&/dev/null; then
|
||||||
|
echo "Database ${DB_NAME} already exists" >&2
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
echo "Creating database ${DB_NAME}..."
|
||||||
|
just psql -c "\"CREATE DATABASE ${DB_NAME};\""
|
||||||
|
echo "Database ${DB_NAME} created."
|
||||||
|
|
||||||
|
# Delete Postgres database
|
||||||
|
delete-db db_name='':
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
DB_NAME=${DB_NAME:-{{ db_name }}}
|
||||||
|
if ! just db-exists ${DB_NAME} >&/dev/null; then
|
||||||
|
echo "Database ${DB_NAME} does not exist." >&2
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
# Terminate all connections to the database
|
||||||
|
just psql -U postgres -P pager=off -c "\"SELECT pg_terminate_backend(pid) FROM pg_stat_activity
|
||||||
|
WHERE datname = '${DB_NAME}' AND pid <> pg_backend_pid();\""
|
||||||
|
just psql -U postgres -c "\"DROP DATABASE ${DB_NAME};\""
|
||||||
|
echo "Database ${DB_NAME} deleted."
|
||||||
|
|
||||||
|
# Check if database exists
|
||||||
|
db-exists db_name='':
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
DB_NAME=${DB_NAME:-{{ db_name }}}
|
||||||
|
while [ -z "${DB_NAME}" ]; do
|
||||||
|
DB_NAME=$(gum input --prompt="Database name: " --width=80)
|
||||||
|
done
|
||||||
|
if echo '\l' | just postgres::psql | grep -E "^ *${DB_NAME} *\|" &>/dev/null; then
|
||||||
|
echo "Database ${DB_NAME} exists."
|
||||||
|
else
|
||||||
|
echo "Database ${DB_NAME} does not exist." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create Postgres user
|
||||||
|
create-user username='' password='':
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
USERNAME=${USERNAME:-"{{ username }}"}
|
||||||
|
PASSWORD=${PASSWORD:-"{{ password }}"}
|
||||||
|
while [ -z "${USERNAME}" ]; do
|
||||||
|
USERNAME=$(gum input --prompt="Username: " --width=80)
|
||||||
|
done
|
||||||
|
if just user-exists ${USERNAME}; then
|
||||||
|
echo "User ${USERNAME} already exists" >&2
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
if [ -z "${PASSWORD}" ]; then
|
||||||
|
PASSWORD=$(gum input --prompt="Password: " --password --width=80 \
|
||||||
|
--placeholder="Empty to generate a random password")
|
||||||
|
fi
|
||||||
|
if [ -z "${PASSWORD}" ]; then
|
||||||
|
PASSWORD=$(just random-password)
|
||||||
|
fi
|
||||||
|
just psql -c "\"CREATE USER ${USERNAME} WITH LOGIN PASSWORD '${PASSWORD}';\""
|
||||||
|
echo "User ${USERNAME} created."
|
||||||
|
|
||||||
|
# Delete Postgres user
|
||||||
|
delete-user username='':
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
USERNAME=${USERNAME:-"{{ username }}"}
|
||||||
|
if ! just user-exists ${USERNAME}; then
|
||||||
|
echo "User ${USERNAME} does not exist." >&2
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
just psql -c "\"ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON TABLES FROM ${USERNAME};\""
|
||||||
|
just psql -c "\"ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON SEQUENCES FROM ${USERNAME};\""
|
||||||
|
just psql -c "\"ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM ${USERNAME};\""
|
||||||
|
just psql -c "\"ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON TYPES FROM ${USERNAME};\""
|
||||||
|
just psql -c "\"ALTER SCHEMA public OWNER TO postgres;\""
|
||||||
|
just psql -c "\"DROP USER ${USERNAME};\""
|
||||||
|
echo "User ${USERNAME} deleted."
|
||||||
|
|
||||||
|
# Check if user exists
|
||||||
|
user-exists username='':
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
USERNAME=${USERNAME:-"{{ username }}"}
|
||||||
|
while [ -z "${USERNAME}" ]; do
|
||||||
|
USERNAME=$(gum input --prompt="Username: " --width=80)
|
||||||
|
done
|
||||||
|
if echo '\du' | just postgres::psql | grep -E "^ *${USERNAME} *\|" &>/dev/null; then
|
||||||
|
echo "User ${USERNAME} exists."
|
||||||
|
else
|
||||||
|
echo "User ${USERNAME} does not exist." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Grant all privileges on database to user
|
||||||
|
grant db_name='' username='':
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
DB_NAME=${DB_NAME:-"{{ db_name }}"}
|
||||||
|
USERNAME=${USERNAME:-"{{ username }}"}
|
||||||
|
while [ -z "${DB_NAME}" ]; do
|
||||||
|
DB_NAME=$(gum input --prompt="Database name: " --width=80)
|
||||||
|
done
|
||||||
|
while [ -z "${USERNAME}" ]; do
|
||||||
|
USERNAME=$(gum input --prompt="Username: " --width=80)
|
||||||
|
done
|
||||||
|
if ! just psql ${DB_NAME} -U postgres -P pager=off -c "\"SELECT 1;\""; then
|
||||||
|
echo "Database ${DB_NAME} does not exist." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
just psql -c "\"GRANT ALL PRIVILEGES ON DATABASE ${DB_NAME} TO ${USERNAME};\""
|
||||||
|
echo "Privileges granted."
|
||||||
|
|
||||||
|
# Revoke all privileges on database from user
|
||||||
|
revoke db_name='' username='':
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
DB_NAME=${DB_NAME:-"{{ db_name }}"}
|
||||||
|
USERNAME=${USERNAME:-"{{ username }}"}
|
||||||
|
while [ -z "${DB_NAME}" ]; do
|
||||||
|
DB_NAME=$(gum input --prompt="Database name: " --width=80)
|
||||||
|
done
|
||||||
|
while [ -z "${USERNAME}" ]; do
|
||||||
|
USERNAME=$(gum input --prompt="Username: " --width=80)
|
||||||
|
done
|
||||||
|
if ! just psql -U postgres ${DB_NAME} -P pager=off -c "\"SELECT 1;\""; then
|
||||||
|
echo "Database ${DB_NAME} does not exist." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
just psql -c "\"REVOKE ALL PRIVILEGES ON DATABASE ${DB_NAME} FROM ${USERNAME};\""
|
||||||
|
echo "Privileges revoked."
|
||||||
|
|
||||||
|
# Create Postgres database and user
|
||||||
|
create-user-and-db db_name='' username='' password='':
|
||||||
|
just create-db "{{ db_name }}"
|
||||||
|
just create-user "{{ username }}" "{{ password }}"
|
||||||
|
just grant "{{ db_name }}" "{{ username }}"
|
||||||
|
|
||||||
|
# Delete Postgres database and user
|
||||||
|
delete-user-and-db db_name='' username='':
|
||||||
|
just revoke "{{ db_name }}" "{{ username }}"
|
||||||
|
just delete-user "{{ username }}"
|
||||||
|
just delete-db "{{ db_name }}"
|
||||||
|
|
||||||
|
# Run psql
|
||||||
|
[no-exit-message]
|
||||||
|
psql *args='':
|
||||||
|
@kubectl exec -it -n postgres postgres-cluster-1 -c postgres -- psql {{ args }}
|
||||||
|
|
||||||
|
# Dump Postgres database by pg_dump
|
||||||
|
dump db_name file:
|
||||||
|
kubectl exec -it -n ${CNPG_NAMESPACE} postgres-cluster-1 -c postgres -- bash -c \
|
||||||
|
"pg_dump -d postgresql://postgres:$(just password)@localhost/{{ db_name }} -Fc > \
|
||||||
|
/var/lib/postgresql/data/db.dump"
|
||||||
|
kubectl cp -n ${CNPG_NAMESPACE} -c postgres \
|
||||||
|
postgres-cluster-1:/var/lib/postgresql/data/db.dump {{ file }}
|
||||||
|
kubectl exec -it -n ${CNPG_NAMESPACE} postgres-cluster-1 -c postgres -- rm /var/lib/postgresql/data/db.dump
|
||||||
|
|
||||||
|
# Restore Postgres database by pg_restore
|
||||||
|
restore db_name file:
|
||||||
|
just create-db {{ db_name }}
|
||||||
|
kubectl cp {{ file }} -n ${CNPG_NAMESPACE} -c postgres \
|
||||||
|
postgres-cluster-1:/var/lib/postgresql/data/db.dump
|
||||||
|
kubectl exec -it -n ${CNPG_NAMESPACE} postgres-cluster-1 -c postgres -- bash -c \
|
||||||
|
"pg_restore -d postgresql://postgres:$(just password)@localhost/{{ db_name }} \
|
||||||
|
/var/lib/postgresql/data/db.dump"
|
||||||
6
postgres/postgres-cluster-values.yaml
Normal file
6
postgres/postgres-cluster-values.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
cluster:
|
||||||
|
instances: 1
|
||||||
|
|
||||||
|
initdb:
|
||||||
|
postInitTemplateSQL:
|
||||||
|
- CREATE EXTENSION IF NOT EXISTS vector;
|
||||||
Reference in New Issue
Block a user