From f9767a755b5d6f0ae3407da30a458693e7e88f5f Mon Sep 17 00:00:00 2001 From: Frank Bernhardt Date: Mon, 1 Dec 2025 09:06:35 +0100 Subject: [PATCH 1/5] feat: sync updates from development repo - Add PowerShell version (awsssoprofiletool.ps1) - Add --default flag to set default AWS profile - Add --map parameter for configurable account name mappings - Add -y flag for non-interactive mode - Fix region and starturl variable storage - Fix process substitution for curl execution - Fix /dev/tty read for SSO login wait - Update copyright to Frank Bernhardt - Remove company-specific references - Update documentation --- LICENSE | 2 +- README.md | 32 +++- awsssoprofiletool.ps1 | 413 ++++++++++++++++++++++++++++++++++++++++++ awsssoprofiletool.sh | 249 +++++++++++++++++++------ 4 files changed, 638 insertions(+), 58 deletions(-) create mode 100644 awsssoprofiletool.ps1 diff --git a/LICENSE b/LICENSE index 1bb4f21..8d34180 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +Copyright 2025 Amazon.com, Inc. or its affiliates. and Frank Bernhardt. All Rights Reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/README.md b/README.md index 0899588..1750076 100644 --- a/README.md +++ b/README.md @@ -39,17 +39,39 @@ the following methods: To run the script, do one of the following: * If the script is executable, run it with `./awsssoprofiletool.sh - []` +[-y] [--map "FROM:TO" ...] [--default ] []` * If the script is not executable, run it with `bash awsssoprofiletool.sh - []` +[-y] [--map "FROM:TO" ...] [--default ] []` The arguments are as follows: -* <region> - the region where AWS SSO is running -* <start_url> - the start URL from the AWS SSO page -* <profile_file> - where the profiles will be created; defaults to +* `-y` - (optional) non-interactive mode; overwrites config and creates all profiles without prompts +* `--map "FROM:TO"` - (optional) map account name FROM to TO in profile names; can be specified multiple times +* `--default ` - (optional) create a `[default]` profile that mirrors the specified profile name +* `` - the region where AWS SSO is running +* `` - the start URL from the AWS SSO page +* `` - where the profiles will be created; defaults to ~/.aws/config +**Example with account name mappings:** +```bash +./awsssoprofiletool.sh -y \ + --map "Infrastructure:Infra" \ + --map "Development:Dev" \ + us-east-1 https://example.awsapps.com/start +``` + +This would create profiles like `InfraAdministratorAccess` instead of `InfrastructureAdministratorAccess`. + +**Example with default profile:** +```bash +./awsssoprofiletool.sh -y \ + --default "DevAdministratorAccess" \ + us-east-1 https://example.awsapps.com/start +``` + +This creates all profiles plus a `[default]` section that mirrors `DevAdministratorAccess`, so AWS CLI commands work without specifying `--profile`. + ## Security See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. diff --git a/awsssoprofiletool.ps1 b/awsssoprofiletool.ps1 new file mode 100644 index 0000000..1cb1936 --- /dev/null +++ b/awsssoprofiletool.ps1 @@ -0,0 +1,413 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + AWS Profile Generator + +.DESCRIPTION + Generates AWS SSO profiles for all accounts and roles available to the user. + +.PARAMETER Region + The region where AWS SSO is configured (e.g., us-east-1) + +.PARAMETER StartUrl + The AWS SSO start URL + +.PARAMETER ProfileFile + The file where the profiles will be written (default is ~/.aws/config) + +.PARAMETER NoPrompt + Run in non-interactive mode. Overwrites the config file and creates all profiles without prompts. + +.PARAMETER Map + Map account names to shorter/different names in profile names. Format: "FROM:TO". + Can be specified multiple times. Example: -Map "Infrastructure:Infra" -Map "Development:Dev" + +.PARAMETER Default + Create a [default] profile that mirrors the specified profile name. + Example: -Default "DevAdministratorAccess" + +.EXAMPLE + .\awsssoprofiletool.ps1 -Region us-east-1 -StartUrl "https://example.awsapps.com/start" + +.EXAMPLE + .\awsssoprofiletool.ps1 -Region us-east-1 -StartUrl "https://example.awsapps.com/start" -NoPrompt + +.EXAMPLE + .\awsssoprofiletool.ps1 -Region us-east-1 -StartUrl "https://example.awsapps.com/start" -Map "Infrastructure:Infra" -Map "Development:Dev" + +.EXAMPLE + .\awsssoprofiletool.ps1 -Region us-east-1 -StartUrl "https://example.awsapps.com/start" -Default "DevAdministratorAccess" + +.NOTES + Copyright 2025 Amazon.com, Inc. or its affiliates. and Frank Bernhardt. All Rights Reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $true, Position = 0)] + [string]$Region, + + [Parameter(Mandatory = $true, Position = 1)] + [string]$StartUrl, + + [Parameter(Mandatory = $false, Position = 2)] + [string]$ProfileFile = (Join-Path $env:USERPROFILE ".aws\config"), + + [Parameter(Mandatory = $false)] + [switch]$NoPrompt, + + [Parameter(Mandatory = $false)] + [string[]]$Map, + + [Parameter(Mandatory = $false)] + [string]$Default +) + +Write-Host "AWS Profile Generator" + +$ACCOUNTPAGESIZE = 10 +$ROLEPAGESIZE = 10 + +# Variables to store default profile settings when found +$defaultAccountId = "" +$defaultRoleName = "" +$defaultRegionValue = "" +$defaultOutputValue = "" + +# Build account name mappings hashtable from -Map parameter +$accountMappings = @{} +if ($Map) { + foreach ($mapping in $Map) { + $parts = $mapping -split ':', 2 + if ($parts.Count -ne 2 -or [string]::IsNullOrEmpty($parts[0]) -or [string]::IsNullOrEmpty($parts[1])) { + Write-Error "Error: -Map value must be in FROM:TO format (e.g., 'Infrastructure:Infra')" + exit 1 + } + $accountMappings[$parts[0]] = $parts[1] + } +} + +# Check AWS CLI version +try { + $awsVersion = aws --version 2>&1 + if ($awsVersion -match "aws-cli/1") { + Write-Error "ERROR: This script requires AWS CLI v2 or higher" + exit 1 + } +} +catch { + Write-Error "ERROR: AWS CLI not found. Please install AWS CLI v2." + exit 1 +} + +# Overwrite option +if ($NoPrompt) { + $overwrite = $true +} +else { + Write-Host "" + $overwriteResp = Read-Host "Would you like to overwrite the output file ($ProfileFile)? (Y/n)" + if ([string]::IsNullOrEmpty($overwriteResp) -or $overwriteResp -eq 'Y' -or $overwriteResp -eq 'y') { + $overwrite = $true + } + else { + $overwrite = $false + } +} + +# Ensure .aws directory exists +$awsDir = Split-Path $ProfileFile -Parent +if (-not (Test-Path $awsDir)) { + New-Item -ItemType Directory -Path $awsDir -Force | Out-Null +} + +if ($overwrite) { + "" | Set-Content -Path $ProfileFile -NoNewline +} + +# Register client +Write-Host "" +Write-Host -NoNewline "Registering client... " + +$registerJson = aws sso-oidc register-client --client-name 'profiletool' --client-type 'public' --region $Region --output json 2>&1 + +if ($LASTEXITCODE -ne 0) { + Write-Host "Failed" + Write-Error "$registerJson" + exit 1 +} + +$registerOutput = $registerJson | ConvertFrom-Json +Write-Host "Succeeded" + +$secret = $registerOutput.clientSecret +$clientId = $registerOutput.clientId + +# Start device authorization +Write-Host -NoNewline "Starting device authorization... " + +$authJson = aws sso-oidc start-device-authorization --client-id $clientId --client-secret $secret --start-url $StartUrl --region $Region --output json 2>&1 + +if ($LASTEXITCODE -ne 0) { + Write-Host "Failed" + Write-Error "$authJson" + exit 1 +} + +$authOutput = $authJson | ConvertFrom-Json +Write-Host "Succeeded" + +$regUrl = $authOutput.verificationUriComplete +$deviceCode = $authOutput.deviceCode + +Write-Host "" +Write-Host "Open the following URL in your browser and sign in, then click the Allow button:" +Write-Host "" +Write-Host $regUrl +Write-Host "" +Read-Host "Press after you have signed in to continue..." + +# Get access token +Write-Host -NoNewline "Getting access token... " + +$tokenJson = aws sso-oidc create-token --client-id $clientId --client-secret $secret --grant-type 'urn:ietf:params:oauth:grant-type:device_code' --device-code $deviceCode --region $Region --output json 2>&1 + +if ($LASTEXITCODE -ne 0) { + Write-Host "Failed" + Write-Error "$tokenJson" + exit 1 +} + +$tokenOutput = $tokenJson | ConvertFrom-Json +Write-Host "Succeeded" + +$token = $tokenOutput.accessToken + +# Set defaults for profiles +$defRegion = $Region +$defOutput = "json" + +# Batch or interactive +if ($NoPrompt) { + $interactive = $false + $awsRegion = $defRegion + $output = $defOutput +} +else { + Write-Host "" + Write-Host "This script can create all profiles with default values" + Write-Host "or it can prompt you regarding each profile before it gets created." + Write-Host "" + $resp = Read-Host "Would you like to be prompted for each profile? (Y/n)" + + # Default to not prompted (N) + $interactive = $false + $awsRegion = $defRegion + $output = $defOutput + + if ($resp -eq 'Y' -or $resp -eq 'y') { + $interactive = $true + } +} + +# Retrieve accounts +Write-Host "" +Write-Host -NoNewline "Retrieving accounts... " + +$accountsJson = aws sso list-accounts --access-token $token --page-size $ACCOUNTPAGESIZE --region $Region --output json 2>&1 + +if ($LASTEXITCODE -ne 0) { + Write-Host "Failed" + Write-Error "$accountsJson" + exit 1 +} + +$accountsOutput = $accountsJson | ConvertFrom-Json +$accounts = $accountsOutput.accountList | Sort-Object -Property accountName +Write-Host "Succeeded" + +$createdProfiles = @() + +# Write sso-session block +Add-Content -Path $ProfileFile -Value "" +Add-Content -Path $ProfileFile -Value "#BEGIN_AWS_SSO_PROFILES" +Add-Content -Path $ProfileFile -Value "" +Add-Content -Path $ProfileFile -Value "[sso-session my-sso]" +Add-Content -Path $ProfileFile -Value "sso_start_url = $StartUrl" +Add-Content -Path $ProfileFile -Value "sso_region = $Region" +Add-Content -Path $ProfileFile -Value "sso_registration_scopes = sso:account:access" + + +# Process each account +foreach ($account in $accounts) { + $acctNum = $account.accountId + $acctName = $account.accountName + + # Apply account name mappings (if -Map was specified) + if ($accountMappings.ContainsKey($acctName)) { + $acctName = $accountMappings[$acctName] + } + + Write-Host "" + Write-Host "Adding roles for account $acctNum ($acctName)..." + + # Add comment to profile file + Add-Content -Path $ProfileFile -Value "" + Add-Content -Path $ProfileFile -Value "# $acctName ($acctNum)" + + $rolesJson = aws sso list-account-roles --account-id $acctNum --access-token $token --page-size $ROLEPAGESIZE --region $Region --output json 2>&1 + + if ($LASTEXITCODE -ne 0) { + Write-Host "Failed to retrieve roles." + Write-Error "$rolesJson" + exit 1 + } + + $rolesOutput = $rolesJson | ConvertFrom-Json + $roles = $rolesOutput.roleList + + foreach ($role in $roles) { + $roleName = $role.roleName + + Write-Host "" + + if ($interactive) { + $create = Read-Host "Create a profile for $roleName role? (Y/n)" + if ($create -eq 'n' -or $create -eq 'N') { + continue + } + + Write-Host "" + $inputRegion = Read-Host "CLI default client Region [$defRegion]" + if (-not [string]::IsNullOrEmpty($inputRegion)) { + $awsRegion = $inputRegion + $defRegion = $awsRegion + } + else { + $awsRegion = $defRegion + } + + $inputOutput = Read-Host "CLI default output format [$defOutput]" + if (-not [string]::IsNullOrEmpty($inputOutput)) { + $output = $inputOutput + $defOutput = $output + } + else { + $output = $defOutput + } + } + + # Sanitize account name (keep only alphanumeric and hyphen) + $safeAcctName = $acctName -replace '[^a-zA-Z0-9-]', '' + $defaultProfileName = "${safeAcctName}${roleName}" + + while ($true) { + if ($interactive) { + $inputProfileName = Read-Host "CLI profile name [$defaultProfileName]" + if (-not [string]::IsNullOrEmpty($inputProfileName)) { + $profileName = $inputProfileName + } + else { + $profileName = $defaultProfileName + } + } + else { + $profileName = $defaultProfileName + } + + # Check if profile already exists + if (Test-Path $ProfileFile) { + $existingContent = Get-Content $ProfileFile -Raw + if ($existingContent -match "\[\s*profile\s+$([regex]::Escape($profileName))\s*\]") { + Write-Host "Profile name already exists!" + if ($interactive) { + continue + } + else { + Write-Host "Skipping..." + break + } + } + } + + Write-Host -NoNewline "Creating $profileName... " + Add-Content -Path $ProfileFile -Value "" + Add-Content -Path $ProfileFile -Value "[profile $profileName]" + Add-Content -Path $ProfileFile -Value "sso_session = my-sso" + Add-Content -Path $ProfileFile -Value "sso_account_id = $acctNum" + Add-Content -Path $ProfileFile -Value "sso_role_name = $roleName" + Add-Content -Path $ProfileFile -Value "region = $awsRegion" + Add-Content -Path $ProfileFile -Value "output = $output" + Write-Host "Succeeded" + $createdProfiles += $profileName + + # Check if this profile should be the default + if (-not [string]::IsNullOrEmpty($Default) -and $profileName -eq $Default) { + $defaultAccountId = $acctNum + $defaultRoleName = $roleName + $defaultRegionValue = $awsRegion + $defaultOutputValue = $output + } + break + } + } + + Write-Host "" + Write-Host "Done adding roles for AWS account $acctNum ($acctName)" +} + +# Write default profile if specified and found +if (-not [string]::IsNullOrEmpty($Default)) { + if (-not [string]::IsNullOrEmpty($defaultAccountId)) { + Add-Content -Path $ProfileFile -Value "" + Add-Content -Path $ProfileFile -Value "# Default profile (mirrors $Default)" + Add-Content -Path $ProfileFile -Value "[default]" + Add-Content -Path $ProfileFile -Value "sso_session = my-sso" + Add-Content -Path $ProfileFile -Value "sso_account_id = $defaultAccountId" + Add-Content -Path $ProfileFile -Value "sso_role_name = $defaultRoleName" + Add-Content -Path $ProfileFile -Value "region = $defaultRegionValue" + Add-Content -Path $ProfileFile -Value "output = $defaultOutputValue" + Write-Host "" + Write-Host "Created [default] profile mirroring $Default" + } + else { + Write-Host "" + Write-Host "WARNING: -Default profile '$Default' was not found among created profiles" + } +} + +# Add old profile +Add-Content -Path $ProfileFile -Value "" +Add-Content -Path $ProfileFile -Value "" +Add-Content -Path $ProfileFile -Value "[profile old]" +Add-Content -Path $ProfileFile -Value "region = us-east-1" +Add-Content -Path $ProfileFile -Value "" +Add-Content -Path $ProfileFile -Value "#END_AWS_SSO_PROFILES" + +Write-Host "" +Write-Host "Processing complete." +Write-Host "" +Write-Host "Added the following profiles to ${ProfileFile}:" +Write-Host "" + +foreach ($profile in $createdProfiles) { + Write-Host $profile +} + +Write-Host "" +exit 0 diff --git a/awsssoprofiletool.sh b/awsssoprofiletool.sh index 55c7b27..0685a9a 100755 --- a/awsssoprofiletool.sh +++ b/awsssoprofiletool.sh @@ -1,6 +1,6 @@ #!/bin/bash # -# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# Copyright 2025 Amazon.com, Inc. or its affiliates. and Frank Bernhardt. All Rights Reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,30 +20,128 @@ # # Syntax: # -# ssoprofiletool [] +# awsssoprofiletool.sh [-y] [--map "FROM:TO" ...] [--default ] [] # # is the region where AWS SSO is configured (e.g., us-east-1) # is the AWS SSO start URL # is the file where the profiles will be written (default is # ~/.aws/config) +# +# Options: +# -y : Run in non-interactive mode. Overwrites the config file and creates all +# profiles without prompts. +# --map "FROM:TO" : Map account name FROM to TO in profile names. Can be +# specified multiple times. Example: --map "Infrastructure:Infra" +# --default : Create a [default] profile that mirrors the specified +# profile. Example: --default "DevAdministratorAccess" ACCOUNTPAGESIZE=10 ROLEPAGESIZE=10 PROFILEFILE="$HOME/.aws/config" -if [ $# -lt 2 ]; -then - echo "Syntax: $0 []" - exit 1 +# Store account name mappings as newline-separated "FROM:TO" entries (bash 3 compatible) +account_mappings="" + +noprompt=false +default_profile="" + +# Variables to store default profile settings when found +default_account_id="" +default_role_name="" +default_region="" +default_output="" + +# Parse options +while [ $# -gt 0 ]; do + case "$1" in + -y) + noprompt=true + shift + ;; + --map) + if [ -z "$2" ] || [ "${2#-}" != "$2" ]; then + echo "Error: --map requires a value in FROM:TO format" + exit 1 + fi + # Validate format contains exactly one colon with non-empty parts + from_name="${2%%:*}" + to_name="${2#*:}" + if [ -z "$from_name" ] || [ -z "$to_name" ] || [ "$from_name" = "$2" ]; then + echo "Error: --map value must be in FROM:TO format (e.g., 'Infrastructure:Infra')" + exit 1 + fi + account_mappings="${account_mappings}${2} +" + shift 2 + ;; + --default) + if [ -z "$2" ] || [ "${2#-}" != "$2" ]; then + echo "Error: --default requires a profile name" + exit 1 + fi + default_profile="$2" + shift 2 + ;; + -*) + echo "Unknown option: $1" + echo "Syntax: $0 [-y] [--map \"FROM:TO\" ...] [--default ] []" + exit 1 + ;; + *) + break + ;; + esac +done + +# Function to apply account name mapping (bash 3 compatible) +apply_mapping() { + local name="$1" + local mapped + # Search for mapping in the stored mappings + mapped=$(echo "$account_mappings" | grep "^${name}:" | head -1) + if [ -n "$mapped" ]; then + echo "${mapped#*:}" + else + echo "$name" + fi +} + +if [ $# -lt 2 ]; then + echo "Syntax: $0 [-y] [--map \"FROM:TO\" ...] [--default ] []" + exit 1 fi -if [ $# -eq 3 ]; -then +region=$1 +starturl=$2 + +if [ $# -eq 3 ]; then profilefile=$3 else profilefile=$PROFILEFILE fi +# Overwrite option +if [ "$noprompt" = true ]; then + overwrite=true +else + echo + printf "%s" "Would you like to overwrite the output file ($profilefile)? (Y/n): " + read overwrite_resp < /dev/tty + if [ -z "$overwrite_resp" ]; + then + overwrite=true + elif [ "$overwrite_resp" == 'n' ] || [ "$overwrite_resp" == 'N' ]; + then + overwrite=false + else + overwrite=true + fi +fi + +if [ "$overwrite" = true ]; then + > "$profilefile" +fi + if [[ $(aws --version) == aws-cli/1* ]] then echo "ERROR: $0 requires AWS CLI v2 or higher" @@ -53,9 +151,9 @@ fi # Get secret and client ID to begin authentication session echo -echo -n "Registering client... " +printf "%s" "Registering client... " -out=$(aws sso-oidc register-client --client-name 'profiletool' --client-type 'public' --region "$1" --output text) +out=$(aws sso-oidc register-client --client-name 'profiletool' --client-type 'public' --region "$region" --output text) if [ $? -ne 0 ]; then @@ -65,14 +163,14 @@ else echo "Succeeded" fi -secret=$(awk -F ' ' '{print $3}' <<< "$out") -clientid=$(awk -F ' ' '{print $1}' <<< "$out") +secret=$(awk '{print $3}' <<< "$out") +clientid=$(awk '{print $1}' <<< "$out") # Start the authentication process -echo -n "Starting device authorization... " +printf "%s" "Starting device authorization... " -out=$(aws sso-oidc start-device-authorization --client-id "$clientid" --client-secret "$secret" --start-url "$2" --region "$1" --output text) +out=$(aws sso-oidc start-device-authorization --client-id "$clientid" --client-secret "$secret" --start-url "$starturl" --region "$region" --output text) if [ $? -ne 0 ]; then @@ -82,8 +180,8 @@ else echo "Succeeded" fi -regurl=$(awk -F ' ' '{print $6}' <<< "$out") -devicecode=$(awk -F ' ' '{print $1}' <<< "$out") +regurl=$(awk '{print $6}' <<< "$out") +devicecode=$(awk '{print $1}' <<< "$out") echo echo "Open the following URL in your browser and sign in, then click the Allow button:" @@ -92,13 +190,13 @@ echo "$regurl" echo echo "Press after you have signed in to continue..." -read continue +read continue < /dev/tty # Get the access token for use in the remaining API calls -echo -n "Getting access token... " +printf "%s" "Getting access token... " -out=$(aws sso-oidc create-token --client-id "$clientid" --client-secret "$secret" --grant-type 'urn:ietf:params:oauth:grant-type:device_code' --device-code "$devicecode" --region "$1" --output text) +out=$(aws sso-oidc create-token --client-id "$clientid" --client-secret "$secret" --grant-type 'urn:ietf:params:oauth:grant-type:device_code' --device-code "$devicecode" --region "$region" --output text) if [ $? -ne 0 ]; then @@ -108,44 +206,49 @@ else echo "Succeeded" fi -token=$(awk -F ' ' '{print $1}' <<< "$out") +token=$(awk '{print $1}' <<< "$out") # Set defaults for profiles -defregion="$1" +defregion="$region" defoutput="json" # Batch or interactive -echo -echo "$0 can create all profiles with default values" -echo "or it can prompt you regarding each profile before it gets created." -echo -echo -n "Would you like to be prompted for each profile? (Y/n): " -read resp < /dev/tty -if [ -z "$resp" ]; -then - interactive=true -elif [ "$resp" == 'n' ] || [ "$resp" == 'N' ]; -then +if [ "$noprompt" = true ]; then interactive=false awsregion=$defregion output=$defoutput else - interactive=true + echo + echo "$0 can create all profiles with default values" + echo "or it can prompt you regarding each profile before it gets created." + echo + printf "%s" "Would you like to be prompted for each profile? (Y/n): " + read resp < /dev/tty + # Default to not prompted (N) + interactive=false + awsregion=$defregion + output=$defoutput + if [ "$resp" == 'Y' ] || [ "$resp" == 'y' ]; + then + interactive=true + fi fi # Retrieve accounts first echo -echo -n "Retrieving accounts... " +printf "%s" "Retrieving accounts... " -acctsfile="$(mktemp ./sso.accts.XXXXXX)" +acctsfile="$(mktemp /tmp/sso.accts.XXXXXX)" # Set up trap to clean up temp file trap '{ rm -f "$acctsfile"; echo; exit 255; }' SIGINT SIGTERM -aws sso list-accounts --access-token "$token" --page-size $ACCOUNTPAGESIZE --region "$1" --output text > "$acctsfile" +aws sso list-accounts --access-token "$token" --page-size $ACCOUNTPAGESIZE --region "$region" --output text > "$acctsfile" +# Sort by account name (3rd column) +sort -t $'\t' -k 3 -o "$acctsfile" "$acctsfile" if [ $? -ne 0 ]; then @@ -158,22 +261,35 @@ fi declare -a created_profiles echo "" >> "$profilefile" -echo "###" >> "$profilefile" -echo "### The section below added by awsssoprofiletool.sh" >> "$profilefile" -echo "###" >> "$profilefile" +echo "#BEGIN_AWS_SSO_PROFILES" >> "$profilefile" + +echo "" >> "$profilefile" +echo "[sso-session my-sso]" >> "$profilefile" +echo "sso_start_url = $starturl" >> "$profilefile" +echo "sso_region = $region" >> "$profilefile" +echo "sso_registration_scopes = sso:account:access" >> "$profilefile" + # Read in accounts while IFS=$'\t' read skip acctnum acctname acctowner; do + # Apply account name mappings (if --map was specified) + acctname=$(apply_mapping "$acctname") + echo echo "Adding roles for account $acctnum ($acctname)..." - rolesfile="$(mktemp ./sso.roles.XXXXXX)" + + # Add comment to profile file + echo "" >> "$profilefile" + echo "# $acctname ($acctnum)" >> "$profilefile" + + rolesfile="$(mktemp /tmp/sso.roles.XXXXXX)" # Set up trap to clean up both temp files trap '{ rm -f "$rolesfile" "$acctsfile"; echo; exit 255; }' SIGINT SIGTERM - aws sso list-account-roles --account-id "$acctnum" --access-token "$token" --page-size $ROLEPAGESIZE --region "$1" --output text > "$rolesfile" + aws sso list-account-roles --account-id "$acctnum" --access-token "$token" --page-size $ROLEPAGESIZE --region "$region" --output text > "$rolesfile" if [ $? -ne 0 ]; then @@ -186,7 +302,7 @@ do echo if $interactive ; then - echo -n "Create a profile for $rolename role? (Y/n): " + printf "%s" "Create a profile for $rolename role? (Y/n): " read create < /dev/tty if [ -z "$create" ]; then @@ -197,21 +313,22 @@ do fi echo - echo -n "CLI default client Region [$defregion]: " + printf "%s" "CLI default client Region [$defregion]: " read awsregion < /dev/tty if [ -z "$awsregion" ]; then awsregion=$defregion ; fi defregion=$awsregion - echo -n "CLI default output format [$defoutput]: " + printf "%s" "CLI default output format [$defoutput]: " read output < /dev/tty if [ -z "$output" ]; then output=$defoutput ; fi defoutput=$output fi - p="$rolename-$acctnum" + safe_acctname=$(echo "$acctname" | tr -cd '[:alnum:]-') + p="${safe_acctname}${rolename}" while true ; do if $interactive ; then - echo -n "CLI profile name [$p]: " + printf "%s" "CLI profile name [$p]: " read profilename < /dev/tty if [ -z "$profilename" ]; then profilename=$p ; fi if [ -f "$profilefile" ]; @@ -238,17 +355,24 @@ do fi fi done - echo -n "Creating $profilename... " + printf "%s" "Creating $profilename... " echo "" >> "$profilefile" echo "[profile $profilename]" >> "$profilefile" - echo "sso_start_url = $2" >> "$profilefile" - echo "sso_region = $1" >> "$profilefile" + echo "sso_session = my-sso" >> "$profilefile" echo "sso_account_id = $acctnum" >> "$profilefile" echo "sso_role_name = $rolename" >> "$profilefile" echo "region = $awsregion" >> "$profilefile" echo "output = $output" >> "$profilefile" echo "Succeeded" created_profiles+=("$profilename") + + # Check if this profile should be the default + if [ -n "$default_profile" ] && [ "$profilename" = "$default_profile" ]; then + default_account_id="$acctnum" + default_role_name="$rolename" + default_region="$awsregion" + default_output="$output" + fi done < "$rolesfile" rm "$rolesfile" @@ -258,10 +382,31 @@ do done < "$acctsfile" rm "$acctsfile" +# Write default profile if specified and found +if [ -n "$default_profile" ]; then + if [ -n "$default_account_id" ]; then + echo "" >> "$profilefile" + echo "# Default profile (mirrors $default_profile)" >> "$profilefile" + echo "[default]" >> "$profilefile" + echo "sso_session = my-sso" >> "$profilefile" + echo "sso_account_id = $default_account_id" >> "$profilefile" + echo "sso_role_name = $default_role_name" >> "$profilefile" + echo "region = $default_region" >> "$profilefile" + echo "output = $default_output" >> "$profilefile" + echo + echo "Created [default] profile mirroring $default_profile" + else + echo + echo "WARNING: --default profile '$default_profile' was not found among created profiles" + fi +fi + echo >> "$profilefile" -echo "###" >> "$profilefile" -echo "### The section above added by awsssoprofiletool.sh" >> "$profilefile" -echo "###" >> "$profilefile" +echo "" >> "$profilefile" +echo "[profile old]" >> "$profilefile" +echo "region = us-east-1" >> "$profilefile" + +echo "#END_AWS_SSO_PROFILES" >> "$profilefile" echo echo "Processing complete." From d54c56723f0e3c501f09e0ca3b5fd2782f4c44f8 Mon Sep 17 00:00:00 2001 From: Frank Bernhardt Date: Mon, 1 Dec 2025 09:18:14 +0100 Subject: [PATCH 2/5] docs: add quick start one-liner installation examples --- README.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/README.md b/README.md index 1750076..628d263 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,40 @@ command before you can use any AWS SSO profile. However, once you have logged in once, you will be able to use any of the created profiles until your authorization token expires. +### Quick Start (One-Liner) + +Run the tool directly without downloading: + +**Bash (macOS/Linux):** +```bash +curl -fsSL https://raw.githubusercontent.com/frank-bee/aws-sso-profile-tool/main/awsssoprofiletool.sh | bash -s -- +``` + +Example: +```bash +curl -fsSL https://raw.githubusercontent.com/frank-bee/aws-sso-profile-tool/main/awsssoprofiletool.sh | bash -s -- us-east-1 "https://mycompany.awsapps.com/start" +``` + +With options (non-interactive + default profile): +```bash +curl -fsSL https://raw.githubusercontent.com/frank-bee/aws-sso-profile-tool/main/awsssoprofiletool.sh | bash -s -- -y --default "DevAdministratorAccess" us-east-1 "https://mycompany.awsapps.com/start" +``` + +**PowerShell (Windows):** +```powershell +& ([scriptblock]::Create((irm https://raw.githubusercontent.com/frank-bee/aws-sso-profile-tool/main/awsssoprofiletool.ps1))) -Region -StartUrl +``` + +Example: +```powershell +& ([scriptblock]::Create((irm https://raw.githubusercontent.com/frank-bee/aws-sso-profile-tool/main/awsssoprofiletool.ps1))) -Region us-east-1 -StartUrl "https://mycompany.awsapps.com/start" +``` + +With options (non-interactive + default profile): +```powershell +& ([scriptblock]::Create((irm https://raw.githubusercontent.com/frank-bee/aws-sso-profile-tool/main/awsssoprofiletool.ps1))) -Region us-east-1 -StartUrl "https://mycompany.awsapps.com/start" -NoPrompt -Default "DevAdministratorAccess" +``` + ### Installation To install the tool, follow these steps: From 607efe6d32be648b3cbf15ab49eec00fc2819706 Mon Sep 17 00:00:00 2001 From: Frank Bernhardt Date: Mon, 1 Dec 2025 09:25:38 +0100 Subject: [PATCH 3/5] docs: fix one-liner syntax and improve examples --- README.md | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 628d263..6df57c1 100644 --- a/README.md +++ b/README.md @@ -28,17 +28,23 @@ Run the tool directly without downloading: **Bash (macOS/Linux):** ```bash -curl -fsSL https://raw.githubusercontent.com/frank-bee/aws-sso-profile-tool/main/awsssoprofiletool.sh | bash -s -- +bash <(curl -fsSL https://raw.githubusercontent.com/frank-bee/aws-sso-profile-tool/main/awsssoprofiletool.sh) ``` Example: ```bash -curl -fsSL https://raw.githubusercontent.com/frank-bee/aws-sso-profile-tool/main/awsssoprofiletool.sh | bash -s -- us-east-1 "https://mycompany.awsapps.com/start" +bash <(curl -fsSL https://raw.githubusercontent.com/frank-bee/aws-sso-profile-tool/main/awsssoprofiletool.sh) \ + us-east-1 "https://mycompany.awsapps.com/start" ``` -With options (non-interactive + default profile): +With options (non-interactive + default profile + account mappings): ```bash -curl -fsSL https://raw.githubusercontent.com/frank-bee/aws-sso-profile-tool/main/awsssoprofiletool.sh | bash -s -- -y --default "DevAdministratorAccess" us-east-1 "https://mycompany.awsapps.com/start" +bash <(curl -fsSL https://raw.githubusercontent.com/frank-bee/aws-sso-profile-tool/main/awsssoprofiletool.sh) \ + -y \ + --map "Infrastructure:Infra" \ + --map "Development:Dev" \ + --default "DevAdministratorAccess" \ + us-east-1 "https://mycompany.awsapps.com/start" ``` **PowerShell (Windows):** @@ -48,12 +54,19 @@ curl -fsSL https://raw.githubusercontent.com/frank-bee/aws-sso-profile-tool/main Example: ```powershell -& ([scriptblock]::Create((irm https://raw.githubusercontent.com/frank-bee/aws-sso-profile-tool/main/awsssoprofiletool.ps1))) -Region us-east-1 -StartUrl "https://mycompany.awsapps.com/start" +& ([scriptblock]::Create((irm https://raw.githubusercontent.com/frank-bee/aws-sso-profile-tool/main/awsssoprofiletool.ps1))) ` + -Region us-east-1 ` + -StartUrl "https://mycompany.awsapps.com/start" ``` -With options (non-interactive + default profile): +With options (non-interactive + default profile + account mappings): ```powershell -& ([scriptblock]::Create((irm https://raw.githubusercontent.com/frank-bee/aws-sso-profile-tool/main/awsssoprofiletool.ps1))) -Region us-east-1 -StartUrl "https://mycompany.awsapps.com/start" -NoPrompt -Default "DevAdministratorAccess" +& ([scriptblock]::Create((irm https://raw.githubusercontent.com/frank-bee/aws-sso-profile-tool/main/awsssoprofiletool.ps1))) ` + -Region us-east-1 ` + -StartUrl "https://mycompany.awsapps.com/start" ` + -NoPrompt ` + -Map "Infrastructure:Infra","Development:Dev" ` + -Default "DevAdministratorAccess" ``` ### Installation From 0e671626bc22be65d62baee42dfc5a64313b6698 Mon Sep 17 00:00:00 2001 From: Frank Bernhardt Date: Mon, 1 Dec 2025 09:43:24 +0100 Subject: [PATCH 4/5] fix: replace exit with return to prevent session close in scriptblock execution --- awsssoprofiletool.ps1 | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/awsssoprofiletool.ps1 b/awsssoprofiletool.ps1 index 1cb1936..c363ba3 100644 --- a/awsssoprofiletool.ps1 +++ b/awsssoprofiletool.ps1 @@ -96,7 +96,7 @@ if ($Map) { $parts = $mapping -split ':', 2 if ($parts.Count -ne 2 -or [string]::IsNullOrEmpty($parts[0]) -or [string]::IsNullOrEmpty($parts[1])) { Write-Error "Error: -Map value must be in FROM:TO format (e.g., 'Infrastructure:Infra')" - exit 1 + return } $accountMappings[$parts[0]] = $parts[1] } @@ -107,12 +107,12 @@ try { $awsVersion = aws --version 2>&1 if ($awsVersion -match "aws-cli/1") { Write-Error "ERROR: This script requires AWS CLI v2 or higher" - exit 1 + return } } catch { Write-Error "ERROR: AWS CLI not found. Please install AWS CLI v2." - exit 1 + return } # Overwrite option @@ -149,7 +149,7 @@ $registerJson = aws sso-oidc register-client --client-name 'profiletool' --clien if ($LASTEXITCODE -ne 0) { Write-Host "Failed" Write-Error "$registerJson" - exit 1 + return } $registerOutput = $registerJson | ConvertFrom-Json @@ -166,7 +166,7 @@ $authJson = aws sso-oidc start-device-authorization --client-id $clientId --clie if ($LASTEXITCODE -ne 0) { Write-Host "Failed" Write-Error "$authJson" - exit 1 + return } $authOutput = $authJson | ConvertFrom-Json @@ -190,7 +190,7 @@ $tokenJson = aws sso-oidc create-token --client-id $clientId --client-secret $se if ($LASTEXITCODE -ne 0) { Write-Host "Failed" Write-Error "$tokenJson" - exit 1 + return } $tokenOutput = $tokenJson | ConvertFrom-Json @@ -234,7 +234,7 @@ $accountsJson = aws sso list-accounts --access-token $token --page-size $ACCOUNT if ($LASTEXITCODE -ne 0) { Write-Host "Failed" Write-Error "$accountsJson" - exit 1 + return } $accountsOutput = $accountsJson | ConvertFrom-Json @@ -275,7 +275,7 @@ foreach ($account in $accounts) { if ($LASTEXITCODE -ne 0) { Write-Host "Failed to retrieve roles." Write-Error "$rolesJson" - exit 1 + return } $rolesOutput = $rolesJson | ConvertFrom-Json @@ -410,4 +410,3 @@ foreach ($profile in $createdProfiles) { } Write-Host "" -exit 0 From 8753b2a6fdb23460f88506fa3e5cee23ef443fb4 Mon Sep 17 00:00:00 2001 From: Frank Bernhardt Date: Mon, 1 Dec 2025 10:06:44 +0100 Subject: [PATCH 5/5] docs: use generic role names in examples --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 6df57c1..e9b3817 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,9 @@ With options (non-interactive + default profile + account mappings): ```bash bash <(curl -fsSL https://raw.githubusercontent.com/frank-bee/aws-sso-profile-tool/main/awsssoprofiletool.sh) \ -y \ - --map "Infrastructure:Infra" \ + --map "Production:Prod" \ --map "Development:Dev" \ - --default "DevAdministratorAccess" \ + --default "DevAdminAccess" \ us-east-1 "https://mycompany.awsapps.com/start" ``` @@ -65,8 +65,8 @@ With options (non-interactive + default profile + account mappings): -Region us-east-1 ` -StartUrl "https://mycompany.awsapps.com/start" ` -NoPrompt ` - -Map "Infrastructure:Infra","Development:Dev" ` - -Default "DevAdministratorAccess" + -Map "Production:Prod","Development:Dev" ` + -Default "DevAdminAccess" ``` ### Installation @@ -103,21 +103,21 @@ The arguments are as follows: **Example with account name mappings:** ```bash ./awsssoprofiletool.sh -y \ - --map "Infrastructure:Infra" \ + --map "Production:Prod" \ --map "Development:Dev" \ us-east-1 https://example.awsapps.com/start ``` -This would create profiles like `InfraAdministratorAccess` instead of `InfrastructureAdministratorAccess`. +This would create profiles like `ProdAdminAccess` instead of `ProductionAdminAccess`. **Example with default profile:** ```bash ./awsssoprofiletool.sh -y \ - --default "DevAdministratorAccess" \ + --default "DevAdminAccess" \ us-east-1 https://example.awsapps.com/start ``` -This creates all profiles plus a `[default]` section that mirrors `DevAdministratorAccess`, so AWS CLI commands work without specifying `--profile`. +This creates all profiles plus a `[default]` section that mirrors `DevAdminAccess`, so AWS CLI commands work without specifying `--profile`. ## Security