diff --git a/scripts/thunder/01-default-resources.sh b/scripts/thunder/01-default-resources.sh new file mode 100755 index 00000000..dfd64032 --- /dev/null +++ b/scripts/thunder/01-default-resources.sh @@ -0,0 +1,1329 @@ +#!/bin/bash +# ---------------------------------------------------------------------------- +# Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). +# +# WSO2 LLC. licenses this file to you under the Apache License, +# Version 2.0 (the "License"); you may not use this file except +# in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# Bootstrap Script: Default Resources Setup +# Creates default organization unit, user schema, admin user, system resource server, system action, admin role, and DEVELOP application + +set -e + +# Parse command line arguments for custom redirect URIs +CUSTOM_DEVELOP_REDIRECT_URIS="" +while [[ $# -gt 0 ]]; do + case $1 in + --develop-redirect-uris) + CUSTOM_DEVELOP_REDIRECT_URIS="$2" + shift 2 + ;; + *) + shift + ;; + esac +done + +# Source common functions from the same directory as this script +SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]:-$0}")" +source "${SCRIPT_DIR}/common.sh" + +log_info "Creating default Thunder resources..." +echo "" + +# ============================================================================ +# Create Default Organization Unit +# ============================================================================ + +log_info "Creating default organization unit..." + +RESPONSE=$(thunder_api_call POST "/organization-units" '{ + "handle": "default", + "name": "Default", + "description": "Default organization unit" +}') + +HTTP_CODE="${RESPONSE: -3}" +BODY="${RESPONSE%???}" + +if [[ "$HTTP_CODE" == "201" ]] || [[ "$HTTP_CODE" == "200" ]]; then + log_success "Organization unit created successfully" + DEFAULT_OU_ID=$(echo "$BODY" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) + if [[ -n "$DEFAULT_OU_ID" ]]; then + log_info "Default OU ID: $DEFAULT_OU_ID" + else + log_error "Could not extract OU ID from response" + exit 1 + fi +elif [[ "$HTTP_CODE" == "409" ]]; then + log_warning "Organization unit already exists, retrieving OU ID..." + # Get existing OU ID by handle to ensure we get the correct "default" OU + RESPONSE=$(thunder_api_call GET "/organization-units/tree/default") + HTTP_CODE="${RESPONSE: -3}" + BODY="${RESPONSE%???}" + + if [[ "$HTTP_CODE" == "200" ]]; then + DEFAULT_OU_ID=$(echo "$BODY" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) + if [[ -n "$DEFAULT_OU_ID" ]]; then + log_success "Found OU ID: $DEFAULT_OU_ID" + else + log_error "Could not find OU ID in response" + exit 1 + fi + else + log_error "Failed to fetch organization unit by handle 'default' (HTTP $HTTP_CODE)" + exit 1 + fi +else + log_error "Failed to create organization unit (HTTP $HTTP_CODE)" + echo "Response: $BODY" + exit 1 +fi + +echo "" + +# ============================================================================ +# Create Default User Schema +# ============================================================================ + +log_info "Creating default user schema (person)..." + +RESPONSE=$(thunder_api_call POST "/user-schemas" '{ + "name": "Person", + "ouId": "'${DEFAULT_OU_ID}'", + "schema": { + "username": { + "type": "string", + "required": true, + "unique": true + }, + "email": { + "type": "string", + "required": true, + "unique": true, + "regex": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$" + }, + "email_verified": { + "type": "boolean", + "required": false + }, + "given_name": { + "type": "string", + "required": false + }, + "family_name": { + "type": "string", + "required": false + }, + "firstName": { + "type": "string", + "required": false + }, + "lastName": { + "type": "string", + "required": false + }, + "mobileNumber": { + "type": "string", + "required": false + }, + "phone_number": { + "type": "string", + "required": false + }, + "phone_number_verified": { + "type": "boolean", + "required": false + }, + "sub": { + "type": "string", + "required": false + }, + "name": { + "type": "string", + "required": false + }, + "picture": { + "type": "string", + "required": false + }, + "password": { + "type": "string", + "required": true, + "credential": true + } + }, + "systemAttributes": { + "display": "username" + } +}') + +HTTP_CODE="${RESPONSE: -3}" + +if [[ "$HTTP_CODE" == "201" ]] || [[ "$HTTP_CODE" == "200" ]]; then + log_success "User schema created successfully" +elif [[ "$HTTP_CODE" == "409" ]]; then + log_warning "User schema already exists, skipping" +else + log_error "Failed to create user schema (HTTP $HTTP_CODE)" + exit 1 +fi + +echo "" + +# ============================================================================ +# Create Admin User +# ============================================================================ + +log_info "Creating admin user..." + +# Read admin credentials from environment variables with defaults +ADMIN_USERNAME="${THUNDER_ADMIN_USERNAME:-admin}" +ADMIN_PASSWORD="${THUNDER_ADMIN_PASSWORD:-admin}" + +log_info "Using admin username: ${ADMIN_USERNAME}" + +RESPONSE=$(thunder_api_call POST "/users" '{ + "type": "Person", + "organizationUnit": "'${DEFAULT_OU_ID}'", + "attributes": { + "username": "'${ADMIN_USERNAME}'", + "password": "'${ADMIN_PASSWORD}'", + "sub": "admin", + "email": "admin@thunder.dev", + "email_verified": true, + "name": "Administrator", + "given_name": "Admin", + "family_name": "User", + "picture": "https://example.com/avatar.jpg", + "phone_number": "+12345678920", + "phone_number_verified": true + } +}') + +HTTP_CODE="${RESPONSE: -3}" +BODY="${RESPONSE%???}" + +if [[ "$HTTP_CODE" == "201" ]] || [[ "$HTTP_CODE" == "200" ]]; then + log_success "Admin user created successfully" + log_info "Username: ${ADMIN_USERNAME}" + log_info "Password: ${ADMIN_PASSWORD}" + + # Extract admin user ID + ADMIN_USER_ID=$(echo "$BODY" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) + if [[ -z "$ADMIN_USER_ID" ]]; then + log_warning "Could not extract admin user ID from response" + else + log_info "Admin user ID: $ADMIN_USER_ID" + fi +elif [[ "$HTTP_CODE" == "409" ]]; then + log_warning "Admin user already exists, retrieving user ID..." + + # Get existing admin user ID + RESPONSE=$(thunder_api_call GET "/users") + HTTP_CODE="${RESPONSE: -3}" + BODY="${RESPONSE%???}" + + if [[ "$HTTP_CODE" == "200" ]]; then + # Parse JSON to find admin user + ADMIN_USER_ID=$(echo "$BODY" | grep -o '"id":"[^"]*","[^"]*":"[^"]*","attributes":{[^}]*"username":"'"${ADMIN_USERNAME}"'"' | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) + + # Fallback parsing + if [[ -z "$ADMIN_USER_ID" ]]; then + ADMIN_USER_ID=$(echo "$BODY" | sed 's/},{/}\n{/g' | grep '"username":"'"${ADMIN_USERNAME}"'"' | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) + fi + + if [[ -n "$ADMIN_USER_ID" ]]; then + log_success "Found admin user ID: $ADMIN_USER_ID" + else + log_error "Could not find admin user in response" + exit 1 + fi + else + log_error "Failed to fetch users (HTTP $HTTP_CODE)" + exit 1 + fi +else + log_error "Failed to create admin user (HTTP $HTTP_CODE)" + echo "Response: $BODY" + exit 1 +fi + +echo "" + +# ============================================================================ +# Create System Resource Server +# ============================================================================ + +log_info "Creating system resource server..." + +if [[ -z "$DEFAULT_OU_ID" ]]; then + log_error "Default OU ID is not available. Cannot create resource server." + exit 1 +fi + +RESPONSE=$(thunder_api_call POST "/resource-servers" "{ + \"name\": \"System\", + \"description\": \"System resource server\", + \"identifier\": \"system\", + \"ouId\": \"${DEFAULT_OU_ID}\" +}") + +HTTP_CODE="${RESPONSE: -3}" +BODY="${RESPONSE%???}" + +if [[ "$HTTP_CODE" == "201" ]] || [[ "$HTTP_CODE" == "200" ]]; then + log_success "Resource server created successfully" + SYSTEM_RS_ID=$(echo "$BODY" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) + if [[ -n "$SYSTEM_RS_ID" ]]; then + log_info "System resource server ID: $SYSTEM_RS_ID" + else + log_error "Could not extract resource server ID from response" + exit 1 + fi +elif [[ "$HTTP_CODE" == "409" ]]; then + log_warning "Resource server already exists, retrieving ID..." + # Get existing resource server ID + RESPONSE=$(thunder_api_call GET "/resource-servers") + HTTP_CODE="${RESPONSE: -3}" + BODY="${RESPONSE%???}" + + if [[ "$HTTP_CODE" == "200" ]]; then + SYSTEM_RS_ID=$(echo "$BODY" | grep -o '"id":"[^"]*","[^"]*":"System"' | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) + + # Fallback parsing + if [[ -z "$SYSTEM_RS_ID" ]]; then + SYSTEM_RS_ID=$(echo "$BODY" | sed 's/},{/}\n{/g' | grep '"identifier":"system"' | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) + fi + + if [[ -n "$SYSTEM_RS_ID" ]]; then + log_success "Found resource server ID: $SYSTEM_RS_ID" + else + log_error "Could not find resource server ID in response" + exit 1 + fi + else + log_error "Failed to fetch resource servers (HTTP $HTTP_CODE)" + exit 1 + fi +else + log_error "Failed to create resource server (HTTP $HTTP_CODE)" + echo "Response: $BODY" + exit 1 +fi + +echo "" + +# ============================================================================ +# Create System Resource Permissions (hierarchical permission model) +# ============================================================================ +# +# Permission auto-derivation: +# Resource Server identifier "system" +# └── Resource handle "system" → permission "system" +# └── Resource handle "ou" → permission "system:ou" +# └── Action handle "view" → permission "system:ou:view" +# └── Resource handle "user" → permission "system:user" +# └── Action handle "view" → permission "system:user:view" +# └── Resource handle "group" → permission "system:group" +# └── Action handle "view" → permission "system:group:view" +# └── Resource handle "userschema" → permission "system:userschema" +# └── Action handle "view" → permission "system:userschema:view" +# ============================================================================ + +log_info "Creating 'system' resource under the system resource server..." + +if [[ -z "$SYSTEM_RS_ID" ]]; then + log_error "System resource server ID is not available. Cannot create system resource." + exit 1 +fi + +RESPONSE=$(thunder_api_call POST "/resource-servers/${SYSTEM_RS_ID}/resources" '{ + "name": "System", + "description": "System resource", + "handle": "system" +}') + +HTTP_CODE="${RESPONSE: -3}" +BODY="${RESPONSE%???}" + +if [[ "$HTTP_CODE" == "201" ]] || [[ "$HTTP_CODE" == "200" ]]; then + log_success "System resource created successfully (permission: system)" + SYSTEM_RESOURCE_ID=$(echo "$BODY" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) + if [[ -n "$SYSTEM_RESOURCE_ID" ]]; then + log_info "System resource ID: $SYSTEM_RESOURCE_ID" + else + log_error "Could not extract system resource ID from response" + exit 1 + fi +elif [[ "$HTTP_CODE" == "409" ]]; then + log_warning "System resource already exists, retrieving ID..." + RESPONSE=$(thunder_api_call GET "/resource-servers/${SYSTEM_RS_ID}/resources") + HTTP_CODE="${RESPONSE: -3}" + BODY="${RESPONSE%???}" + + if [[ "$HTTP_CODE" == "200" ]]; then + SYSTEM_RESOURCE_ID=$(echo "$BODY" | sed 's/},{/}\n{/g' | grep '"handle":"system"' | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) + if [[ -n "$SYSTEM_RESOURCE_ID" ]]; then + log_success "Found system resource ID: $SYSTEM_RESOURCE_ID" + else + log_error "Could not find system resource in response" + exit 1 + fi + else + log_error "Failed to fetch resources (HTTP $HTTP_CODE)" + exit 1 + fi +else + log_error "Failed to create system resource (HTTP $HTTP_CODE)" + echo "Response: $BODY" + exit 1 +fi + +log_info "Creating 'ou' sub-resource under the 'system' resource..." + +RESPONSE=$(thunder_api_call POST "/resource-servers/${SYSTEM_RS_ID}/resources" "{ + \"name\": \"Organization Unit\", + \"description\": \"Organization unit resource\", + \"handle\": \"ou\", + \"parent\": \"${SYSTEM_RESOURCE_ID}\" +}") + +HTTP_CODE="${RESPONSE: -3}" +BODY="${RESPONSE%???}" + +if [[ "$HTTP_CODE" == "201" ]] || [[ "$HTTP_CODE" == "200" ]]; then + log_success "OU resource created successfully (permission: system:ou)" + OU_RESOURCE_ID=$(echo "$BODY" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) + if [[ -n "$OU_RESOURCE_ID" ]]; then + log_info "OU resource ID: $OU_RESOURCE_ID" + else + log_error "Could not extract OU resource ID from response" + exit 1 + fi +elif [[ "$HTTP_CODE" == "409" ]]; then + log_warning "OU resource already exists, retrieving ID..." + RESPONSE=$(thunder_api_call GET "/resource-servers/${SYSTEM_RS_ID}/resources?parentId=${SYSTEM_RESOURCE_ID}") + HTTP_CODE="${RESPONSE: -3}" + BODY="${RESPONSE%???}" + + if [[ "$HTTP_CODE" == "200" ]]; then + OU_RESOURCE_ID=$(echo "$BODY" | sed 's/},{/}\n{/g' | grep '"handle":"ou"' | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) + if [[ -n "$OU_RESOURCE_ID" ]]; then + log_success "Found OU resource ID: $OU_RESOURCE_ID" + else + log_error "Could not find OU resource in response" + exit 1 + fi + else + log_error "Failed to fetch resources (HTTP $HTTP_CODE)" + exit 1 + fi +else + log_error "Failed to create OU resource (HTTP $HTTP_CODE)" + echo "Response: $BODY" + exit 1 +fi + +log_info "Creating 'view' action under the 'ou' resource..." + +RESPONSE=$(thunder_api_call POST "/resource-servers/${SYSTEM_RS_ID}/resources/${OU_RESOURCE_ID}/actions" '{ + "name": "View", + "description": "Read-only access to organization units", + "handle": "view" +}') + +HTTP_CODE="${RESPONSE: -3}" +BODY="${RESPONSE%???}" + +if [[ "$HTTP_CODE" == "201" ]] || [[ "$HTTP_CODE" == "200" ]]; then + log_success "OU view action created successfully (permission: system:ou:view)" +elif [[ "$HTTP_CODE" == "409" ]]; then + log_warning "OU view action already exists, skipping" +else + log_error "Failed to create OU view action (HTTP $HTTP_CODE)" + echo "Response: $BODY" + exit 1 +fi + +log_info "Creating 'user' sub-resource under the 'system' resource..." + +RESPONSE=$(thunder_api_call POST "/resource-servers/${SYSTEM_RS_ID}/resources" "{ + \"name\": \"User\", + \"description\": \"User resource\", + \"handle\": \"user\", + \"parent\": \"${SYSTEM_RESOURCE_ID}\" +}") + +HTTP_CODE="${RESPONSE: -3}" +BODY="${RESPONSE%???}" + +if [[ "$HTTP_CODE" == "201" ]] || [[ "$HTTP_CODE" == "200" ]]; then + log_success "User resource created successfully (permission: system:user)" + USER_RESOURCE_ID=$(echo "$BODY" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) + if [[ -n "$USER_RESOURCE_ID" ]]; then + log_info "User resource ID: $USER_RESOURCE_ID" + else + log_error "Could not extract user resource ID from response" + exit 1 + fi +elif [[ "$HTTP_CODE" == "409" ]]; then + log_warning "User resource already exists, retrieving ID..." + RESPONSE=$(thunder_api_call GET "/resource-servers/${SYSTEM_RS_ID}/resources?parentId=${SYSTEM_RESOURCE_ID}") + HTTP_CODE="${RESPONSE: -3}" + BODY="${RESPONSE%???}" + + if [[ "$HTTP_CODE" == "200" ]]; then + USER_RESOURCE_ID=$(echo "$BODY" | sed 's/},{/}\n{/g' | grep '"handle":"user"' | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) + if [[ -n "$USER_RESOURCE_ID" ]]; then + log_success "Found user resource ID: $USER_RESOURCE_ID" + else + log_error "Could not find user resource in response" + exit 1 + fi + else + log_error "Failed to fetch resources (HTTP $HTTP_CODE)" + exit 1 + fi +else + log_error "Failed to create user resource (HTTP $HTTP_CODE)" + echo "Response: $BODY" + exit 1 +fi + +log_info "Creating 'view' action under the 'user' resource..." + +RESPONSE=$(thunder_api_call POST "/resource-servers/${SYSTEM_RS_ID}/resources/${USER_RESOURCE_ID}/actions" '{ + "name": "View", + "description": "Read-only access to users", + "handle": "view" +}') + +HTTP_CODE="${RESPONSE: -3}" +BODY="${RESPONSE%???}" + +if [[ "$HTTP_CODE" == "201" ]] || [[ "$HTTP_CODE" == "200" ]]; then + log_success "User view action created successfully (permission: system:user:view)" +elif [[ "$HTTP_CODE" == "409" ]]; then + log_warning "User view action already exists, skipping" +else + log_error "Failed to create user view action (HTTP $HTTP_CODE)" + echo "Response: $BODY" + exit 1 +fi + +log_info "Creating 'userschema' sub-resource under the 'system' resource..." + +RESPONSE=$(thunder_api_call POST "/resource-servers/${SYSTEM_RS_ID}/resources" "{ + \"name\": \"User Schema\", + \"description\": \"User schema resource\", + \"handle\": \"userschema\", + \"parent\": \"${SYSTEM_RESOURCE_ID}\" +}") + +HTTP_CODE="${RESPONSE: -3}" +BODY="${RESPONSE%???}" + +if [[ "$HTTP_CODE" == "201" ]] || [[ "$HTTP_CODE" == "200" ]]; then + log_success "User schema resource created successfully (permission: system:userschema)" + USER_SCHEMA_RESOURCE_ID=$(echo "$BODY" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) + if [[ -n "$USER_SCHEMA_RESOURCE_ID" ]]; then + log_info "User schema resource ID: $USER_SCHEMA_RESOURCE_ID" + else + log_error "Could not extract user schema resource ID from response" + exit 1 + fi +elif [[ "$HTTP_CODE" == "409" ]]; then + log_warning "User schema resource already exists, retrieving ID..." + RESPONSE=$(thunder_api_call GET "/resource-servers/${SYSTEM_RS_ID}/resources?parentId=${SYSTEM_RESOURCE_ID}") + HTTP_CODE="${RESPONSE: -3}" + BODY="${RESPONSE%???}" + + if [[ "$HTTP_CODE" == "200" ]]; then + USER_SCHEMA_RESOURCE_ID=$(echo "$BODY" | sed 's/},{/}\n{/g' | grep '"handle":"userschema"' | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) + if [[ -n "$USER_SCHEMA_RESOURCE_ID" ]]; then + log_success "Found user schema resource ID: $USER_SCHEMA_RESOURCE_ID" + else + log_error "Could not find user schema resource in response" + exit 1 + fi + else + log_error "Failed to fetch resources (HTTP $HTTP_CODE)" + exit 1 + fi +else + log_error "Failed to create user schema resource (HTTP $HTTP_CODE)" + echo "Response: $BODY" + exit 1 +fi + +log_info "Creating 'view' action under the 'userschema' resource..." + +RESPONSE=$(thunder_api_call POST "/resource-servers/${SYSTEM_RS_ID}/resources/${USER_SCHEMA_RESOURCE_ID}/actions" '{ + "name": "View", + "description": "Read-only access to user schemas", + "handle": "view" +}') + +HTTP_CODE="${RESPONSE: -3}" +BODY="${RESPONSE%???}" + +if [[ "$HTTP_CODE" == "201" ]] || [[ "$HTTP_CODE" == "200" ]]; then + log_success "User schema view action created successfully (permission: system:userschema:view)" +elif [[ "$HTTP_CODE" == "409" ]]; then + log_warning "User schema view action already exists, skipping" +else + log_error "Failed to create user schema view action (HTTP $HTTP_CODE)" + echo "Response: $BODY" + exit 1 +fi + +echo "" + +log_info "Creating 'group' sub-resource under the 'system' resource..." + +RESPONSE=$(thunder_api_call POST "/resource-servers/${SYSTEM_RS_ID}/resources" "{ + \"name\": \"Group\", + \"description\": \"Group resource\", + \"handle\": \"group\", + \"parent\": \"${SYSTEM_RESOURCE_ID}\" +}") + +HTTP_CODE="${RESPONSE: -3}" +BODY="${RESPONSE%???}" + +if [[ "$HTTP_CODE" == "201" ]] || [[ "$HTTP_CODE" == "200" ]]; then + log_success "Group resource created successfully (permission: system:group)" + GROUP_RESOURCE_ID=$(echo "$BODY" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) + if [[ -n "$GROUP_RESOURCE_ID" ]]; then + log_info "Group resource ID: $GROUP_RESOURCE_ID" + else + log_error "Could not extract group resource ID from response" + exit 1 + fi +elif [[ "$HTTP_CODE" == "409" ]]; then + log_warning "Group resource already exists, retrieving ID..." + RESPONSE=$(thunder_api_call GET "/resource-servers/${SYSTEM_RS_ID}/resources?parentId=${SYSTEM_RESOURCE_ID}") + HTTP_CODE="${RESPONSE: -3}" + BODY="${RESPONSE%???}" + + if [[ "$HTTP_CODE" == "200" ]]; then + GROUP_RESOURCE_ID=$(echo "$BODY" | sed 's/},{/}\n{/g' | grep '"handle":"group"' | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) + if [[ -n "$GROUP_RESOURCE_ID" ]]; then + log_success "Found group resource ID: $GROUP_RESOURCE_ID" + else + log_error "Could not find group resource in response" + exit 1 + fi + else + log_error "Failed to fetch resources (HTTP $HTTP_CODE)" + exit 1 + fi +else + log_error "Failed to create group resource (HTTP $HTTP_CODE)" + echo "Response: $BODY" + exit 1 +fi + +log_info "Creating 'view' action under the 'group' resource..." + +RESPONSE=$(thunder_api_call POST "/resource-servers/${SYSTEM_RS_ID}/resources/${GROUP_RESOURCE_ID}/actions" '{ + "name": "View", + "description": "Read-only access to groups", + "handle": "view" +}') + +HTTP_CODE="${RESPONSE: -3}" +BODY="${RESPONSE%???}" + +if [[ "$HTTP_CODE" == "201" ]] || [[ "$HTTP_CODE" == "200" ]]; then + log_success "Group view action created successfully (permission: system:group:view)" +elif [[ "$HTTP_CODE" == "409" ]]; then + log_warning "Group view action already exists, skipping" +else + log_error "Failed to create group view action (HTTP $HTTP_CODE)" + echo "Response: $BODY" + exit 1 +fi + +echo "" + +# ============================================================================ +# Create Admin Role +# ============================================================================ + +log_info "Creating admin role with 'system' permission..." + +if [[ -z "$ADMIN_USER_ID" ]]; then + log_error "Admin user ID is not available. Cannot create role." + exit 1 +fi + +if [[ -z "$DEFAULT_OU_ID" ]]; then + log_error "Default OU ID is not available. Cannot create role." + exit 1 +fi + +if [[ -z "$SYSTEM_RS_ID" ]]; then + log_error "System resource server ID is not available. Cannot create role." + exit 1 +fi + +RESPONSE=$(thunder_api_call POST "/roles" "{ + \"name\": \"Administrator\", + \"description\": \"System administrator role with full permissions\", + \"ouId\": \"${DEFAULT_OU_ID}\", + \"permissions\": [ + { + \"resourceServerId\": \"${SYSTEM_RS_ID}\", + \"permissions\": [\"system\"] + } + ], + \"assignments\": [ + { + \"id\": \"${ADMIN_USER_ID}\", + \"type\": \"user\" + } + ] +}") + +HTTP_CODE="${RESPONSE: -3}" +BODY="${RESPONSE%???}" + +if [[ "$HTTP_CODE" == "201" ]] || [[ "$HTTP_CODE" == "200" ]]; then + log_success "Admin role created and assigned to admin user" + ADMIN_ROLE_ID=$(echo "$BODY" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) + if [[ -n "$ADMIN_ROLE_ID" ]]; then + log_info "Admin role ID: $ADMIN_ROLE_ID" + fi +elif [[ "$HTTP_CODE" == "409" ]]; then + log_warning "Admin role already exists" +else + log_error "Failed to create admin role (HTTP $HTTP_CODE)" + echo "Response: $BODY" + exit 1 +fi + +echo "" + +# ============================================================================ +# Create Default Flows +# ============================================================================ + +log_info "Creating default flows..." + +# Path to flow definitions directories +AUTH_FLOWS_DIR="${SCRIPT_DIR}/flows/authentication" +REG_FLOWS_DIR="${SCRIPT_DIR}/flows/registration" +USER_ONBOARDING_FLOWS_DIR="${SCRIPT_DIR}/flows/user_onboarding" + +# Check if flows directory exists +if [[ ! -d "$AUTH_FLOWS_DIR" ]] && [[ ! -d "$REG_FLOWS_DIR" ]] && [[ ! -d "$USER_ONBOARDING_FLOWS_DIR" ]]; then + log_warning "Flow definition directories not found, skipping flow creation" +else + FLOW_COUNT=0 + FLOW_SUCCESS=0 + FLOW_SKIPPED=0 + + # Process authentication flows + if [[ -d "$AUTH_FLOWS_DIR" ]]; then + shopt -s nullglob + AUTH_FILES=("$AUTH_FLOWS_DIR"/*.json) + shopt -u nullglob + + if [[ ${#AUTH_FILES[@]} -gt 0 ]]; then + log_info "Processing authentication flows..." + + # Fetch existing auth flows + RESPONSE=$(thunder_api_call GET "/flows?flowType=AUTHENTICATION&limit=200") + HTTP_CODE="${RESPONSE: -3}" + BODY="${RESPONSE%???}" + + # Store existing auth flows as "handle|id" pairs + EXISTING_AUTH_FLOWS="" + if [[ "$HTTP_CODE" == "200" ]]; then + while IFS= read -r line; do + FLOW_ID=$(echo "$line" | grep -o '"id":"[^"]*"' | cut -d'"' -f4) + FLOW_HANDLE=$(echo "$line" | grep -o '"handle":"[^"]*"' | cut -d'"' -f4) + if [[ -n "$FLOW_ID" ]] && [[ -n "$FLOW_HANDLE" ]]; then + EXISTING_AUTH_FLOWS="${EXISTING_AUTH_FLOWS}${FLOW_HANDLE}|${FLOW_ID}"$'\n' + log_debug "Found existing auth flow: handle=$FLOW_HANDLE (ID: $FLOW_ID)" + fi + done < <(echo "$BODY" | grep -o '{[^}]*"id":"[^"]*"[^}]*"handle":"[^"]*"[^}]*}') + fi + + log_debug "Total existing auth flows found: $(echo "$EXISTING_AUTH_FLOWS" | grep -c '|' || echo 0)" + + for FLOW_FILE in "$AUTH_FLOWS_DIR"/*.json; do + [[ ! -f "$FLOW_FILE" ]] && continue + + FLOW_COUNT=$((FLOW_COUNT + 1)) + FLOW_HANDLE=$(grep -o '"handle"[[:space:]]*:[[:space:]]*"[^"]*"' "$FLOW_FILE" | head -1 | sed 's/"handle"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/') + FLOW_NAME=$(grep -o '"name"[[:space:]]*:[[:space:]]*"[^"]*"' "$FLOW_FILE" | head -1 | sed 's/"name"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/') + log_debug "Processing flow file: $FLOW_FILE with handle: $FLOW_HANDLE, name: $FLOW_NAME" + + # Check if flow exists by handle + if echo "$EXISTING_AUTH_FLOWS" | grep -q "^${FLOW_HANDLE}|"; then + # Update existing flow + FLOW_ID=$(echo "$EXISTING_AUTH_FLOWS" | grep "^${FLOW_HANDLE}|" | cut -d'|' -f2) + log_info "Updating existing auth flow: $FLOW_NAME (handle: $FLOW_HANDLE)" + update_flow "$FLOW_ID" "$FLOW_FILE" + RESULT=$? + if [[ $RESULT -eq 0 ]]; then + FLOW_SUCCESS=$((FLOW_SUCCESS + 1)) + fi + else + # Create new flow + create_flow "$FLOW_FILE" + RESULT=$? + if [[ $RESULT -eq 0 ]]; then + FLOW_SUCCESS=$((FLOW_SUCCESS + 1)) + elif [[ $RESULT -eq 2 ]]; then + FLOW_SKIPPED=$((FLOW_SKIPPED + 1)) + fi + fi + done + else + log_warning "No authentication flow files found" + fi + fi + + # Process registration flows + if [[ -d "$REG_FLOWS_DIR" ]]; then + shopt -s nullglob + REG_FILES=("$REG_FLOWS_DIR"/*.json) + shopt -u nullglob + + if [[ ${#REG_FILES[@]} -gt 0 ]]; then + log_info "Processing registration flows..." + + # Fetch existing registration flows + RESPONSE=$(thunder_api_call GET "/flows?flowType=REGISTRATION&limit=200") + HTTP_CODE="${RESPONSE: -3}" + BODY="${RESPONSE%???}" + + # Store existing registration flows as "handle|id" pairs + EXISTING_REG_FLOWS="" + if [[ "$HTTP_CODE" == "200" ]]; then + while IFS= read -r line; do + FLOW_ID=$(echo "$line" | grep -o '"id":"[^"]*"' | cut -d'"' -f4) + FLOW_HANDLE=$(echo "$line" | grep -o '"handle":"[^"]*"' | cut -d'"' -f4) + if [[ -n "$FLOW_ID" ]] && [[ -n "$FLOW_HANDLE" ]]; then + EXISTING_REG_FLOWS="${EXISTING_REG_FLOWS}${FLOW_HANDLE}|${FLOW_ID}"$'\n' + fi + done < <(echo "$BODY" | grep -o '{[^}]*"id":"[^"]*"[^}]*"handle":"[^"]*"[^}]*}') + fi + + for FLOW_FILE in "$REG_FLOWS_DIR"/*.json; do + [[ ! -f "$FLOW_FILE" ]] && continue + + FLOW_COUNT=$((FLOW_COUNT + 1)) + FLOW_HANDLE=$(grep -o '"handle"[[:space:]]*:[[:space:]]*"[^"]*"' "$FLOW_FILE" | head -1 | sed 's/"handle"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/') + FLOW_NAME=$(grep -o '"name"[[:space:]]*:[[:space:]]*"[^"]*"' "$FLOW_FILE" | head -1 | sed 's/"name"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/') + + # Check if flow exists by handle + if echo "$EXISTING_REG_FLOWS" | grep -q "^${FLOW_HANDLE}|"; then + # Update existing flow + FLOW_ID=$(echo "$EXISTING_REG_FLOWS" | grep "^${FLOW_HANDLE}|" | cut -d'|' -f2) + log_info "Updating existing registration flow: $FLOW_NAME (handle: $FLOW_HANDLE)" + update_flow "$FLOW_ID" "$FLOW_FILE" + RESULT=$? + if [[ $RESULT -eq 0 ]]; then + FLOW_SUCCESS=$((FLOW_SUCCESS + 1)) + fi + else + # Create new flow + create_flow "$FLOW_FILE" + RESULT=$? + if [[ $RESULT -eq 0 ]]; then + FLOW_SUCCESS=$((FLOW_SUCCESS + 1)) + elif [[ $RESULT -eq 2 ]]; then + FLOW_SKIPPED=$((FLOW_SKIPPED + 1)) + fi + fi + done + else + log_warning "No registration flow files found" + fi + fi + + # Process user onboarding flows + if [[ -d "$USER_ONBOARDING_FLOWS_DIR" ]]; then + shopt -s nullglob + INVITE_FILES=("$USER_ONBOARDING_FLOWS_DIR"/*.json) + shopt -u nullglob + + if [[ ${#INVITE_FILES[@]} -gt 0 ]]; then + log_info "Processing user onboarding flows..." + + # Fetch existing user onboarding flows + RESPONSE=$(thunder_api_call GET "/flows?flowType=USER_ONBOARDING&limit=200") + HTTP_CODE="${RESPONSE: -3}" + BODY="${RESPONSE%???}" + + # Store existing user onboarding flows as "handle|id" pairs + EXISTING_INVITE_FLOWS="" + if [[ "$HTTP_CODE" == "200" ]]; then + while IFS= read -r line; do + FLOW_ID=$(echo "$line" | grep -o '"id":"[^"]*"' | cut -d'"' -f4) + FLOW_HANDLE=$(echo "$line" | grep -o '"handle":"[^"]*"' | cut -d'"' -f4) + if [[ -n "$FLOW_ID" ]] && [[ -n "$FLOW_HANDLE" ]]; then + EXISTING_INVITE_FLOWS="${EXISTING_INVITE_FLOWS}${FLOW_HANDLE}|${FLOW_ID}"$'\n' + fi + done < <(echo "$BODY" | grep -o '{[^}]*"id":"[^"]*"[^}]*"handle":"[^"]*"[^}]*}') + fi + + for FLOW_FILE in "$USER_ONBOARDING_FLOWS_DIR"/*.json; do + [[ ! -f "$FLOW_FILE" ]] && continue + + FLOW_COUNT=$((FLOW_COUNT + 1)) + FLOW_HANDLE=$(grep -o '"handle"[[:space:]]*:[[:space:]]*"[^"]*"' "$FLOW_FILE" | head -1 | sed 's/"handle"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/') + FLOW_NAME=$(grep -o '"name"[[:space:]]*:[[:space:]]*"[^"]*"' "$FLOW_FILE" | head -1 | sed 's/"name"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/') + + # Check if flow exists by handle + if echo "$EXISTING_INVITE_FLOWS" | grep -q "^${FLOW_HANDLE}|"; then + # Update existing flow + FLOW_ID=$(echo "$EXISTING_INVITE_FLOWS" | grep "^${FLOW_HANDLE}|" | cut -d'|' -f2) + log_info "Updating existing user onboarding flow: $FLOW_NAME (handle: $FLOW_HANDLE)" + update_flow "$FLOW_ID" "$FLOW_FILE" + RESULT=$? + if [[ $RESULT -eq 0 ]]; then + FLOW_SUCCESS=$((FLOW_SUCCESS + 1)) + fi + else + # Create new flow + create_flow "$FLOW_FILE" + RESULT=$? + if [[ $RESULT -eq 0 ]]; then + FLOW_SUCCESS=$((FLOW_SUCCESS + 1)) + elif [[ $RESULT -eq 2 ]]; then + FLOW_SKIPPED=$((FLOW_SKIPPED + 1)) + fi + fi + done + else + log_debug "No user onboarding flow files found" + fi + fi + + if [[ $FLOW_COUNT -gt 0 ]]; then + log_info "Flow creation summary: $FLOW_SUCCESS created/updated, $FLOW_SKIPPED skipped, $((FLOW_COUNT - FLOW_SUCCESS - FLOW_SKIPPED)) failed" + fi +fi + +echo "" + +# ============================================================================ +# Create Application-Specific Flows +# ============================================================================ + +log_info "Creating application-specific flows..." + +APPS_FLOWS_DIR="${SCRIPT_DIR}/flows/apps" + +# Store application flow IDs as "app_name|auth_flow_id|reg_flow_id" pairs +APP_FLOW_IDS="" + +if [[ -d "$APPS_FLOWS_DIR" ]]; then + # Fetch all existing flows once + log_info "Fetching existing flows for application flow processing..." + + # Get auth flows + RESPONSE=$(thunder_api_call GET "/flows?flowType=AUTHENTICATION&limit=200") + HTTP_CODE="${RESPONSE: -3}" + BODY="${RESPONSE%???}" + EXISTING_APP_AUTH_FLOWS="" + if [[ "$HTTP_CODE" == "200" ]]; then + while IFS= read -r line; do + FLOW_ID=$(echo "$line" | grep -o '"id":"[^"]*"' | cut -d'"' -f4) + FLOW_HANDLE=$(echo "$line" | grep -o '"handle":"[^"]*"' | cut -d'"' -f4) + if [[ -n "$FLOW_ID" ]] && [[ -n "$FLOW_HANDLE" ]]; then + EXISTING_APP_AUTH_FLOWS="${EXISTING_APP_AUTH_FLOWS}${FLOW_HANDLE}|${FLOW_ID}"$'\n' + fi + done < <(echo "$BODY" | grep -o '{[^}]*"id":"[^"]*"[^}]*"handle":"[^"]*"[^}]*}') + fi + + # Get registration flows + RESPONSE=$(thunder_api_call GET "/flows?flowType=REGISTRATION&limit=200") + HTTP_CODE="${RESPONSE: -3}" + BODY="${RESPONSE%???}" + EXISTING_APP_REG_FLOWS="" + if [[ "$HTTP_CODE" == "200" ]]; then + while IFS= read -r line; do + FLOW_ID=$(echo "$line" | grep -o '"id":"[^"]*"' | cut -d'"' -f4) + FLOW_HANDLE=$(echo "$line" | grep -o '"handle":"[^"]*"' | cut -d'"' -f4) + if [[ -n "$FLOW_ID" ]] && [[ -n "$FLOW_HANDLE" ]]; then + EXISTING_APP_REG_FLOWS="${EXISTING_APP_REG_FLOWS}${FLOW_HANDLE}|${FLOW_ID}"$'\n' + fi + done < <(echo "$BODY" | grep -o '{[^}]*"id":"[^"]*"[^}]*"handle":"[^"]*"[^}]*}') + fi + + # Process each application directory + for APP_DIR in "$APPS_FLOWS_DIR"/*; do + [[ ! -d "$APP_DIR" ]] && continue + + APP_NAME=$(basename "$APP_DIR") + APP_AUTH_FLOW_ID="" + APP_REG_FLOW_ID="" + + log_info "Processing flows for application: $APP_NAME" + + # Process authentication flow for app + shopt -s nullglob + AUTH_FLOW_FILES=("$APP_DIR"/auth_*.json) + shopt -u nullglob + + if [[ ${#AUTH_FLOW_FILES[@]} -gt 0 ]]; then + AUTH_FLOW_FILE="${AUTH_FLOW_FILES[0]}" + FLOW_HANDLE=$(grep -o '"handle"[[:space:]]*:[[:space:]]*"[^"]*"' "$AUTH_FLOW_FILE" | head -1 | sed 's/"handle"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/') + FLOW_NAME=$(grep -o '"name"[[:space:]]*:[[:space:]]*"[^"]*"' "$AUTH_FLOW_FILE" | head -1 | sed 's/"name"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/') + + # Check if auth flow exists by handle + if echo "$EXISTING_APP_AUTH_FLOWS" | grep -q "^${FLOW_HANDLE}|"; then + # Update existing flow + APP_AUTH_FLOW_ID=$(echo "$EXISTING_APP_AUTH_FLOWS" | grep "^${FLOW_HANDLE}|" | cut -d'|' -f2) + log_info "Updating existing auth flow: $FLOW_NAME (handle: $FLOW_HANDLE)" + update_flow "$APP_AUTH_FLOW_ID" "$AUTH_FLOW_FILE" + else + # Create new flow + APP_AUTH_FLOW_ID=$(create_flow "$AUTH_FLOW_FILE") + fi + + # Re-fetch registration flows after creating auth flow + if [[ -n "$APP_AUTH_FLOW_ID" ]]; then + RESPONSE=$(thunder_api_call GET "/flows?flowType=REGISTRATION&limit=200") + HTTP_CODE="${RESPONSE: -3}" + BODY="${RESPONSE%???}" + EXISTING_APP_REG_FLOWS="" + if [[ "$HTTP_CODE" == "200" ]]; then + while IFS= read -r line; do + FLOW_ID=$(echo "$line" | grep -o '"id":"[^"]*"' | cut -d'"' -f4) + FLOW_HANDLE_TEMP=$(echo "$line" | grep -o '"handle":"[^"]*"' | cut -d'"' -f4) + if [[ -n "$FLOW_ID" ]] && [[ -n "$FLOW_HANDLE_TEMP" ]]; then + EXISTING_APP_REG_FLOWS="${EXISTING_APP_REG_FLOWS}${FLOW_HANDLE_TEMP}|${FLOW_ID}"$'\n' + fi + done < <(echo "$BODY" | grep -o '{[^}]*"id":"[^"]*"[^}]*"handle":"[^"]*"[^}]*}') + fi + fi + else + log_warning "No authentication flow file found for app: $APP_NAME" + fi + + # Process registration flow for app + shopt -s nullglob + REG_FLOW_FILES=("$APP_DIR"/registration_*.json) + shopt -u nullglob + + if [[ ${#REG_FLOW_FILES[@]} -gt 0 ]]; then + REG_FLOW_FILE="${REG_FLOW_FILES[0]}" + FLOW_HANDLE=$(grep -o '"handle"[[:space:]]*:[[:space:]]*"[^"]*"' "$REG_FLOW_FILE" | head -1 | sed 's/"handle"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/') + FLOW_NAME=$(grep -o '"name"[[:space:]]*:[[:space:]]*"[^"]*"' "$REG_FLOW_FILE" | head -1 | sed 's/"name"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/') + + # Check if registration flow exists by handle + if echo "$EXISTING_APP_REG_FLOWS" | grep -q "^${FLOW_HANDLE}|"; then + # Update existing flow + APP_REG_FLOW_ID=$(echo "$EXISTING_APP_REG_FLOWS" | grep "^${FLOW_HANDLE}|" | cut -d'|' -f2) + log_info "Updating existing registration flow: $FLOW_NAME (handle: $FLOW_HANDLE)" + update_flow "$APP_REG_FLOW_ID" "$REG_FLOW_FILE" + else + # Create new flow + APP_REG_FLOW_ID=$(create_flow "$REG_FLOW_FILE") + fi + else + log_warning "No registration flow file found for app: $APP_NAME" + fi + + # Store the flow IDs for this app + log_debug "Storing flow IDs for $APP_NAME: auth=$APP_AUTH_FLOW_ID, reg=$APP_REG_FLOW_ID" + APP_FLOW_IDS="${APP_FLOW_IDS}${APP_NAME}|${APP_AUTH_FLOW_ID}|${APP_REG_FLOW_ID}"$'\n' + done +else + log_warning "Application flows directory not found at $APPS_FLOWS_DIR" +fi + +echo "" + +# ============================================================================ +# Create DEVELOP Application +# ============================================================================ + +log_info "Creating DEVELOP application..." + +# Get flow IDs for develop app from the APP_FLOW_IDS created/found during flow processing +DEVELOP_AUTH_FLOW_ID=$(echo "$APP_FLOW_IDS" | grep "^develop|" | cut -d'|' -f2) +DEVELOP_REG_FLOW_ID=$(echo "$APP_FLOW_IDS" | grep "^develop|" | cut -d'|' -f3) +log_debug "Extracted flow IDs: auth=$DEVELOP_AUTH_FLOW_ID, reg=$DEVELOP_REG_FLOW_ID" + +# Validate that flow IDs are available +if [[ -z "$DEVELOP_AUTH_FLOW_ID" ]]; then + log_error "Develop authentication flow ID not found, cannot create DEVELOP application" + exit 1 +fi +if [[ -z "$DEVELOP_REG_FLOW_ID" ]]; then + log_error "Develop registration flow ID not found, cannot create DEVELOP application" + exit 1 +fi + +# Use THUNDER_PUBLIC_URL for redirect URIs, fallback to THUNDER_API_BASE if not set +PUBLIC_URL="${THUNDER_PUBLIC_URL:-$THUNDER_API_BASE}" + +# Build redirect URIs array - default + custom if provided +REDIRECT_URIS="\"${PUBLIC_URL}/develop\"" +if [[ -n "$CUSTOM_DEVELOP_REDIRECT_URIS" ]]; then + log_info "Adding custom redirect URIs: $CUSTOM_DEVELOP_REDIRECT_URIS" + # Split comma-separated URIs and append to array + IFS=',' read -ra URI_ARRAY <<< "$CUSTOM_DEVELOP_REDIRECT_URIS" + for uri in "${URI_ARRAY[@]}"; do + # Trim whitespace + uri=$(echo "$uri" | xargs) + REDIRECT_URIS="${REDIRECT_URIS},\"${uri}\"" + done +fi + +RESPONSE=$(thunder_api_call POST "/applications" "{ + \"name\": \"Develop\", + \"description\": \"Developer application for Thunder\", + \"url\": \"${PUBLIC_URL}/develop\", + \"logo_url\": \"${PUBLIC_URL}/develop/assets/images/logo-mini.svg\", + \"auth_flow_id\": \"${DEVELOP_AUTH_FLOW_ID}\", + \"registration_flow_id\": \"${DEVELOP_REG_FLOW_ID}\", + \"is_registration_flow_enabled\": false, + \"allowed_user_types\": [\"Person\"], + \"user_attributes\": [\"given_name\",\"family_name\",\"email\",\"groups\", \"name\", \"ouId\"], + \"inbound_auth_config\": [{ + \"type\": \"oauth2\", + \"config\": { + \"client_id\": \"DEVELOP\", + \"redirect_uris\": [${REDIRECT_URIS}], + \"grant_types\": [\"authorization_code\"], + \"response_types\": [\"code\"], + \"pkce_required\": true, + \"token_endpoint_auth_method\": \"none\", + \"public_client\": true, + \"token\": { + \"access_token\": { + \"validity_period\": 3600, + \"user_attributes\": [\"given_name\",\"family_name\",\"email\",\"groups\", \"name\", \"ouId\"] + }, + \"id_token\": { + \"validity_period\": 3600, + \"user_attributes\": [\"given_name\",\"family_name\",\"email\",\"groups\", \"name\", \"ouId\"] + } + }, + \"scope_claims\": { + \"profile\": [\"name\",\"given_name\",\"family_name\",\"picture\"], + \"email\": [\"email\",\"email_verified\"], + \"phone\": [\"phone_number\",\"phone_number_verified\"], + \"group\": [\"groups\"], + \"ou\": [\"ouId\"] + } + } + }] +}") + +HTTP_CODE="${RESPONSE: -3}" +BODY="${RESPONSE%???}" + +if [[ "$HTTP_CODE" == "201" ]] || [[ "$HTTP_CODE" == "200" ]]; then + log_success "DEVELOP application created successfully" + DEVELOP_APP_ID=$(echo "$BODY" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) + if [[ -n "$DEVELOP_APP_ID" ]]; then + log_info "DEVELOP_APP_ID: $DEVELOP_APP_ID" + else + log_warning "Could not extract DEVELOP application ID from response" + fi +elif [[ "$HTTP_CODE" == "409" ]]; then + log_warning "DEVELOP application already exists, retrieving application ID..." + # Get existing DEVELOP application ID + RESPONSE=$(thunder_api_call GET "/applications") + HTTP_CODE="${RESPONSE: -3}" + BODY="${RESPONSE%???}" + + if [[ "$HTTP_CODE" == "200" ]]; then + # Parse JSON to find DEVELOP application by client_id + DEVELOP_APP_ID=$(echo "$BODY" | sed 's/},{/}\n{/g' | grep '"client_id":"DEVELOP"' | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) + + if [[ -n "$DEVELOP_APP_ID" ]]; then + log_success "Found DEVELOP application ID: $DEVELOP_APP_ID" + log_info "DEVELOP_APP_ID: $DEVELOP_APP_ID" + else + log_error "Could not find DEVELOP application in response" + fi + else + log_error "Failed to fetch applications (HTTP $HTTP_CODE)" + fi +elif [[ "$HTTP_CODE" == "400" ]] && [[ "$BODY" =~ (Application already exists|APP-1022) ]]; then + log_warning "DEVELOP application already exists, retrieving application ID..." + # Get existing DEVELOP application ID + RESPONSE=$(thunder_api_call GET "/applications") + HTTP_CODE="${RESPONSE: -3}" + BODY="${RESPONSE%???}" + + if [[ "$HTTP_CODE" == "200" ]]; then + # Parse JSON to find DEVELOP application by client_id + DEVELOP_APP_ID=$(echo "$BODY" | sed 's/},{/}\n{/g' | grep '"client_id":"DEVELOP"' | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) + + if [[ -n "$DEVELOP_APP_ID" ]]; then + log_success "Found DEVELOP application ID: $DEVELOP_APP_ID" + log_info "DEVELOP_APP_ID: $DEVELOP_APP_ID" + else + log_error "Could not find DEVELOP application in response" + fi + else + log_error "Failed to fetch applications (HTTP $HTTP_CODE)" + fi +else + log_error "Failed to create DEVELOP application (HTTP $HTTP_CODE)" + echo "Response: $BODY" + exit 1 +fi + +echo "" + +# ============================================================================ +# Create Themes +# ============================================================================ + +log_info "Creating themes..." + +# Get the script directory to locate theme files +THEMES_DIR="${SCRIPT_DIR}/themes" + +# Check if themes directory exists +if [[ ! -d "$THEMES_DIR" ]]; then + log_warning "Themes directory not found at ${THEMES_DIR}, skipping theme creation" +else + shopt -s nullglob + THEME_FILES=("$THEMES_DIR"/*.json) + shopt -u nullglob + + if [[ ${#THEME_FILES[@]} -gt 0 ]]; then + log_info "Processing themes from ${THEMES_DIR}..." + + THEME_COUNT=0 + THEME_SUCCESS=0 + THEME_SKIPPED=0 + + for THEME_FILE in "${THEME_FILES[@]}"; do + [[ ! -f "$THEME_FILE" ]] && continue + + THEME_COUNT=$((THEME_COUNT + 1)) + + # Get theme name from file content or use filename + THEME_NAME=$(grep -o '"displayName"[[:space:]]*:[[:space:]]*"[^"]*"' "$THEME_FILE" | head -1 | sed 's/"displayName"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/') + if [[ -z "$THEME_NAME" ]]; then + THEME_NAME=$(basename "$THEME_FILE" .json) + fi + + log_info "Creating theme: ${THEME_NAME} (from $(basename "$THEME_FILE"))" + THEME_PAYLOAD=$(cat "$THEME_FILE") + + RESPONSE=$(thunder_api_call POST "/design/themes" "${THEME_PAYLOAD}") + HTTP_CODE="${RESPONSE: -3}" + BODY="${RESPONSE%???}" + + if [[ "$HTTP_CODE" == "201" ]] || [[ "$HTTP_CODE" == "200" ]]; then + log_success "Theme '${THEME_NAME}' created successfully" + THEME_ID=$(echo "$BODY" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) + if [[ -n "$THEME_ID" ]]; then + log_info "Theme ID: $THEME_ID" + fi + THEME_SUCCESS=$((THEME_SUCCESS + 1)) + elif [[ "$HTTP_CODE" == "409" ]]; then + log_warning "Theme '${THEME_NAME}' already exists, skipping" + THEME_SKIPPED=$((THEME_SKIPPED + 1)) + else + log_error "Failed to create theme '${THEME_NAME}' (HTTP $HTTP_CODE)" + echo "Response: $BODY" + exit 1 + fi + done + + echo "" + log_info "Theme creation summary: ${THEME_SUCCESS} created, ${THEME_SKIPPED} skipped (Total: ${THEME_COUNT})" + else + log_warning "No theme files found in ${THEMES_DIR}" + fi +fi + +echo "" + +# ============================================================================ +# Seed i18n Translations +# ============================================================================ + +log_info "Seeding i18n translations..." + +I18N_DIR="${SCRIPT_DIR}/i18n" + +if [[ ! -d "$I18N_DIR" ]]; then + log_warning "i18n directory not found at ${I18N_DIR}, skipping translation seeding" +else + shopt -s nullglob + I18N_FILES=("$I18N_DIR"/*.json) + shopt -u nullglob + + if [[ ${#I18N_FILES[@]} -gt 0 ]]; then + log_info "Processing i18n translations from ${I18N_DIR}..." + + I18N_COUNT=0 + I18N_SUCCESS=0 + + for I18N_FILE in "${I18N_FILES[@]}"; do + [[ ! -f "$I18N_FILE" ]] && continue + + I18N_COUNT=$((I18N_COUNT + 1)) + + # Extract language from filename (e.g., en-US.json -> en-US) + LANGUAGE=$(basename "$I18N_FILE" .json) + + log_info "Seeding translations for language: ${LANGUAGE} (from $(basename "$I18N_FILE"))" + + PAYLOAD=$(cat "$I18N_FILE") + + RESPONSE=$(thunder_api_call POST "/i18n/languages/${LANGUAGE}/translations" "$PAYLOAD") + HTTP_CODE="${RESPONSE: -3}" + BODY="${RESPONSE%???}" + + if [[ "$HTTP_CODE" == "200" ]]; then + TOTAL=$(echo "$BODY" | grep -o '"totalResults":[0-9]*' | cut -d':' -f2) + log_success "Translations for '${LANGUAGE}' seeded successfully (${TOTAL:-?} translations)" + I18N_SUCCESS=$((I18N_SUCCESS + 1)) + else + log_error "Failed to seed translations for '${LANGUAGE}' (HTTP $HTTP_CODE)" + log_error "Response: $BODY" + exit 1 + fi + done + + echo "" + log_info "Translation seeding summary: ${I18N_SUCCESS} seeded (Total: ${I18N_COUNT})" + else + log_warning "No i18n translation files found in ${I18N_DIR}" + fi +fi + +echo "" + +# ============================================================================ +# Summary +# ============================================================================ + +log_success "Default resources setup completed successfully!" +echo "" +log_info "👤 Admin credentials:" +log_info " Username: ${ADMIN_USERNAME}" +log_info " Role: Administrator (system permission)" +echo "" \ No newline at end of file diff --git a/scripts/thunder/02-sample-resources.sh b/scripts/thunder/02-sample-resources.sh new file mode 100755 index 00000000..e8c55aef --- /dev/null +++ b/scripts/thunder/02-sample-resources.sh @@ -0,0 +1 @@ +echo "Overwriting the default sample resources creation script with empty content..." \ No newline at end of file diff --git a/scripts/utils/thunder-auth.sh b/scripts/utils/thunder-auth.sh index 93e874d5..08108306 100755 --- a/scripts/utils/thunder-auth.sh +++ b/scripts/utils/thunder-auth.sh @@ -10,7 +10,7 @@ # Usage: # source "$(dirname "$0")/../utils/thunder-auth.sh" # thunder_authenticate "$THUNDER_HOST" "$THUNDER_PORT" -# # Now you can use: $SAMPLE_APP_ID, $BEARER_TOKEN +# # Now you can use: $DEVELOP_APP_ID, $BEARER_TOKEN # # thunder_get_org_unit "$THUNDER_HOST" "$THUNDER_PORT" "$BEARER_TOKEN" "silver" # # Now you can use: $ORG_UNIT_ID @@ -24,19 +24,20 @@ RED="\033[0;31m" NC="\033[0m" # No Color # ============================================ -# Function: Extract Sample App ID from Thunder setup logs +# Function: Extract DEVELOP App ID from Thunder setup logs # ============================================ -thunder_get_sample_app_id() { - local sample_app_id - sample_app_id=$(docker logs thunder-setup 2>&1 | grep 'Sample App ID:' | head -n1 | grep -o '[a-f0-9-]\{36\}') +thunder_get_develop_app_id() { + local develop_app_id + # Look for DEVELOP_APP_ID in logs (handles both "DEVELOP_APP_ID:" and "[INFO] DEVELOP_APP_ID:" formats) + develop_app_id=$(docker logs thunder-setup 2>&1 | grep -i 'DEVELOP_APP_ID' | head -n1 | grep -o '[a-f0-9-]\{36\}') - if [ -z "$sample_app_id" ]; then - echo -e "${RED}✗ Failed to extract Sample App ID from Thunder setup logs${NC}" >&2 + if [ -z "$develop_app_id" ]; then + echo -e "${RED}✗ Failed to extract DEVELOP_APP_ID from Thunder setup logs${NC}" >&2 echo "Please ensure Thunder setup container has completed successfully." >&2 return 1 fi - echo "$sample_app_id" + echo "$develop_app_id" return 0 } @@ -49,7 +50,7 @@ thunder_get_sample_app_id() { # Returns: # 0 on success, 1 on failure # Exports: -# SAMPLE_APP_ID - The application ID extracted from logs +# DEVELOP_APP_ID - The application ID extracted from logs # BEARER_TOKEN - The authentication token # ============================================ thunder_authenticate() { @@ -63,16 +64,16 @@ thunder_authenticate() { echo -e "${YELLOW}Authenticating with Thunder...${NC}" - # Step 1: Extract Sample App ID - echo " - Extracting Sample App ID from Thunder setup logs..." - SAMPLE_APP_ID=$(thunder_get_sample_app_id) + # Step 1: Extract DEVELOP App ID + echo " - Extracting DEVELOP_APP_ID from Thunder setup logs..." + DEVELOP_APP_ID=$(thunder_get_develop_app_id) - if [ $? -ne 0 ] || [ -z "$SAMPLE_APP_ID" ]; then - echo -e "${RED}✗ Failed to extract Sample App ID${NC}" >&2 + if [ $? -ne 0 ] || [ -z "$DEVELOP_APP_ID" ]; then + echo -e "${RED}✗ Failed to extract DEVELOP_APP_ID${NC}" >&2 return 1 fi - echo -e "${GREEN} ✓ Sample App ID extracted: $SAMPLE_APP_ID${NC}" + echo -e "${GREEN} ✓ DEVELOP_APP_ID extracted: $DEVELOP_APP_ID${NC}" # Step 2: Start authentication flow (get flowId) echo " - Starting authentication flow..." @@ -80,7 +81,7 @@ thunder_authenticate() { start_response=$(curl -s -w "\n%{http_code}" -X POST \ "https://${thunder_host}:${thunder_port}/flow/execute" \ -H "Content-Type: application/json" \ - -d "{\"applicationId\":\"${SAMPLE_APP_ID}\",\"flowType\":\"AUTHENTICATION\"}") + -d "{\"applicationId\":\"${DEVELOP_APP_ID}\",\"flowType\":\"AUTHENTICATION\"}") local start_body local start_status @@ -132,7 +133,7 @@ thunder_authenticate() { echo -e "${GREEN} ✓ Authentication successful${NC}" # Export variables for use in calling script - export SAMPLE_APP_ID + export DEVELOP_APP_ID export FLOW_ID export BEARER_TOKEN diff --git a/services/.env.example b/services/.env.example index e92af3d6..d1d06989 100644 --- a/services/.env.example +++ b/services/.env.example @@ -1,6 +1,10 @@ # Rspamd web UI password RSPAMD_PASSWORD=your_secure_password +# Thunder admin credentials (used during initial setup) +THUNDER_ADMIN_USERNAME=admin +THUNDER_ADMIN_PASSWORD=admin + # Public IP of this mail server MAIL_SERVER_IP= diff --git a/services/docker-compose.yaml b/services/docker-compose.yaml index 0702ec6f..6311b4c1 100644 --- a/services/docker-compose.yaml +++ b/services/docker-compose.yaml @@ -66,11 +66,16 @@ services: environment: - SOCKETMAP_HOST=0.0.0.0 - SOCKETMAP_PORT=9100 - # THUNDER_SAMPLE_APP_ID not required - auto-extracted from thunder-setup logs + # THUNDER_DEVELOP_APP_ID not required - auto-extracted from thunder-setup logs - THUNDER_HOST=thunder-server - THUNDER_PORT=8090 - CACHE_TTL_SECONDS=300 - TOKEN_REFRESH_SECONDS=3300 + depends_on: + thunder-setup: + condition: service_completed_successfully + thunder: + condition: service_started healthcheck: test: ["CMD", "nc", "-z", "localhost", "9100"] interval: 10s @@ -175,7 +180,7 @@ services: # Initialize database from the image thunder-db-init: - image: ghcr.io/asgardeo/thunder:0.22.0 + image: ghcr.io/asgardeo/thunder:0.24.0 container_name: thunder-db-init command: sh -c "cp -r /opt/thunder/repository/database/* /data/" volumes: @@ -184,12 +189,17 @@ services: # Run setup once with the shared database thunder-setup: - image: ghcr.io/asgardeo/thunder:0.22.0 + image: ghcr.io/asgardeo/thunder:0.24.0 container_name: thunder-setup command: ./setup.sh volumes: - thunder-db:/opt/thunder/repository/database - ./silver-config/thunder/deployment.yaml:/opt/thunder/repository/conf/deployment.yaml:ro + - ./../scripts/thunder/01-default-resources.sh:/opt/thunder/bootstrap/01-default-resources.sh + - ./../scripts/thunder/02-sample-resources.sh:/opt/thunder/bootstrap/02-sample-resources.sh + environment: + - THUNDER_ADMIN_USERNAME=${THUNDER_ADMIN_USERNAME:-admin} + - THUNDER_ADMIN_PASSWORD=${THUNDER_ADMIN_PASSWORD:-admin} depends_on: thunder-db-init: condition: service_completed_successfully @@ -197,7 +207,7 @@ services: # Run Thunder server with the shared database thunder: - image: ghcr.io/asgardeo/thunder:0.22.0 + image: ghcr.io/asgardeo/thunder:0.24.0 container_name: thunder-server depends_on: thunder-setup: diff --git a/services/socketmap/internal/thunder/auth.go b/services/socketmap/internal/thunder/auth.go index 85a68e09..544bc285 100644 --- a/services/socketmap/internal/thunder/auth.go +++ b/services/socketmap/internal/thunder/auth.go @@ -18,11 +18,11 @@ var ( thunderAuthMutex sync.RWMutex ) -// getSampleAppIDFromThunderSetup extracts Sample App ID from thunder-setup container -func getSampleAppIDFromThunderSetup() (string, error) { - log.Printf(" │ Extracting Sample App ID from thunder-setup container...") +// getDevelopAppIDFromThunderSetup extracts DEVELOP App ID from thunder-setup container +func getDevelopAppIDFromThunderSetup() (string, error) { + log.Printf(" │ Extracting DEVELOP_APP_ID from thunder-setup container...") - // Execute: docker logs thunder-setup 2>&1 | grep 'Sample App ID:' | head -n1 + // Execute: docker logs thunder-setup 2>&1 | grep 'DEVELOP_APP_ID:' | head -n1 cmd := exec.Command("docker", "logs", "thunder-setup") output, err := cmd.CombinedOutput() if err != nil { @@ -38,52 +38,54 @@ func getSampleAppIDFromThunderSetup() (string, error) { log.Printf(" │ - Running inside a container without Docker socket") } - // Search for "Sample App ID:" in logs (case-insensitive) + // Search for "DEVELOP_APP_ID:" in logs + // Log format: [INFO] DEVELOP_APP_ID: 019cdc47-3537-74ee-951e-3f50e48786ab lines := strings.Split(string(output), "\n") for _, line := range lines { - if strings.Contains(line, "Sample App ID:") { + // Look for line containing DEVELOP_APP_ID (case-insensitive) + if strings.Contains(line, "DEVELOP_APP_ID") || strings.Contains(line, "develop_app_id") { // Extract UUID pattern: [a-f0-9-]{36} re := regexp.MustCompile(`[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}`) match := re.FindString(line) if match != "" { - log.Printf(" │ ✓ Sample App ID extracted: %s", match) + log.Printf(" │ ✓ DEVELOP_APP_ID extracted: %s", match) return match, nil } } } - return "", fmt.Errorf("Sample App ID not found in thunder-setup logs") + return "", fmt.Errorf("DEVELOP_APP_ID not found in thunder-setup logs") } // Authenticate performs the full authentication flow with Thunder IDP func Authenticate(host, port string, tokenRefreshSeconds int) (*Auth, error) { log.Printf(" ┌─ Thunder Authentication ─────────") - // Step 1: Get Sample App ID - sampleAppID := os.Getenv("THUNDER_SAMPLE_APP_ID") + // Step 1: Get DEVELOP App ID + developAppID := os.Getenv("THUNDER_DEVELOP_APP_ID") - if sampleAppID != "" { - log.Printf(" │ Using Sample App ID from environment variable") - log.Printf(" │ Sample App ID: %s", sampleAppID) + if developAppID != "" { + log.Printf(" │ Using DEVELOP App ID from environment variable") + log.Printf(" │ DEVELOP_APP_ID: %s", developAppID) } else { - log.Printf(" │ THUNDER_SAMPLE_APP_ID not set") + log.Printf(" │ THUNDER_DEVELOP_APP_ID not set") log.Printf(" │ Attempting to extract from thunder-setup container logs...") var err error - sampleAppID, err = getSampleAppIDFromThunderSetup() + developAppID, err = getDevelopAppIDFromThunderSetup() if err != nil { - log.Printf(" │ ✗ Failed to extract Sample App ID: %v", err) + log.Printf(" │ ✗ Failed to extract DEVELOP_APP_ID: %v", err) log.Printf(" │") log.Printf(" │ Please ensure Thunder setup container has completed successfully") log.Printf(" │") log.Printf(" │ To fix this issue:") log.Printf(" │ 1. Check thunder-setup logs: docker logs thunder-setup") log.Printf(" │ 2. Extract App ID manually and set environment:") - log.Printf(" │ export THUNDER_SAMPLE_APP_ID=$(docker logs thunder-setup 2>&1 | grep 'Sample App ID:' | grep -o '[a-f0-9-]\\{36\\}')") + log.Printf(" │ export THUNDER_DEVELOP_APP_ID=$(docker logs thunder-setup 2>&1 | grep 'DEVELOP_APP_ID:' | grep -o '[a-f0-9-]\\{36\\}')") log.Printf(" │ 3. Or if running in Docker, mount the Docker socket:") log.Printf(" │ volumes: ['/var/run/docker.sock:/var/run/docker.sock']") log.Printf(" └───────────────────────────────────") - return nil, fmt.Errorf("failed to get Sample App ID: %w", err) + return nil, fmt.Errorf("failed to get DEVELOP App ID: %w", err) } } @@ -93,7 +95,7 @@ func Authenticate(host, port string, tokenRefreshSeconds int) (*Auth, error) { // Step 2: Start authentication flow log.Printf(" │ Starting authentication flow...") flowPayload := map[string]interface{}{ - "applicationId": sampleAppID, + "applicationId": developAppID, "flowType": "AUTHENTICATION", } flowData, err := json.Marshal(flowPayload) @@ -164,7 +166,7 @@ func Authenticate(host, port string, tokenRefreshSeconds int) (*Auth, error) { log.Printf(" └───────────────────────────────────") auth := &Auth{ - SampleAppID: sampleAppID, + DevelopAppID: developAppID, FlowID: flowResp.FlowID, BearerToken: authResp.Assertion, ExpiresAt: time.Now().Add(time.Duration(tokenRefreshSeconds) * time.Second), diff --git a/services/socketmap/internal/thunder/types.go b/services/socketmap/internal/thunder/types.go index d8f3990b..e27e6a51 100644 --- a/services/socketmap/internal/thunder/types.go +++ b/services/socketmap/internal/thunder/types.go @@ -6,7 +6,7 @@ import ( // Auth holds Thunder authentication state type Auth struct { - SampleAppID string + DevelopAppID string FlowID string BearerToken string ExpiresAt time.Time diff --git a/services/socketmap/main.go b/services/socketmap/main.go index e2e2b22f..7f47fefe 100644 --- a/services/socketmap/main.go +++ b/services/socketmap/main.go @@ -20,16 +20,37 @@ func main() { // Load configuration cfg := config.Load() - // Authenticate with Thunder at startup + // Authenticate with Thunder at startup with retry logic log.Println("┌─ Thunder Authentication ─────────") - auth, err := thunder.Authenticate(cfg.ThunderHost, cfg.ThunderPort, cfg.TokenRefreshSeconds) + + var auth *thunder.Auth + var err error + maxRetries := 5 + retryDelay := 2 * time.Second + + for attempt := 1; attempt <= maxRetries; attempt++ { + if attempt > 1 { + log.Printf("│ Retry attempt %d/%d (waiting %v)...", attempt, maxRetries, retryDelay) + time.Sleep(retryDelay) + retryDelay *= 2 // Exponential backoff + } + + auth, err = thunder.Authenticate(cfg.ThunderHost, cfg.ThunderPort, cfg.TokenRefreshSeconds) + if err == nil { + thunder.SetAuth(auth) + log.Println("└───────────────────────────────────") + break + } + + if attempt < maxRetries { + log.Printf("│ ⚠ Authentication attempt %d failed: %v", attempt, err) + } + } + if err != nil { - log.Printf("│ ⚠ Initial authentication failed: %v", err) + log.Printf("│ ⚠ Initial authentication failed after %d attempts: %v", maxRetries, err) log.Printf("│ Service will attempt to authenticate on first request") log.Println("└───────────────────────────────────") - } else { - thunder.SetAuth(auth) - log.Println("└───────────────────────────────────") } log.Println("")