diff --git a/keycloak/justfile b/keycloak/justfile index 76f726f..0265ead 100644 --- a/keycloak/justfile +++ b/keycloak/justfile @@ -71,7 +71,8 @@ delete-database-secret: # Install Keycloak install: #!/bin/bash - set -euxo pipefail + set -euo pipefail + # Setup vault environment once at the beginning if vault is enabled just create-credentials just postgres::create-db keycloak just create-database-secret @@ -159,6 +160,16 @@ add-audience-mapper client_id: export KEYCLOAK_CLIENT_ID={{ client_id }} dotenvx run -f ../.env.local -- tsx ./scripts/add-audience-mapper.ts +# Add Keycloak client groups mapper +add-groups-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-groups-mapper.ts + # Create Keycloak group create-group group_name parent_group='' description='': #!/bin/bash @@ -362,14 +373,14 @@ user-exists username='': # Print Keycloak admin username admin-username: #!/bin/bash - set -euxo pipefail + set -euo 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 + if just vault::exist keycloak/admin 2>/dev/null; then just vault::get keycloak/admin username echo exit 0 @@ -387,7 +398,7 @@ admin-password: fi if [ "${VAULT_ENABLED}" != "false" ]; then just vault::setup-env - if just vault::exist keycloak/admin; then + if just vault::exist keycloak/admin 2>/dev/null; then just vault::get keycloak/admin password echo exit 0 diff --git a/keycloak/scripts/add-groups-mapper.ts b/keycloak/scripts/add-groups-mapper.ts new file mode 100644 index 0000000..afd0a88 --- /dev/null +++ b/keycloak/scripts/add-groups-mapper.ts @@ -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 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 }); + + // Find the client + const clients = await kcAdminClient.clients.find({ clientId }); + const client = clients.find(c => c.clientId === clientId); + + if (!client) { + throw new Error(`Client '${clientId}' not found`); + } + + // Check if groups mapper already exists + const existingMappers = await kcAdminClient.clients.listProtocolMappers({ + id: client.id!, + }); + + const groupsMapper = existingMappers.find(mapper => + mapper.name === "groups" || mapper.config?.["claim.name"] === "groups" + ); + + if (groupsMapper) { + console.log("Groups mapper already exists for the client."); + return; + } + + // Add groups mapper + await kcAdminClient.clients.addProtocolMapper({ + id: client.id!, + }, { + name: "groups", + protocol: "openid-connect", + protocolMapper: "oidc-group-membership-mapper", + config: { + "claim.name": "groups", + "full.path": "false", + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true", + }, + }); + + console.log("Groups mapper added to the client."); + } catch (error) { + console.error("Error adding groups mapper:", error); + // eslint-disable-next-line unicorn/no-process-exit + process.exit(1); + } +}; + +main(); \ No newline at end of file