AwsCredentialsManager.psm1

<#
.SYNOPSIS
 
A module for managing AWS CLI configuration and credentials
 
.DESCRIPTION
 
AwsCredentialsManager is a PowerShell module for managing AWS CLI configuration
and credentials. It defines a naming convention for related profiles, makes it
easy to add, find and switch between profiles, and greatly simplifies the
process of refreshing temporary MFA credentials.
 
For example, at work you might have an IAM user and then several roles (one per
environment) which require MFA to use. AwsCredentialsManager makes it easy to
set up and manage the following profiles:
 
* work:iam to represent your IAM user, defined by your CLI access key/secret,
* work:mfa to represent your temporary MFA credentials, which uses work:iam to
  obtain a session token,
* work:dev to represent your role for the dev account, which uses work:mfa as
  the source profile,
* etc.
#>



<#
.SYNOPSIS
 
Create a new IAM user profile
 
.DESCRIPTION
 
Create a new user profile that authenticates via an access key and secret
 
.PARAMETER Domain
 
The domain that the user belongs to, to help group related users, e.g. work
 
.PARAMETER AccessKeyId
 
The AWS Access Key ID
 
.PARAMETER SecretAccessKey
 
The AWS Secret Access Key
 
.PARAMETER PathToCsv
 
A path to a CSV file containing the "Access key ID" and "Secret access key"
#>

Function New-AwsIamUser
{
    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName='KeySecret')]
    Param(
        [Parameter(Mandatory)]
        [string]
        $Domain,

        [Parameter(Mandatory, ParameterSetName='KeySecret')]
        [SecureString]
        $AccessKeyId,

        [Parameter(Mandatory, ParameterSetName='KeySecret')]
        [SecureString]
        $SecretAccessKey,

        [Parameter(Mandatory, ParameterSetName='CSV')]
        [string]
        $PathToCsv
    )

    If ($PathToCsv)
    {
        $credentials = Import-Csv -Path $PathToCsv
        $AccessKeyIdPlainText = $credentials.'Access key ID'
        $SecretAccessKeyPlainText = $credentials.'Secret access key'
    }
    Else
    {
        $AccessKeyIdPlainText = SecureStringToPlainText $AccessKeyId
        $SecretAccessKeyPlainText = SecureStringToPlainText $SecretAccessKey
    }

    $profileName = "$Domain`:iam"

    If ($PSCmdlet.ShouldProcess(
        "aws configure set aws_access_key_id $(ConvertTo-MaskedString $AccessKeyIdPlainText) --profile $profileName",
        "[profile $profileName]",
        "Set aws_access_key_id to $(ConvertTo-MaskedString $AccessKeyIdPlainText)"))
    {
        aws configure set aws_access_key_id $AccessKeyIdPlainText --profile $profileName
    }

    If ($PSCmdlet.ShouldProcess(
        "aws configure set aws_secret_access_key $(ConvertTo-MaskedString $SecretAccessKeyPlainText) --profile $profileName",
        "[profile $profileName]",
        "Set aws_secret_access_key to $(ConvertTo-MaskedString $SecretAccessKeyPlainText)"))
    {
        aws configure set aws_secret_access_key $SecretAccessKeyPlainText --profile $profileName
    }
}

<#
.SYNOPSIS
 
Create a new MFA user profile
 
.DESCRIPTION
 
Create a new user profile that authenticates via MFA-enabled tempoary session
tokens
 
.PARAMETER Domain
 
The domain that the user (and corresponding IAM user) belongs to, to help group
related users, e.g. work
 
.PARAMETER DeviceArn
 
The ARN of the user's MFA device
#>

Function New-AwsMfaUser
{
    [CmdletBinding(SupportsShouldProcess)]
    Param(
        [Parameter(Mandatory)]
        [ArgumentCompleter({ Get-AwsDomainsCompleter @args })]
        [string]
        $Domain,

        [Parameter(Mandatory)]
        [string]
        $DeviceArn
    )

    $mfaProfileName = "$domain`:mfa"

    If ($PSCmdlet.ShouldProcess(
        "aws configure set mfa_device_arn $DeviceArn --profile $mfaProfileName",
        "[profile $mfaProfileName]",
        "Set mfa_device_arn to $DeviceArn"))
    {
        aws configure set mfa_device_arn $DeviceArn --profile $mfaProfileName
    }
}

