284 task create bootstrap scripts for thunder#292
Conversation
…P in scripts and authentication flow
…sitive formats in logs
|
/gemini review |
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly refines the initial setup process for a Thunder instance, making it more robust and configurable. The changes streamline the bootstrapping of essential resources like organization units, user schemas, and an admin user, while also integrating application-specific flows and themes. By externalizing admin credentials and updating the core Thunder version, the system becomes easier to deploy and maintain, ensuring a smoother initial experience. Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
The pull request introduces a comprehensive 01-default-resources.sh script to bootstrap default Thunder resources, including organization units, user schemas, an admin user, system resource servers with hierarchical permissions, admin roles, a "Develop" application, themes, and i18n translations. This replaces the previous sample resources script. Related changes include updating the thunder-auth.sh utility and the socketmap Go application to reference DEVELOP_APP_ID instead of SAMPLE_APP_ID, enhancing the log parsing for the application ID, and adding retry logic with exponential backoff for initial authentication in the socketmap service. The docker-compose.yaml file is updated to use Thunder image 0.24.0, mount the new bootstrap scripts, and pass admin credentials. Review comments suggest improving the robustness of JSON parsing for ADMIN_USER_ID and SYSTEM_RS_ID in 01-default-resources.sh by directly using more reliable parsing methods. Additionally, significant code duplication in the resource creation and flow processing sections of the bootstrap script is highlighted, with recommendations to refactor these into reusable shell functions for better maintainability.
| 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 |
There was a problem hiding this comment.
The primary method for parsing the admin user ID on line 241 is very fragile as it depends on the specific order of JSON fields. A minor change in the API response could break it. The fallback logic on line 245 is more robust as it only relies on finding the username field within a JSON object. It would be better to use the more robust method directly.
| 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 | |
| ADMIN_USER_ID=$(echo "$BODY" | sed 's/},{/}\n{/g' | grep '"username":"'"${ADMIN_USERNAME}"'"' | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) |
| 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 |
There was a problem hiding this comment.
This grep command is fragile as it assumes the name field with value System appears right after the id field. It's better to rely on the unique identifier field, as done in the fallback logic on line 308. I suggest using the more robust method directly.
| 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 | |
| SYSTEM_RS_ID=$(echo "$BODY" | sed 's/},{/}\n{/g' | grep '"identifier":"system"' | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) |
| 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 |
There was a problem hiding this comment.
There is significant code duplication in the creation of various resources (ou, user, userschema, group). This block of code for creating the ou resource is repeated with minor changes for other resources between lines 395 and 659. This logic could be extracted into a reusable shell function to improve maintainability and reduce code size. A similar pattern of duplication exists for creating the view action for each resource.
| # 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 |
There was a problem hiding this comment.
The logic for processing authentication, registration, and user onboarding flows is nearly identical. This block for authentication flows is repeated for registration and user onboarding flows between lines 740 and 919. You could create a function that takes the flow type (e.g., AUTHENTICATION) and the source directory as parameters to avoid this duplication. This would make the script much shorter and easier to maintain.
There was a problem hiding this comment.
Code Review
This pull request introduces bootstrap scripts for Thunder, updates configurations, and refactors related code to use the new DEVELOP application instead of a sample application. A critical aspect to address is the identification of two medium-severity vulnerabilities in scripts/thunder/01-default-resources.sh: a JSON injection vulnerability due to improper handling of custom redirect URIs and the logging of sensitive admin credentials. Beyond security, the review also focuses on improving the maintainability and robustness of the new bash scripts and ensuring consistency across the codebase, including the acceptable use of grep and sed for JSON parsing when encapsulated for maintainability.
| REDIRECT_URIS="${REDIRECT_URIS},\"${uri}\"" | ||
| done | ||
| fi | ||
|
|
||
| RESPONSE=$(thunder_api_call POST "/applications" "{ |
There was a problem hiding this comment.
The script constructs a JSON payload by concatenating the CUSTOM_DEVELOP_REDIRECT_URIS input without proper escaping or validation. An attacker providing a specially crafted redirect URI containing double quotes could inject arbitrary fields into the JSON payload sent to the /applications API, leading to a JSON injection vulnerability. Additionally, constructing large multi-line JSON payloads as a single string with escaped quotes and newlines is hard to read and maintain. Consider using a heredoc for better readability and maintainability, but ensure REDIRECT_URIS is properly escaped before inclusion to prevent the injection.
| if [[ "$HTTP_CODE" == "201" ]] || [[ "$HTTP_CODE" == "200" ]]; then | ||
| log_success "Admin user created successfully" | ||
| log_info "Username: ${ADMIN_USERNAME}" | ||
| log_info "Password: ${ADMIN_PASSWORD}" |
| 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 |
There was a problem hiding this comment.
This complex and repeated logic for parsing JSON to find the admin user ID is brittle and hard to maintain. A fallback is provided, but the whole block can be simplified by using a helper function.
I suggest adding the following helper function near the top of your script (e.g., after source "${SCRIPT_DIR}/common.sh"):
# Helper function to extract a value from a JSON array based on a filter.
# It splits the JSON array into lines, finds the line with the filter,
# and extracts the value of the target key.
#
# Arguments:
# $1: JSON body (string)
# $2: Filter pattern to find the correct JSON object (e.g., '"username":"admin"')
# $3: The key of the value to extract (e.g., "id")
extract_json_value() {
local body="$1"
local filter_pattern="$2"
local target_key="$3"
echo "$body" | sed 's/},{/}\n{/g' | grep "$filter_pattern" | grep -o "\"$target_key\":\"[^\"]*\"" | head -1 | cut -d'\"' -f4
}Then you can replace these lines with a single, more readable line:
ADMIN_USER_ID=$(extract_json_value "$BODY" "\"username\":\"${ADMIN_USERNAME}\"" "id")This pattern can be reused throughout the script (e.g., for finding the system resource server ID on lines 304-309) to significantly improve readability and maintainability.
References
- Using
grepandsedfor simple JSON parsing is acceptable to avoid introducing new dependencies likejq.
| # 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 |
There was a problem hiding this comment.
There is significant code duplication in the logic for processing authentication, registration, and user onboarding flows. This makes the script harder to read and maintain. This entire block of logic can be refactored into a single, reusable function.
Consider adding a function like process_flows_by_type to your script:
process_flows_by_type() {
local flow_type="$1"
local flow_dir="$2"
if [[ ! -d "$flow_dir" ]]; then
log_warning "Flow definition directory for '$flow_type' not found, skipping"
return
fi
shopt -s nullglob
local flow_files=("$flow_dir"/*.json)
shopt -u nullglob
if [[ ${#flow_files[@]} -eq 0 ]]; then
log_warning "No flow files found in ${flow_dir}"
return
fi
log_info "Processing ${flow_type,,} flows..."
# Fetch existing flows
local response
response=$(thunder_api_call GET "/flows?flowType=${flow_type}&limit=200")
# ... (rest of the logic from the original blocks) ...
}You can then replace the duplicated blocks (lines 740-801, 803-859, and 862-918) with simple calls to this function:
process_flows_by_type "AUTHENTICATION" "$AUTH_FLOWS_DIR"
process_flows_by_type "REGISTRATION" "$REG_FLOWS_DIR"
process_flows_by_type "USER_ONBOARDING" "$USER_ONBOARDING_FLOWS_DIR"This refactoring will make the script much shorter, cleaner, and easier to maintain.
| 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\}') |
There was a problem hiding this comment.
The regex used to extract the DEVELOP_APP_ID is a bit loose ([a-f0-9-]\{36\}). For better robustness and consistency with the Go code in this PR, I recommend using a more specific UUID regex.
| develop_app_id=$(docker logs thunder-setup 2>&1 | grep -i 'DEVELOP_APP_ID' | head -n1 | grep -o '[a-f0-9-]\{36\}') | |
| develop_app_id=$(docker logs thunder-setup 2>&1 | grep -i 'DEVELOP_APP_ID' | head -n1 | grep -o '[a-f0-9]\{8\}-[a-f0-9]\{4\}-[a-f0-9]\{4\}-[a-f0-9]\{4\}-[a-f0-9]\{12\}') |
| 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\\}')") |
There was a problem hiding this comment.
The example command in the error log for manually extracting the DEVELOP_APP_ID can be improved. Using grep -i makes it case-insensitive, and a more specific regex for the UUID makes it more robust. This aligns it with the logic in the Go code and the accompanying shell scripts.
| 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(" │ export THUNDER_DEVELOP_APP_ID=$(docker logs thunder-setup 2>&1 | grep -i 'DEVELOP_APP_ID' | grep -o '[a-f0-9]\\{8\\}-[a-f0-9]\\{4\\}-[a-f0-9]\\{4\\}-[a-f0-9]\\{4\\}-[a-f0-9]\\{12\\}')") |
There was a problem hiding this comment.
Code Review
The pull request introduces a comprehensive bootstrap script (01-default-resources.sh) to set up default Thunder resources, including organization units, user schemas, an admin user, system resource servers, permissions, roles, authentication/registration flows, a "Develop" application, themes, and i18n translations. Correspondingly, 02-sample-resources.sh is made empty, and references to SAMPLE_APP_ID are updated to DEVELOP_APP_ID across utility scripts and the socketmap service, which also gains retry logic for initial authentication. Docker Compose configurations are updated to use Thunder image 0.24.0 and integrate the new bootstrap scripts and admin credentials. Review comments highlight a high-severity JSON injection vulnerability in 01-default-resources.sh due to direct concatenation of user-provided redirect URIs into a JSON payload. Additionally, there are code duplication issues in 01-default-resources.sh related to processing different flow types and handling HTTP 400/409 errors during application creation, both of which could be refactored for improved maintainability.
| 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}], |
There was a problem hiding this comment.
The script takes user input from the command line argument --develop-redirect-uris and directly concatenates it into a JSON payload sent to the Thunder API. An attacker can provide a specially crafted string to inject arbitrary fields into the JSON object, potentially leading to unauthorized configuration changes. For example, providing a value like http://example.com", "is_registration_flow_enabled": true would inject the is_registration_flow_enabled field into the JSON payload.
| # 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 |
There was a problem hiding this comment.
There is significant code duplication in the sections for processing authentication (lines 740-801), registration (803-860), and user onboarding (862-919) flows. The logic within each block is nearly identical. This makes the script harder to read and maintain, as any change to the flow processing logic would need to be manually applied in three separate places.
To improve maintainability, this repeated logic should be extracted into a reusable function. This function could take parameters like the flow type and directory path.
| 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 |
There was a problem hiding this comment.
The logic inside this elif block for handling a 400 error is identical to the logic in the elif block for the 409 error (lines 1147-1166). This duplicates the code for retrieving an existing application ID.
These two conditions can be combined to avoid repetition. You can modify the condition on line 1147 and remove this duplicated block:
elif [[ "$HTTP_CODE" == "409" ]] || ([[ "$HTTP_CODE" == "400" ]] && [[ "$BODY" =~ (Application already exists|APP-1022) ]]); then
# ... single implementation of the logic* feat(thunder): update Thunder image version to 0.24.0 and add sample resources script * Refactor code structure for improved readability and maintainability * feat(thunder): add default and sample resources scripts to Thunder service * chore: update file permissions for default and sample resources scripts * feat(thunder): update application ID references from Sample to DEVELOP in scripts and authentication flow * feat(thunder): enhance DEVELOP_APP_ID extraction to handle case-insensitive formats in logs * feat(socketmap): implement retry logic for Thunder authentication on startup
📌 Description
Change the bootstrap scripts of the Thunder for the initial setup of the Thunder instance.
🔍 Changes Made
✅ Checklist (Email System)
🧪 Testing Instructions
📷 Screenshots / Logs (if applicable)