diff --git a/src/docker/TENANT_MANAGEMENT.md b/src/docker/TENANT_MANAGEMENT.md new file mode 100644 index 000000000..c97afe89f --- /dev/null +++ b/src/docker/TENANT_MANAGEMENT.md @@ -0,0 +1,146 @@ +# Tenant Management for RelayServer + +## Overview + +This document describes how to manage tenants in the Keycloak identity provider for RelayServer. + +## Prerequisites + +1. Keycloak container must be running (`relay-identityprovider`) +2. The `keycloak-admin` client must be configured in the realm (already included in the updated `relayserver-realm.json`) + +## Creating New Tenants + +### Using the Shell Script + +The `create-tenant.sh` script allows you to create new tenant clients via the Keycloak API. + +#### Usage + +```bash +./create-tenant.sh [description] [client_secret] +``` + +#### Parameters + +- `tenant_name` (required): The unique identifier for the tenant (e.g., `TestTenant3`) +- `display_name` (required): Human-readable name for the tenant (e.g., `"Test Tenant 3"`) +- `description` (optional): Description of the tenant (default: "Tenant client for RelayServer") +- `client_secret` (optional): The client secret to use (default: ``) + +#### Examples + +Create a tenant with default settings: +```bash +./create-tenant.sh TestTenant3 "Test Tenant 3" +``` + +Create a tenant with custom description and secret: +```bash +./create-tenant.sh MyTenant "My Custom Tenant" "Production tenant for ACME Corp" "MySecurePassword123!" +``` + +#### Environment Variables + +- `KEYCLOAK_URL`: The Keycloak server URL (default: `http://localhost:5002`) +- `ADMIN_CLIENT_SECRET`: The keycloak-admin client secret (default: ``) + +Example with custom Keycloak URL: +```bash +KEYCLOAK_URL=http://keycloak.example.com:8080 ./create-tenant.sh TestTenant4 "Test Tenant 4" +``` + +## What the Script Does + +1. **Authenticates**: Obtains an access token using the `keycloak-admin` service account +2. **Creates Client**: Creates a new OAuth2/OIDC client with the specified configuration +3. **Configures Permissions**: Assigns the `Access-To-RelayServer` role to the client's service account +4. **Sets up Protocol Mappers**: Configures client IP, client ID, and client host mappers + +The created client will have: +- Service accounts enabled +- Client credentials grant type +- The same configuration as TestTenant1 and TestTenant2 +- Access to the RelayServer via the assigned role + +## Using the New Tenant + +After creating a tenant, you can use it with a connector by setting these environment variables: + +```yaml +environment: + RelayConnector__TenantName: YourTenantName + RelayConnector__RelayServerBaseUri: http://relay-server-a:5000 +``` + +Or in a docker-compose service: + +```yaml +my-connector: + image: relay-connector + environment: + RelayConnector__TenantName: TestTenant3 + RelayConnector__RelayServerBaseUri: http://relay-server-a:5000 + # ... other configuration +``` + +## Manual Tenant Creation via Keycloak Admin Console + +If you prefer to create tenants manually: + +1. Access Keycloak Admin Console at `http://localhost:5002` +2. Login with admin/admin +3. Select the `relayserver` realm +4. Go to Clients → Create client +5. Configure the client with: + - Client type: OpenID Connect + - Client ID: Your tenant name + - Client authentication: ON + - Service accounts roles: ON + - Standard flow: OFF + - Direct access grants: OFF +6. Save and configure the client secret +7. Go to the Service accounts roles tab +8. Assign the `Access-To-RelayServer` role from the `relayserver` client + +## Keycloak Admin Client Details + +The `keycloak-admin` client is configured with the following permissions: +- `manage-clients`: Manage all clients in the realm +- `create-client`: Create new clients +- `view-clients`: View client configurations +- `query-clients`: Query for clients +- `view-realm`: View realm configuration +- `manage-users`: Manage users (needed for service account configuration) +- `view-users`: View users +- `query-users`: Query for users + +This client uses service account authentication with the client credentials grant type. + +## Security Notes + +1. The default password `` should only be used in demo/development environments +2. In production, use strong, unique passwords for each tenant +3. The `keycloak-admin` client has elevated privileges - protect its credentials carefully +4. Consider implementing additional security measures like: + - IP restrictions + - Short token lifetimes + - Audit logging + - Regular credential rotation + +## Troubleshooting + +### Script fails to get access token +- Check that Keycloak is running and accessible +- Verify the KEYCLOAK_URL is correct +- Ensure the keycloak-admin client exists and has the correct secret + +### Client creation succeeds but role assignment fails +- The service account may take a moment to be created - the script waits 2 seconds +- Check that the `relayserver` client exists in the realm +- Verify the `Access-To-RelayServer` role exists + +### Connector can't authenticate with new tenant +- Verify the tenant was created successfully in Keycloak +- Check that the service account has the `Access-To-RelayServer` role +- Ensure the client secret matches what's configured in the connector \ No newline at end of file diff --git a/src/docker/check-keycloak-admin.sh b/src/docker/check-keycloak-admin.sh new file mode 100644 index 000000000..b222eec7f --- /dev/null +++ b/src/docker/check-keycloak-admin.sh @@ -0,0 +1,153 @@ +#!/bin/bash + +# Diagnostic script to check keycloak-admin client configuration +# Usage: ./check-keycloak-admin.sh + +set -e + +# Configuration +KEYCLOAK_URL="${KEYCLOAK_URL:-http://localhost:5002}" +REALM="relayserver" +ADMIN_CLIENT_ID="keycloak-admin" +ADMIN_CLIENT_SECRET="${ADMIN_CLIENT_SECRET:-}" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Function to print colored messages +print_message() { + local color=$1 + local message=$2 + echo -e "${color}${message}${NC}" +} + +print_message "$BLUE" "=========================================" +print_message "$BLUE" "Keycloak Admin Client Diagnostic" +print_message "$BLUE" "=========================================" +echo "" + +# Check Keycloak availability +print_message "$YELLOW" "1. Checking Keycloak availability..." +if curl -s -f -o /dev/null "${KEYCLOAK_URL}/realms/${REALM}/.well-known/openid-configuration"; then + print_message "$GREEN" " ✓ Keycloak is accessible at ${KEYCLOAK_URL}" +else + print_message "$RED" " ✗ Cannot reach Keycloak at ${KEYCLOAK_URL}" + print_message "$RED" " Please ensure Docker containers are running" + exit 1 +fi + +# Try to get token +print_message "$YELLOW" "2. Testing authentication with keycloak-admin client..." +token_response=$(curl -s -X POST \ + "${KEYCLOAK_URL}/realms/${REALM}/protocol/openid-connect/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=client_credentials" \ + -d "client_id=${ADMIN_CLIENT_ID}" \ + -d "client_secret=${ADMIN_CLIENT_SECRET}") + +access_token=$(echo "$token_response" | grep -o '"access_token":"[^"]*' | sed 's/"access_token":"//') + +if [ -n "$access_token" ]; then + print_message "$GREEN" " ✓ Successfully authenticated" + + # Decode token to check roles (if jq is available) + if command -v jq >/dev/null 2>&1 && command -v base64 >/dev/null 2>&1; then + print_message "$YELLOW" "3. Checking token permissions..." + token_payload=$(echo "$access_token" | cut -d. -f2 | base64 -d 2>/dev/null | jq . 2>/dev/null || echo "{}") + + # Check for realm-management roles + realm_roles=$(echo "$token_payload" | jq -r '.resource_access."realm-management".roles[]?' 2>/dev/null) + + if [ -n "$realm_roles" ]; then + print_message "$GREEN" " ✓ Found realm-management roles:" + echo "$realm_roles" | while read -r role; do + echo " - $role" + done + else + print_message "$YELLOW" " ⚠ No realm-management roles found in token" + fi + fi +else + print_message "$RED" " ✗ Failed to authenticate" + error_msg=$(echo "$token_response" | grep -o '"error_description":"[^"]*' | sed 's/"error_description":"//') + if [ -n "$error_msg" ]; then + print_message "$RED" " Error: $error_msg" + else + print_message "$RED" " Response: $token_response" + fi + echo "" + print_message "$YELLOW" " Possible causes:" + echo " - The keycloak-admin client may not exist" + echo " - The client secret may be incorrect" + echo " - Keycloak may still be initializing" + exit 1 +fi + +# Test API access +print_message "$YELLOW" "4. Testing Admin API access..." + +# Try to list clients +list_response=$(curl -s -w "\n%{http_code}" -X GET \ + "${KEYCLOAK_URL}/admin/realms/${REALM}/clients?max=1" \ + -H "Authorization: Bearer ${access_token}") + +http_code=$(echo "$list_response" | tail -n1) + +if [ "$http_code" = "200" ]; then + print_message "$GREEN" " ✓ Can list clients (HTTP 200)" +else + print_message "$RED" " ✗ Cannot list clients (HTTP ${http_code})" + response_body=$(echo "$list_response" | sed '$d') + if [ -n "$response_body" ]; then + print_message "$RED" " Response: $response_body" + fi +fi + +# Try to check create permission +print_message "$YELLOW" "5. Checking create client permission..." +# We'll do a dry-run by sending an invalid request that should fail with 400 (bad request) not 403 (forbidden) +create_test=$(curl -s -w "\n%{http_code}" -X POST \ + "${KEYCLOAK_URL}/admin/realms/${REALM}/clients" \ + -H "Authorization: Bearer ${access_token}" \ + -H "Content-Type: application/json" \ + -d '{}') + +http_code=$(echo "$create_test" | tail -n1) + +if [ "$http_code" = "400" ]; then + print_message "$GREEN" " ✓ Has permission to create clients (got expected 400 for invalid request)" +elif [ "$http_code" = "403" ]; then + print_message "$RED" " ✗ No permission to create clients (HTTP 403 Forbidden)" + print_message "$RED" " The keycloak-admin client needs realm-admin permissions" +else + print_message "$YELLOW" " ⚠ Unexpected response code: HTTP ${http_code}" +fi + +echo "" +print_message "$BLUE" "=========================================" +print_message "$BLUE" "Diagnostic Complete" +print_message "$BLUE" "=========================================" + +if [ "$http_code" = "400" ] || [ "$http_code" = "201" ]; then + echo "" + print_message "$GREEN" "✓ The keycloak-admin client appears to be configured correctly!" + echo "" + echo "You can now create tenants using:" + echo " ./create-tenant.sh [description] [secret]" +else + echo "" + print_message "$YELLOW" "⚠ There may be permission issues with the keycloak-admin client." + echo "" + echo "To fix this:" + echo "1. Stop the Docker containers: docker-compose down" + echo "2. Start them again: docker-compose up -d" + echo "3. Wait about 30 seconds for Keycloak to initialize" + echo "4. Run this diagnostic again" + echo "" + echo "If the problem persists, check the realm configuration in:" + echo " keycloak_data/relayserver-realm.json" +fi \ No newline at end of file diff --git a/src/docker/create-tenant.sh b/src/docker/create-tenant.sh new file mode 100644 index 000000000..edbdbb3e5 --- /dev/null +++ b/src/docker/create-tenant.sh @@ -0,0 +1,355 @@ +#!/bin/bash + +# Script to create a new tenant client in Keycloak for RelayServer +# Usage: ./create-tenant.sh [description] [client_secret] +# Example: ./create-tenant.sh TestTenant3 "Test Tenant 3" "Third test tenant" "" + +set -e + +# Enable debug mode if DEBUG=1 +if [ "${DEBUG}" = "1" ]; then + set -x +fi + +# Configuration +KEYCLOAK_URL="${KEYCLOAK_URL:-http://localhost:5002}" +REALM="relayserver" +ADMIN_CLIENT_ID="keycloak-admin" +ADMIN_CLIENT_SECRET="${ADMIN_CLIENT_SECRET:-}" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Function to print colored messages +print_message() { + local color=$1 + local message=$2 + echo -e "${color}${message}${NC}" +} + +# Function to get access token for the admin client +get_admin_token() { + local token_response=$(curl -s -X POST \ + "${KEYCLOAK_URL}/realms/${REALM}/protocol/openid-connect/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=client_credentials" \ + -d "client_id=${ADMIN_CLIENT_ID}" \ + -d "client_secret=${ADMIN_CLIENT_SECRET}") + + # Extract access token using grep and sed (portable across systems) + local access_token=$(echo "$token_response" | grep -o '"access_token":"[^"]*' | sed 's/"access_token":"//') + + if [ -z "$access_token" ]; then + print_message "$RED" "Error: Failed to obtain access token" + print_message "$RED" "Response: $token_response" + exit 1 + fi + + # Debug: decode token if jq is available + if [ "${DEBUG}" = "1" ] && command -v jq >/dev/null 2>&1; then + echo "Token payload:" >&2 + echo "$access_token" | cut -d. -f2 | base64 -d 2>/dev/null | jq . >&2 || true + fi + + echo "$access_token" +} + +# Function to create a new client +create_client() { + local tenant_name=$1 + local display_name=$2 + local description=${3:-"Tenant client for RelayServer"} + local client_secret=${4:-""} + local token=$5 + + # Generate a unique client ID + local client_uuid=$(uuidgen 2>/dev/null || cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "$(date +%s)-$tenant_name") + + # Prepare the client configuration JSON + local client_json=$(cat < [description] [client_secret]" + echo "Example: $0 TestTenant3 \"Test Tenant 3\" \"Third test tenant\" \"\"" + exit 1 + fi + + local tenant_name=$1 + local display_name=$2 + local description=${3:-"Tenant client for RelayServer"} + local client_secret=${4:-""} + + print_message "$YELLOW" "Creating tenant: ${tenant_name}" + print_message "$YELLOW" "Display name: ${display_name}" + print_message "$YELLOW" "Description: ${description}" + echo "" + + # Get admin token + print_message "$YELLOW" "Obtaining admin access token..." + local token=$(get_admin_token) + print_message "$GREEN" "✓ Access token obtained" + + # Test API access + if ! test_api_access "$token"; then + exit 1 + fi + + # Create the client + print_message "$YELLOW" "Creating client..." + if create_client "$tenant_name" "$display_name" "$description" "$client_secret" "$token"; then + + # Wait a moment for the service account to be created + sleep 2 + + # Get the service account user ID + print_message "$YELLOW" "Configuring service account..." + local user_id=$(get_service_account_user "$tenant_name" "$token") + + if [ -n "$user_id" ]; then + print_message "$GREEN" "✓ Service account found: ${user_id}" + + # Assign the Access-To-RelayServer role + print_message "$YELLOW" "Assigning RelayServer access role..." + assign_relay_access_role "$user_id" "$token" + fi + + echo "" + print_message "$GREEN" "=========================================" + print_message "$GREEN" "Tenant '${tenant_name}' created successfully!" + print_message "$GREEN" "=========================================" + echo "" + echo "Client ID: ${tenant_name}" + echo "Client Secret: ${client_secret}" + echo "" + echo "You can now use this tenant to connect to the RelayServer." + echo "" + echo "To use with a connector, set these environment variables:" + echo " RelayConnector__TenantName=${tenant_name}" + echo " RelayConnector__RelayServerBaseUri=http://relay-server-a:5000 # or relay-server-b" + echo "" + else + print_message "$RED" "Failed to create tenant" + exit 1 + fi +} + +# Run the main function +main "$@" \ No newline at end of file diff --git a/src/docker/keycloak_data/relayserver-realm.json b/src/docker/keycloak_data/relayserver-realm.json index 33f3ecd1f..043ecb7e8 100644 --- a/src/docker/keycloak_data/relayserver-realm.json +++ b/src/docker/keycloak_data/relayserver-realm.json @@ -419,6 +419,23 @@ }, "notBefore" : 0, "groups" : [ ] + }, { + "id" : "keycloak-admin-service-account-id", + "username" : "service-account-keycloak-admin", + "emailVerified" : false, + "createdTimestamp" : 1715160002750, + "enabled" : true, + "totp" : false, + "serviceAccountClientId" : "keycloak-admin", + "credentials" : [ ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-relayserver" ], + "clientRoles" : { + "realm-management" : [ "realm-admin" ] + }, + "notBefore" : 0, + "groups" : [ ] } ], "scopeMappings" : [ { "clientScope" : "offline_access", @@ -602,6 +619,94 @@ } ], "defaultClientScopes" : [ "web-origins", "acr", "connector" ], "optionalClientScopes" : [ "address", "phone", "offline_access", "roles", "profile", "microprofile-jwt", "email" ] + }, { + "id" : "keycloak-admin-client-id", + "clientId" : "keycloak-admin", + "name" : "Keycloak Admin Client", + "description" : "Service account for creating and managing tenant clients", + "rootUrl" : "", + "adminUrl" : "", + "baseUrl" : "", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "secret" : "", + "redirectUris" : [ "/*" ], + "webOrigins" : [ "/*" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : false, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : true, + "publicClient" : false, + "frontchannelLogout" : true, + "protocol" : "openid-connect", + "attributes" : { + "client.secret.creation.time" : "1715160002", + "oauth2.device.authorization.grant.enabled" : "false", + "backchannel.logout.revoke.offline.tokens" : "false", + "use.refresh.tokens" : "true", + "oidc.ciba.grant.enabled" : "false", + "client.use.lightweight.access.token.enabled" : "false", + "backchannel.logout.session.required" : "true", + "client_credentials.use_refresh_token" : "false", + "tls.client.certificate.bound.access.tokens" : "false", + "require.pushed.authorization.requests" : "false", + "acr.loa.map" : "{}", + "display.on.consent.screen" : "false", + "token.response.type.bearer.lower-case" : "false" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : -1, + "protocolMappers" : [ { + "id" : "keycloak-admin-client-ip-mapper", + "name" : "Client IP Address", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usersessionmodel-note-mapper", + "consentRequired" : false, + "config" : { + "user.session.note" : "clientAddress", + "introspection.token.claim" : "true", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "clientIpAddress", + "jsonType.label" : "String" + } + }, { + "id" : "keycloak-admin-client-id-mapper", + "name" : "Client ID", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usersessionmodel-note-mapper", + "consentRequired" : false, + "config" : { + "user.session.note" : "client_id", + "introspection.token.claim" : "true", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "client_id", + "jsonType.label" : "String" + } + }, { + "id" : "keycloak-admin-client-host-mapper", + "name" : "Client Host", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usersessionmodel-note-mapper", + "consentRequired" : false, + "config" : { + "user.session.note" : "clientHost", + "introspection.token.claim" : "true", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "clientHost", + "jsonType.label" : "String" + } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] }, { "id" : "27ab4947-bc77-4123-abeb-3f0e375a04d8", "clientId" : "account", diff --git a/src/docker/test-tenant-creation.sh b/src/docker/test-tenant-creation.sh new file mode 100644 index 000000000..34a4fde23 --- /dev/null +++ b/src/docker/test-tenant-creation.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +# Test script to demonstrate tenant creation +# This script should be run after the Docker containers are up and running + +set -e + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${YELLOW}=========================================${NC}" +echo -e "${YELLOW}RelayServer Tenant Creation Test${NC}" +echo -e "${YELLOW}=========================================${NC}" +echo "" +echo "This script will demonstrate creating a new tenant in Keycloak." +echo "" +echo "Prerequisites:" +echo "1. Docker containers must be running (docker-compose up -d)" +echo "2. Wait for Keycloak to be fully initialized (about 30 seconds)" +echo "" +echo -e "${YELLOW}Press Enter to continue or Ctrl+C to cancel...${NC}" +read + +# Check if Keycloak is accessible +echo -e "${YELLOW}Checking Keycloak availability...${NC}" +if curl -s -f -o /dev/null "http://localhost:5002/realms/relayserver/.well-known/openid-configuration"; then + echo -e "${GREEN}✓ Keycloak is running and accessible${NC}" +else + echo "Error: Keycloak is not accessible at http://localhost:5002" + echo "Please ensure the containers are running: docker-compose up -d" + exit 1 +fi + +echo "" +echo -e "${YELLOW}Creating test tenant: TestTenant3${NC}" +echo "" + +# Create the test tenant +./create-tenant.sh TestTenant3 "Test Tenant 3" "Third test tenant for demonstration" + +echo "" +echo -e "${GREEN}=========================================${NC}" +echo -e "${GREEN}Test completed successfully!${NC}" +echo -e "${GREEN}=========================================${NC}" +echo "" +echo "You can verify the tenant was created by:" +echo "1. Logging into Keycloak Admin Console at http://localhost:5002" +echo " Username: admin" +echo " Password: admin" +echo "2. Navigate to the 'relayserver' realm → Clients" +echo "3. You should see 'TestTenant3' in the list" +echo "" +echo "To create additional tenants, run:" +echo " ./create-tenant.sh [description] [secret]" +echo "" \ No newline at end of file