<#
.SYNOPSIS
 
Create a new assume-role profile
 
.DESCRIPTION
 
Create a new user profile for assuming roles, which authenticates via a source
profile of either an IAM or MFA user
 
.PARAMETER RoleName
 
A short name to describe the role
 
.PARAMETER RoleArn
 
The ARN of the role to assume
 
.PARAMETER Region
 
The default AWS region for the role
 
.PARAMETER User
 
The source profile to use for authentication. Tab-completion will list all
available IAM and MFA users
 
.PARAMETER Iam
 
The source profile to use for authentication. Tab-completion will list all
available IAM users
 
.PARAMETER Mfa
 
The source profile to use for authentication. Tab-completion will list all
available MFA users
#>

Function New-AwsAssumeRole
{
    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName='User')]
    Param(
        [Parameter(Mandatory)]
        [string]
        $RoleName,

        [Parameter(Mandatory)]
        [string]
        $RoleArn,

        [Parameter(Mandatory)]
        [string]
        $Region,

        [Parameter(Mandatory, ParameterSetName='User')]
        [ArgumentCompleter({ Get-AwsProfilesCompleter @args })]
        [string]
        $User,

        [Parameter(Mandatory, ParameterSetName='IamUser')]
        [ArgumentCompleter({ Get-AwsProfilesCompleter @args })]
        [string]
        $Iam,

        [Parameter(Mandatory, ParameterSetName='MfaUser')]
        [ArgumentCompleter({ Get-AwsProfilesCompleter @args })]
        [string]
        $Mfa
    )

    $SourceProfile = If ($User) { $User } `
        ElseIf ($Iam) { $Iam } `
        Else { $Mfa }

    $domain = Get-AwsDomain $SourceProfile

    $profileName = "$domain`:$RoleName"

    If ($PSCmdlet.ShouldProcess(
        "aws configure set role_arn $RoleArn --profile $profileName",
        "[profile $profileName]",
        "Set role_arn to $RoleArn"))
    {
        aws configure set role_arn $RoleArn --profile $profileName
    }

    If ($PSCmdlet.ShouldProcess(
        "aws configure set source_profile $SourceProfile --profile $profileName",
        "[profile $profileName]",
        "Set source_profile to $SourceProfile"))
    {
        aws configure set source_profile $SourceProfile --profile $profileName
    }

    If ($PSCmdlet.ShouldProcess(
        "aws configure set region $Region --profile $profileName",
        "[profile $profileName]",
        "Set region to $Region"))
    {
        aws configure set region $Region --profile $profileName
    }
}

<#
.SYNOPSIS
 
Set the active AWS profile
 
.DESCRIPTION
 
Set the active AWS profile by setting the AWS_PROFILE environment variable
 
.PARAMETER Domain
 
The domain that the user belongs to
 
.PARAMETER All
 
The profile within the given domain to use. Tab-completion will list all
available IAM, MFA and assume-role profiles
 
.PARAMETER AssumeRole
 
The profile within the given domain to use. Tab-completion will list all
available assume-role profiles
 
.PARAMETER Iam
 
The profile within the given domain to use. Tab-completion will list all
available IAM profiles
 
.PARAMETER Mfa
 
The profile within the given domain to use. Tab-completion will list all
available MFA profiles
#>

