Packages

phoenix_kit

1.7.86
1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib phoenix_kit aws credentials_verifier.ex
Raw

lib/phoenix_kit/aws/credentials_verifier.ex

defmodule PhoenixKit.AWS.CredentialsVerifier do
@moduledoc """
AWS credentials verification module.
This module provides functionality to:
- Validate AWS Access Key ID and Secret Access Key format
- Verify credential connectivity via AWS STS GetCallerIdentity
- List available AWS regions
- Check minimal required permissions for email operations
## Features
- **Credential Validation**: Basic format validation for access key and secret
- **Connectivity Testing**: Verify credentials can make AWS API calls
- **Region Discovery**: List available regions for the AWS account
- **Permission Checks**: Validate access to SQS, SNS, and SES services
- **Error Handling**: Detailed error messages for common issues
## Usage
# Basic credential verification
PhoenixKit.AWS.CredentialsVerifier.verify_credentials(
access_key_id: "AKIA...",
secret_access_key: "****************",
region: "eu-north-1"
)
# Get available regions
PhoenixKit.AWS.CredentialsVerifier.get_available_regions(
access_key_id: "AKIA...",
secret_access_key: "****************",
region: "eu-north-1"
)
"""
require Logger
alias ExAws.{EC2, SNS, SQS, STS}
# Note: SES doesn't have ExAws library, will use direct HTTP calls via ExAws.Operation.Query
@doc """
Verifies AWS credentials using STS GetCallerIdentity.
## Parameters
- `access_key_id`: AWS Access Key ID (string)
- `secret_access_key`: AWS Secret Access Key (string)
- `region`: AWS region (string)
## Returns
- `{:ok, %{access_key_id: string, aws_user_id: string, account_id: string, arn: string}}` on success
- `{:error, :invalid_credentials}` for format issues
- `{:error, :authentication_failed}` for invalid credentials
- `{:error, :network_error}` for connectivity issues
- `{:error, rate_limited}` for AWS rate limiting
"""
def verify_credentials(access_key_id, secret_access_key, region) do
# Validate credential format first
with {:format_ok, true} <-
{:format_ok, validate_credentials_format(access_key_id, secret_access_key)},
{:config, {:ok, config}} <-
{:config, create_config(access_key_id, secret_access_key, region)},
{:sts_call, {:ok, %{body: body}}} <-
{:sts_call, STS.get_caller_identity() |> ExAws.request(config)},
{:parse, {:ok, aws_user_id, account_id, arn}} <- {:parse, parse_sts_response(body)} do
{:ok,
%{
access_key_id: access_key_id,
aws_user_id: aws_user_id,
account_id: account_id,
arn: arn
}}
else
{:format_ok, false} ->
{:error, :invalid_credentials,
"Invalid credential format. Access key should be 20 characters, secret key should not be empty."}
{:config, {:error, reason}} ->
{:error, :configuration_error, "Failed to create AWS configuration: #{inspect(reason)}"}
{:sts_call, {:error, %{status_code: 403}}} ->
{:error, :authentication_failed,
"AWS authentication failed. Please check your access key and secret key."}
{:sts_call, {:error, %{status_code: 404}}} ->
{:error, :authentication_failed,
"AWS authentication failed. Region not found or incorrect."}
{:sts_call, {:error, %{status_code: 429}}} ->
{:error, :rate_limited, "AWS API rate limit exceeded. Please try again later."}
{:sts_call, {:error, reason}} ->
{:error, :network_error, "Network or AWS API error: #{inspect(reason)}"}
{:parse, {:error, reason}} ->
{:error, :response_error, "Failed to parse AWS response: #{reason}"}
end
end
@doc """
Gets list of available AWS regions for the account.
## Parameters
- `access_key_id`: AWS Access Key ID (string)
- `secret_access_key`: AWS Secret Access Key (string)
- `region`: AWS region (string)
## Returns
- `{:ok, [region_names]}` on success
- `{:error, reason}` on failure
"""
def get_available_regions(access_key_id, secret_access_key, region) do
case create_config(access_key_id, secret_access_key, region) do
{:ok, config} ->
# Try to get regions from EC2 API first
case get_regions_from_ec2(config) do
{:ok, regions} when is_list(regions) and regions != [] ->
{:ok, regions}
{:error, :permission_denied} ->
# EC2 permission missing - fallback to common regions
Logger.warning(
"EC2 DescribeRegions permission missing. Using common regions list. " <>
"Add 'ec2:DescribeRegions' to IAM policy for accurate region list."
)
{:ok, list_common_regions(nil)}
{:error, reason} ->
# Other error - also fallback but log error
Logger.error(
"Failed to get regions from EC2 API: #{inspect(reason)}. Using fallback."
)
{:ok, list_common_regions(nil)}
end
{:error, reason} ->
{:error, reason}
end
end
# Private function to get regions from EC2 API
defp get_regions_from_ec2(config) do
case EC2.describe_regions() |> ExAws.request(config) do
{:ok, %{body: body}} ->
parse_ec2_regions(body)
{:error, {:http_error, 403, _}} ->
{:error, :permission_denied}
{:error, %{status_code: 403}} ->
{:error, :permission_denied}
{:error, reason} ->
{:error, reason}
end
rescue
e ->
Logger.error("Exception in EC2 describe_regions: #{inspect(e)}")
{:error, :ec2_api_error}
end
# Parse EC2 DescribeRegions response
defp parse_ec2_regions(body) when is_map(body) do
# ExAws parses XML to map structure
# Response structure: %{regions_set: [%{region_name: "us-east-1", ...}, ...]}
regions =
body
|> Map.get(:regions_set, [])
|> Enum.map(fn region -> Map.get(region, :region_name) end)
|> Enum.filter(&is_binary/1)
|> Enum.sort()
if Enum.empty?(regions) do
{:error, :empty_regions_list}
else
{:ok, regions}
end
rescue
e ->
Logger.error("Failed to parse EC2 regions response: #{inspect(e)}")
{:error, :parse_error}
end
defp parse_ec2_regions(_body) do
{:error, :invalid_response_format}
end
@doc """
Performs basic AWS permissions check using List operations.
⚠️ **Important Disclaimer:**
- This checks READ permissions (List operations), NOT CREATE permissions
- `ListQueues` does NOT guarantee `CreateQueue` permission
- `ListTopics` does NOT guarantee `CreateTopic` permission
- Actual CREATE permissions are verified during "Setup AWS Infrastructure"
This provides a basic sanity check that credentials have SOME access to required services.
## Checked Operations
- SQS: `ListQueues` (indicates basic SQS access)
- SNS: `ListTopics` (indicates basic SNS access)
- SES: `ListConfigurationSets` (indicates basic SES access)
- EC2: `DescribeRegions` (optional - for auto-loading regions feature)
## Parameters
- `access_key_id`: AWS Access Key ID (string)
- `secret_access_key`: AWS Secret Access Key (string)
- `region`: AWS region (string)
## Returns
- `{:ok, permissions_map}` where permissions_map is:
```
%{
sqs: %{"ListQueues" => :granted | :denied},
sns: %{"ListTopics" => :granted | :denied},
ses: %{"ListConfigurationSets" => :granted | :denied},
ec2: %{"DescribeRegions" => :granted | :denied, optional: true}
}
```
- `{:error, reason}` if configuration fails
"""
def check_permissions(access_key_id, secret_access_key, region) do
case create_config(access_key_id, secret_access_key, region) do
{:ok, config} ->
permissions = %{
sqs: check_sqs_permissions(config),
sns: check_sns_permissions(config),
ses: check_ses_permissions(config, region),
ec2: check_ec2_permissions(config)
}
{:ok, permissions}
{:error, reason} ->
{:error, reason}
end
end
# Check SQS permissions - only ListQueues (basic access indicator)
defp check_sqs_permissions(config) do
%{
"ListQueues" => check_sqs_list_queues(config)
}
end
# Check SNS permissions - only ListTopics (basic access indicator)
defp check_sns_permissions(config) do
%{
"ListTopics" => check_sns_list_topics(config)
}
end
# Check SES permissions - only ListConfigurationSets (basic access indicator)
defp check_ses_permissions(config, region) do
%{
"ListConfigurationSets" => check_ses_list_configuration_sets(config, region)
}
end
# Check EC2 permissions - optional feature for auto-loading regions
defp check_ec2_permissions(config) do
%{
"DescribeRegions" => check_ec2_describe_regions(config),
optional: true
}
end
# SQS permission checks - ListQueues only
defp check_sqs_list_queues(config) do
case SQS.list_queues() |> ExAws.request(config) do
{:ok, _} -> :granted
{:error, {:http_error, 403, _}} -> :denied
{:error, %{status_code: 403}} -> :denied
_ -> :denied
end
rescue
_ -> :denied
end
# SNS permission checks - ListTopics only
defp check_sns_list_topics(config) do
case SNS.list_topics() |> ExAws.request(config) do
{:ok, _} -> :granted
{:error, {:http_error, 403, _}} -> :denied
{:error, %{status_code: 403}} -> :denied
_ -> :denied
end
rescue
_ -> :denied
end
# SES permission checks - ListConfigurationSets only
defp check_ses_list_configuration_sets(config, region) do
case call_ses_api("ListConfigurationSets", %{}, config, region) do
{:ok, _} -> :granted
{:error, {:http_error, 403, _}} -> :denied
{:error, %{status_code: 403}} -> :denied
_ -> :denied
end
rescue
_ -> :denied
end
# EC2 permission checks - DescribeRegions (optional feature)
defp check_ec2_describe_regions(config) do
case EC2.describe_regions() |> ExAws.request(config) do
{:ok, _} -> :granted
{:error, {:http_error, 403, _}} -> :denied
{:error, %{status_code: 403}} -> :denied
_ -> :denied
end
rescue
_ -> :denied
end
# Helper to call SES API directly (ExAws doesn't have SES module)
defp call_ses_api(action, params, config, region) do
operation = %ExAws.Operation.Query{
path: "/",
params:
params
|> Map.put("Action", action)
|> Map.put("Version", "2010-12-01"),
service: :email,
action: action,
parser: &ExAws.Utils.identity/2
}
ExAws.request(operation, Keyword.put(config, :region, region))
end
# Private helper functions
defp validate_credentials_format(access_key_id, secret_access_key) do
access_key_valid? = String.length(String.trim(access_key_id)) == 20
secret_key_valid? = String.length(String.trim(secret_access_key)) > 0
access_key_valid? and secret_key_valid?
end
defp create_config(access_key_id, secret_access_key, region) do
config = [
access_key_id: String.trim(access_key_id),
secret_access_key: String.trim(secret_access_key),
region: String.trim(region)
]
{:ok, config}
rescue
e ->
{:error, "Failed to create config: #{inspect(e)}"}
end
# Handle already parsed map response from ExAws (modern behavior)
defp parse_sts_response(body) when is_map(body) do
# ExAws automatically parses the XML response into a map
# Structure: %{user_id: "...", account: "...", arn: "..."}
with {:aws_user_id, aws_user_id} when is_binary(aws_user_id) <-
{:aws_user_id, Map.get(body, :user_id)},
{:account, account} when is_binary(account) <- {:account, Map.get(body, :account)},
{:arn, arn} when is_binary(arn) <- {:arn, Map.get(body, :arn)} do
{:ok, aws_user_id, account, arn}
else
{:aws_user_id, _} -> {:error, "Missing or invalid user_id in STS response"}
{:account, _} -> {:error, "Missing or invalid account in STS response"}
{:arn, _} -> {:error, "Missing or invalid arn in STS response"}
end
rescue
e ->
{:error, "Map parsing error: #{inspect(e)}"}
end
# Handle XML string response (legacy/fallback)
defp parse_sts_response(body) when is_binary(body) do
# Parse XML response from STS
# Example structure:
# <GetCallerIdentityResponse>
# <GetCallerIdentityResult>
# <UserId>AIDACKEVSAMPLE...</UserId>
# <Account>123456789012</Account>
# <Arn>arn:aws:sts::123456789012:assumed-role/...</Arn>
# </GetCallerIdentityResult>
# </GetCallerIdentityResponse>
with true <- String.contains?(body, "<UserId>"),
{:ok, aws_user_id} <- extract_xml_value(body, "UserId"),
{:ok, account_id} <- extract_xml_value(body, "Account"),
{:ok, arn} <- extract_xml_value(body, "Arn") do
{:ok, aws_user_id, account_id, arn}
else
false -> {:error, "Could not find UserId in STS response"}
{:error, _} -> {:error, "Could not parse all required fields from STS response"}
end
rescue
e ->
{:error, "XML parsing error: #{inspect(e)}"}
end
defp parse_sts_response(_body) do
{:error, "Invalid STS response format"}
end
# Helper to extract XML tag value
defp extract_xml_value(body, tag_name) do
open_tag = "<#{tag_name}>"
close_tag = "</#{tag_name}>"
case String.split(body, open_tag) do
[_, rest] ->
case String.split(rest, close_tag) do
[value, _] -> {:ok, String.trim(value)}
_ -> {:error, :tag_not_closed}
end
_ ->
{:error, :tag_not_found}
end
end
defp list_common_regions(_account_id) do
# List of commonly used AWS regions. In a production environment,
# you would call EC2 DescribeRegions API to get actual regions for the account.
[
"us-east-1",
"us-east-2",
"us-west-1",
"us-west-2",
"af-south-1",
"ap-east-1",
"ap-northeast-1",
"ap-northeast-2",
"ap-northeast-3",
"ap-south-1",
"ap-southeast-1",
"ap-southeast-2",
"ca-central-1",
"eu-central-1",
"eu-north-1",
"eu-south-1",
"eu-south-2",
"eu-west-1",
"eu-west-2",
"eu-west-3",
"me-south-1",
"sa-east-1"
]
end
end