diff --git a/keycloak/justfile b/keycloak/justfile index ba4bc43..dc22260 100644 --- a/keycloak/justfile +++ b/keycloak/justfile @@ -434,3 +434,38 @@ update-realm-token-settings realm access_token_lifespan='3600' refresh_token_lif export ACCESS_TOKEN_LIFESPAN={{ access_token_lifespan }} export REFRESH_TOKEN_LIFESPAN={{ refresh_token_lifespan }} dotenvx run -q -f ../.env.local -- tsx ./scripts/update-realm-token-settings.ts + +# Create Keycloak client role +create-client-role realm client_id role_name: + #!/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_ROLE_NAME={{ role_name }} + dotenvx run -q -f ../.env.local -- tsx ./scripts/create-client-role.ts + +# Add user to client role +add-user-to-client-role realm username client_id role_name: + #!/bin/bash + set -euo pipefail + export KEYCLOAK_ADMIN_USER=$(just admin-username) + export KEYCLOAK_ADMIN_PASSWORD=$(just admin-password) + export KEYCLOAK_REALM={{ realm }} + export USERNAME={{ username }} + export KEYCLOAK_CLIENT_ID={{ client_id }} + export KEYCLOAK_ROLE_NAME={{ role_name }} + dotenvx run -q -f ../.env.local -- tsx ./scripts/add-user-to-client-role.ts + +# Remove user from client role +remove-user-from-client-role realm username client_id role_name: + #!/bin/bash + set -euo pipefail + export KEYCLOAK_ADMIN_USER=$(just admin-username) + export KEYCLOAK_ADMIN_PASSWORD=$(just admin-password) + export KEYCLOAK_REALM={{ realm }} + export USERNAME={{ username }} + export KEYCLOAK_CLIENT_ID={{ client_id }} + export KEYCLOAK_ROLE_NAME={{ role_name }} + dotenvx run -q -f ../.env.local -- tsx ./scripts/remove-user-from-client-role.ts diff --git a/keycloak/scripts/add-user-to-client-role.ts b/keycloak/scripts/add-user-to-client-role.ts new file mode 100755 index 0000000..8a40e14 --- /dev/null +++ b/keycloak/scripts/add-user-to-client-role.ts @@ -0,0 +1,109 @@ +#!/usr/bin/env tsx + +import KcAdminClient from "@keycloak/keycloak-admin-client"; +import RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation"; +import invariant from "tiny-invariant"; + +async function main() { + const kcAdminClient = new KcAdminClient({ + baseUrl: `https://${process.env.KEYCLOAK_HOST}`, + realmName: "master", + }); + + await kcAdminClient.auth({ + username: process.env.KEYCLOAK_ADMIN_USER!, + password: process.env.KEYCLOAK_ADMIN_PASSWORD!, + grantType: "password", + clientId: "admin-cli", + }); + + const realm = process.env.KEYCLOAK_REALM!; + const username = process.env.USERNAME!; + const clientId = process.env.KEYCLOAK_CLIENT_ID!; + const roleName = process.env.KEYCLOAK_ROLE_NAME!; + + invariant(realm, "KEYCLOAK_REALM is required"); + invariant(username, "USERNAME is required"); + invariant(clientId, "KEYCLOAK_CLIENT_ID is required"); + invariant(roleName, "KEYCLOAK_ROLE_NAME is required"); + + // Find the user + const users = await kcAdminClient.users.find({ realm, username }); + + if (users.length === 0) { + throw new Error(`User '${username}' not found in realm '${realm}'`); + } + + const user = users[0]; + + // Find the client + const clients = await kcAdminClient.clients.find({ realm, clientId }); + + if (clients.length === 0) { + throw new Error(`Client '${clientId}' not found in realm '${realm}'`); + } + + const client = clients[0]; + + let role: RoleRepresentation | null; + try { + role = await kcAdminClient.clients.findRole({ + realm, + id: client.id!, + roleName, + }); + } catch (error) { + // If role not found, try to list all roles for debugging + const allRoles = await kcAdminClient.clients.listRoles({ + realm, + id: client.id!, + }); + console.error( + `Available roles for client '${clientId}':`, + allRoles.map((r) => r.name), + ); + throw new Error( + `Role '${roleName}' not found for client '${clientId}' in realm '${realm}'. Available roles: ${allRoles.map((r) => r.name).join(", ")}`, + ); + } + + if (!role) { + throw new Error( + `Role '${roleName}' not found for client '${clientId}' in realm '${realm}'`, + ); + } + + // Check if user already has this role + const existingRoles = await kcAdminClient.users.listClientRoleMappings({ + realm, + id: user.id!, + clientUniqueId: client.id!, + }); + + const hasRole = existingRoles.some((r) => r.name === roleName); + if (hasRole) { + console.log( + `User '${username}' already has role '${roleName}' for client '${clientId}'`, + ); + return; + } + + // Add role to user + await kcAdminClient.users.addClientRoleMappings({ + realm, + id: user.id!, + clientUniqueId: client.id!, + roles: [ + { + id: role.id!, + name: role.name!, + }, + ], + }); + + console.log( + `✓ Role '${roleName}' assigned to user '${username}' for client '${clientId}' in realm '${realm}'`, + ); +} + +main().catch(console.error); diff --git a/keycloak/scripts/create-client-role.ts b/keycloak/scripts/create-client-role.ts new file mode 100755 index 0000000..400f5c2 --- /dev/null +++ b/keycloak/scripts/create-client-role.ts @@ -0,0 +1,65 @@ +#!/usr/bin/env tsx + +import KcAdminClient from "@keycloak/keycloak-admin-client"; +import invariant from "tiny-invariant"; + +async function main() { + const kcAdminClient = new KcAdminClient({ + baseUrl: `https://${process.env.KEYCLOAK_HOST}`, + realmName: "master", + }); + + await kcAdminClient.auth({ + username: process.env.KEYCLOAK_ADMIN_USER!, + password: process.env.KEYCLOAK_ADMIN_PASSWORD!, + grantType: "password", + clientId: "admin-cli", + }); + + const realm = process.env.KEYCLOAK_REALM!; + const clientId = process.env.KEYCLOAK_CLIENT_ID!; + const roleName = process.env.KEYCLOAK_ROLE_NAME!; + + invariant(realm, "KEYCLOAK_REALM is required"); + invariant(clientId, "KEYCLOAK_CLIENT_ID is required"); + invariant(roleName, "KEYCLOAK_ROLE_NAME is required"); + + // Find the client by clientId + const clients = await kcAdminClient.clients.find({ realm, clientId }); + + if (clients.length === 0) { + throw new Error(`Client '${clientId}' not found in realm '${realm}'`); + } + + const client = clients[0]; + + // Check if role already exists + try { + const existingRole = await kcAdminClient.clients.findRole({ + realm, + id: client.id!, + roleName, + }); + if (existingRole) { + console.log(`Role '${roleName}' already exists for client '${clientId}'`); + return; + } + } catch (error) { + // Role doesn't exist, continue to create it + console.log(`Role '${roleName}' doesn't exist, creating it...`); + } + + // Create the client role + await kcAdminClient.clients.createRole({ + realm, + id: client.id!, + name: roleName, + }); + + console.log( + `✓ Client role '${roleName}' created for client '${clientId}' in realm '${realm}'`, + ); +} + +main().catch(console.error); + diff --git a/keycloak/scripts/remove-user-from-client-role.ts b/keycloak/scripts/remove-user-from-client-role.ts new file mode 100755 index 0000000..0c7de16 --- /dev/null +++ b/keycloak/scripts/remove-user-from-client-role.ts @@ -0,0 +1,72 @@ +#!/usr/bin/env tsx + +import KcAdminClient from "@keycloak/keycloak-admin-client"; +import invariant from "tiny-invariant"; + +async function main() { + const kcAdminClient = new KcAdminClient({ + baseUrl: `https://${process.env.KEYCLOAK_HOST}`, + realmName: "master", + }); + + await kcAdminClient.auth({ + username: process.env.KEYCLOAK_ADMIN_USER!, + password: process.env.KEYCLOAK_ADMIN_PASSWORD!, + grantType: "password", + clientId: "admin-cli", + }); + + const realm = process.env.KEYCLOAK_REALM!; + const username = process.env.USERNAME!; + const clientId = process.env.KEYCLOAK_CLIENT_ID!; + const roleName = process.env.KEYCLOAK_ROLE_NAME!; + + invariant(realm, "KEYCLOAK_REALM is required"); + invariant(username, "USERNAME is required"); + invariant(clientId, "KEYCLOAK_CLIENT_ID is required"); + invariant(roleName, "KEYCLOAK_ROLE_NAME is required"); + + // Find the user + const users = await kcAdminClient.users.find({ realm, username }); + + if (users.length === 0) { + throw new Error(`User '${username}' not found in realm '${realm}'`); + } + + const user = users[0]; + + // Find the client + const clients = await kcAdminClient.clients.find({ realm, clientId }); + + if (clients.length === 0) { + throw new Error(`Client '${clientId}' not found in realm '${realm}'`); + } + + const client = clients[0]; + + // Find the role + const role = await kcAdminClient.clients.findRole({ + realm, + id: client.id!, + roleName, + }); + + // Remove role from user + await kcAdminClient.users.delClientRoleMappings({ + realm, + id: user.id!, + clientUniqueId: client.id!, + roles: [ + { + id: role?.id!, + name: role?.name!, + }, + ], + }); + + console.log( + `✓ Role '${roleName}' removed from user '${username}' for client '${clientId}' in realm '${realm}'`, + ); +} + +main().catch(console.error);