Function Set-AwsProfile
{
    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName='All')]
    Param(
        [string]
        [ArgumentCompleter({ Get-AwsDomainsCompleter @args })]
        $Domain,

        [Parameter(Mandatory, ParameterSetName='All')]
        [string]
        [ArgumentCompleter({ Get-AwsProfilesCompleter @args })]
        $All,

        [Parameter(Mandatory, ParameterSetName='AssumeRole')]
        [string]
        [ArgumentCompleter({ Get-AwsProfilesCompleter @args })]
        $AssumeRole,

        [Parameter(Mandatory, ParameterSetName='IamUser')]
        [ArgumentCompleter({ Get-AwsProfilesCompleter @args })]
        [string]
        $Iam,

        [Parameter(Mandatory, ParameterSetName='MfaUser')]
        [ArgumentCompleter({ Get-AwsProfilesCompleter @args })]
        [string]
        $Mfa
    )

    $roleName = If ($All) { $All } `
        ElseIf ($AssumeRole) { $AssumeRole} `
        ElseIf ($Iam) { $Iam } `
        Else { $Mfa }

    $profileName = If ($Domain) { "$Domain`:$roleName" } Else { $RoleName }

    If ($PSCmdlet.ShouldProcess(
        "`$env:AWS_PROFILE = '$profileName'",
        "`$env:AWS_PROFILE",
        "Set value to $profileName"))
    {
        $env:AWS_PROFILE = $profileName
    }
}
<#
.SYNOPSIS
 
Update an MFA-based temporary session token
 
.DESCRIPTION
 
Use an MFA code to update the temporary session token for the MFA profile
associated with the currently active profile
 
.PARAMETER Code
 
The MFA code
#>

Function Update-AwsMfaCredentials
{
    [CmdletBinding(SupportsShouldProcess)]
    Param(
        [SecureString]
        $Code,

        [Switch]
        $Force
    )

    If (-not (Get-AwsProfile))
    {
        Write-Error "No profile set. Please run Set-AwsProfile"
        Return
    }

    $profile = Get-AwsProfile
    $domain = Get-AwsDomain $profile
    $iamProfileName = "$domain`:iam"
    $mfaProfileName = "$domain`:mfa"

    $expiration = [DateTime](aws configure get expiration --profile $mfaProfileName)

    If (-not $Force -and $expiration - [DateTime]::Now -gt [TimeSpan]::FromHours(1))
    {
        Write-Information "Session already valid until $expiration" -InformationAction Continue
        Return
    }

    $region = aws configure get region --profile $profile
    If (-not $region)
    {
        $region = 'us-east-1'
        Write-Warning "No default region configured for $profile. Using $region"
        Write-Warning "Run 'aws configure set region <region>' to set a default region"
    }

    If (-not $Code)
    {
        $Code = Read-Host -AsSecureString -Prompt "Code"
    }

    $codePlainText = SecureStringToPlainText $Code

    $deviceArn = aws configure get mfa_device_arn --profile $mfaProfileName

    If ($PSCmdlet.ShouldProcess(
        "aws sts get-session-token --serial-number $deviceArn --token-code $(ConvertTo-MaskedString $codePlainText) --duration-seconds 129600 --profile $iamProfileName --region $region",
        "[profile $iamProfileName]",
        "Get session token"))
    {
        $resp = aws sts get-session-token `
            --serial-number $deviceArn `
            --token-code $codePlainText `
            --duration-seconds 129600 <# 36hrs #> `
            --profile $iamProfileName `
            --region $region

        If (-not $?)
        {
            Write-Error "Failed to acquire new session token"
            Return
        }
    }
    Else
    {
        Write-Error "Failed to acquire new session token"
        Return
    }

    $json = $resp | ConvertFrom-Json | Select-Object -Expand Credentials

    $accessKeyId = $json.AccessKeyId
    $secretAccessKey = $json.SecretAccessKey
    $sessionToken = $json.SessionToken
    $expiration = $json.Expiration.ToString("o")

    If ($PSCmdlet.ShouldProcess(
        "aws configure set aws_access_key_id $(ConvertTo-MaskedString $accessKeyId) --profile $mfaProfileName",
        "[profile $mfaProfileName]",
        "Set aws_access_key_id to $(ConvertTo-MaskedString $accessKeyId)"))
    {
        aws configure set aws_access_key_id $accessKeyId --profile $mfaProfileName
    }

    If ($PSCmdlet.ShouldProcess(
        "aws configure set aws_secret_access_key $(ConvertTo-MaskedString $accessKeyId) --profile $mfaProfileName",
        "[profile $mfaProfileName]",
        "Set aws_secret_access_key to $(ConvertTo-MaskedString $secretAccessKey)"))
    {
        aws configure set aws_secret_access_key $secretAccessKey --profile $mfaProfileName
    }

    If ($PSCmdlet.ShouldProcess(
        "aws configure set aws_secret_access_key $(ConvertTo-MaskedString $accessKeyId) --profile $mfaProfileName",
        "[profile $mfaProfileName]",
        "Set aws_secret_access_key to $(ConvertTo-MaskedString $secretAccessKey)"))
    {
        aws configure set aws_session_token $sessionToken --profile $mfaProfileName
    }

    If ($PSCmdlet.ShouldProcess(
        "aws configure set expiration $expiration --profile $mfaProfileName",
        "[profile $mfaProfileName]",
        "Set expiration to $expiration"))
    {
        aws configure set expiration $expiration --profile $mfaProfileName
    }
}


# Helpers

Function Get-AwsDomain
{
    Param(
        [Parameter(Mandatory)]
        [string]
        $ProfileName
    )

    ($ProfileName -split ':',2)[0]
}

Function Get-AwsDomains
{
    Param(
        [string]
        $Search
    )

    $searchLocal = $Search

    aws configure list-profiles `
        | ForEach-Object { Get-AwsDomain $_ } `
        | Where-Object { $_ -like "$searchLocal*" } `
        | Sort-Object -Unique
}

Function Get-AwsDomainsCompleter
{
    $Search = $args[2]

    Get-AwsDomains -Search $Search
}

Function Get-AwsRoleName
{
    Param(
        [Parameter(Mandatory)]
        [string]
        $ProfileName
    )

    ($ProfileName -split ':',2)[1]
}

Function Get-AwsProfile
{
    $env:AWS_PROFILE
}

Function Get-AwsProfiles
{
    [CmdletBinding()]
    Param(
        [ValidateSet('Iam', 'Mfa', 'User', 'AssumeRole', 'All')]
        [string]
        $Type = 'All',

        [ArgumentCompleter({ Get-AwsDomainsCompleter @args })]
        [string]
        $Domain
    )

    $profiles = aws configure list-profiles

    $profiles = Switch ($Type)
    {
        'Iam' { $profiles | Where-Object { $_ -like '*:iam' } }
        'Mfa' { $profiles | Where-Object { $_ -like '*:mfa' } }
        'User' { $profiles | Where-Object { $_ -like '*:iam' -or $_ -like '*:mfa' } }
        'AssumeRole' { $profiles | Where-Object { -not ($_ -like '*:iam' -or $_ -like '*:mfa') } }
        'All' { $profiles }
    }

    $profiles = Switch ($Domain)
    {
        '' { $profiles }
        Default { $profiles | Where-Object { $_ -like "$domain`:*" } }
    }

    $profiles | Sort-Object
}

Function Get-AwsProfilesCompleter
{
    $Parameter = $args[1]
    $Search = $args[2]
    $FakeBoundParameters = $args[4]

    $domain = $FakeBoundParameters.Domain

    $profiles = Get-AwsProfiles -Type $Parameter -Domain $domain

    $Search = If ($domain) { "*:*$Search*" } Else { "*$Search*" }

    $profiles = $profiles | Where-Object { $_ -like $Search }

    If ($domain) { $profiles = $profiles | ForEach-Object { Get-AwsRoleName $_ } }

    $profiles
}

Function SecureStringToPlainText
{
    Param(
        [Parameter(Mandatory)]
        [SecureString]
        $SecureString
    )

    If ($Host.Version.Major -ge 7)
    {
        ConvertFrom-SecureString -AsPlainText $SecureString
    }
    Else
    {
        [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(
            [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString)
        )
    }
}

Function ConvertTo-MaskedString
{
    Param(
        [Parameter(Mandatory)]
        [String]
        $String
    )

    $String -Replace '.','*'
}