Skip to content

Commit

Permalink
Make AWSConfig its own type
Browse files Browse the repository at this point in the history
Rather than defining `AWSConfig` as a `SymbolDict` with no restrictions
on the contents, we can make it its own type, which gives more control
over how things are stored and accessed.

Fixes #53
  • Loading branch information
ararslan committed Feb 1, 2019
1 parent b7f7b90 commit 6607543
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 113 deletions.
201 changes: 132 additions & 69 deletions src/AWSCore.jl
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,134 @@ using DataStructures: OrderedDict
using JSON
using LazyJSON

# NOTE: This needs to be defined before AWSConfig. Methods defined on AWSCredentials are
# in src/AWSCredentials.jl.
"""
AWSCredentials
A type which holds AWS credentials.
When you interact with AWS, you specify your
[AWS Security Credentials](http://docs.aws.amazon.com/general/latest/gr/aws-security-credentials.html)
to verify who you are and whether you have permission to access the resources that you are
requesting. AWS uses the security credentials to authenticate and authorize your requests.
The fields `access_key_id` and `secret_key` hold the access keys used to authenticate API
requests (see [Creating, Modifying, and Viewing Access
Keys](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html#Using_CreateAccessKey)).
[Temporary Security Credentials](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp.html)
require the extra session `token` field.
The `user_arn` and `account_number` fields are used to cache the result of the
[`aws_user_arn`](@ref) and [`aws_account_number`](@ref) functions.
The `AWSCredentials()` constructor tries to load local credentials from:
* `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`
[environment variables](http://docs.aws.amazon.com/cli/latest/userguide/cli-environment.html),
* [`~/.aws/credentials`](http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html), or
* [EC2 Instance Credentials](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#instance-metadata-security-credentials).
To specify the profile to use from `~/.aws/credentials`, do, for example,
`AWSCredentials(profile="profile-name")`.
A `~/.aws/credentials` file can be created using the
[AWS CLI](https://aws.amazon.com/cli/) command `aws configrue`.
Or it can be created manually:
```ini
[default]
aws_access_key_id = AKIAXXXXXXXXXXXXXXXX
aws_secret_access_key = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
```
If your `~/.aws/credentials` file contains multiple profiles you can pass the
profile name as a string to the `profile` keyword argument (`nothing` by
default) or select a profile by setting the `AWS_PROFILE` environment variable.
"""
Most `AWSCore` functions take a `AWSConfig` dictionary as the first argument.
This dictionary holds [`AWSCredentials`](@ref) and AWS region configuration.
mutable struct AWSCredentials
access_key_id::String
secret_key::String
token::String
user_arn::String
account_number::String

function AWSCredentials(access_key_id, secret_key,
token="", user_arn="", account_number="")
new(access_key_id, secret_key, token, user_arn, account_number)
end
end

"""
AWSConfig
Most `AWSCore` functions take an `AWSConfig` object as the first argument.
This type holds [`AWSCredentials`](@ref), region, and output configuration.
# Constructors
AWSConfig(; profile, creds, region, output)
```julia
aws = AWSConfig(:creds => AWSCredentials(), :region => "us-east-1")`
Construct an `AWSConfig` object with the given profile, credentials, region, and output
format. All keyword arguments have default values and are thus optional.
* `profile`: Profile name passed to [`AWSCredentials`](@ref), or `nothing` (default)
* `creds`: `AWSCredentials` object, constructed using `profile` if not provided
* `region`: Region, read from `AWS_DEFAULT_REGION` if present, otherwise `"us-east-1"`
* `output`: Output format, defaulting to JSON (`"json"`)
# Examples
```julia-repl
julia> AWSConfig(profile="example", region="ap-southeast-2")
AWSConfig((AKIDEXAMPLE, wJa...)
, "ap-southeast-2", "json")
julia> AWSConfig(creds=AWSCredentials("AKIDEXAMPLE", "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY"))
AWSConfig((AKIDEXAMPLE, wJa...)
, "us-east-1", "json")
```
"""
const AWSConfig = SymbolDict
mutable struct AWSConfig
creds::AWSCredentials
region::String
output::String
end

function AWSConfig(; profile=nothing,
creds=AWSCredentials(profile=profile),
region=get(ENV, "AWS_DEFAULT_REGION", "us-east-1"),
output="json")
AWSConfig(creds, region, output)
end

# Relics of using SymbolDict
import Base: getindex, setindex!
Base.@deprecate AWSConfig(pairs::Pair...) AWSConfig(; pairs...)
Base.@deprecate getindex(conf::AWSConfig, x::Symbol) getfield(conf, x)
Base.@deprecate setindex!(conf::AWSConfig, val, var::Symbol) setfield!(conf, var, val)
Base.@deprecate aws_config AWSConfig
function Base.get(conf::AWSConfig, field::Symbol, alternative)
Base.depwarn("get(::AWSConf, a, b) is deprecated; access fields directly instead", :get)
if Base.fieldindex(AWSConfig, field, false) > 0
getfield(conf, field)
else
alternative
end
end
function Base.merge(conf::AWSConfig, d::AbstractDict{Symbol,<:Any})
Base.depwarn("merge(::AWSConf, dict) is deprecated; set fields directly instead", :merge)
for (k, v) in d
setfield!(conf, k, v)
end
conf
end
function Base.merge(d::AbstractDict{K,V}, conf::AWSConfig) where {K,V}
for f in fieldnames(AWSConfig)
d[convert(K, f)] = getfield(conf, f)
end
d
end

"""
The `AWSRequest` dictionary describes a single API request:
Expand All @@ -57,78 +174,24 @@ include("names.jl")
include("mime.jl")



#------------------------------------------------------------------------------#
# Configuration.
#------------------------------------------------------------------------------#

"""
The `aws_config` function provides a simple way to creates an
[`AWSConfig`](@ref) configuration dictionary.
```julia
>aws = aws_config()
>aws = aws_config(creds = my_credentials)
>aws = aws_config(region = "ap-southeast-2")
>aws = aws_config(profile = "profile-name")
```
By default, the `aws_config` attempts to load AWS credentials from:
- `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` [environemnt variables](http://docs.aws.amazon.com/cli/latest/userguide/cli-environment.html),
- [`~/.aws/credentials`](http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html) or
- [EC2 Instance Credentials](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#instance-metadata-security-credentials).
A `~/.aws/credentials` file can be created using the
[AWS CLI](https://aws.amazon.com/cli/) command `aws configrue`.
Or it can be created manually:
```ini
[default]
aws_access_key_id = AKIAXXXXXXXXXXXXXXXX
aws_secret_access_key = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
```
If your `~/.aws/credentials` file contains multiple profiles you can pass the
profile name as a string to the `profile` keyword argument (`nothing` by
default) or select a profile by setting the `AWS_PROFILE` environment variable.
`aws_config` understands the following [AWS CLI environment
variables](http://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html#cli-environment):
`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`,
`AWS_DEFAULT_REGION`, `AWS_PROFILE` and `AWS_CONFIG_FILE`.
An configuration dictionary can also be created directly from a key pair
as follows. However, putting access credentials in source code is discouraged.
```julia
aws = aws_config(creds = AWSCredentials("AKIAXXXXXXXXXXXXXXXX",
"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"))
```
global _default_aws_config = Ref{Union{AWSConfig,Nothing}}(nothing)

"""
function aws_config(;profile=nothing,
creds=AWSCredentials(profile=profile),
region=get(ENV, "AWS_DEFAULT_REGION", "us-east-1"),
args...)
@SymDict(creds, region, args...)
end
default_aws_config()

global _default_aws_config = nothing # Union{AWSConfig,Nothing}


"""
`default_aws_config` returns a global shared [`AWSConfig`](@ref) object
obtained by calling [`aws_config`](@ref) with no optional arguments.
Return the global shared [`AWSConfig`](@ref) object obtained by calling
[`AWSConfig()`](@ref) with no arguments.
"""
function default_aws_config()
global _default_aws_config
if _default_aws_config === nothing
_default_aws_config = aws_config()
if _default_aws_config[] === nothing
_default_aws_config[] = AWSConfig()
end
return _default_aws_config
return _default_aws_config[]
end


Expand Down Expand Up @@ -201,7 +264,7 @@ Service endpoint URL for `request`.
"""
function service_url(aws, request)
endpoint = get(request, :endpoint, request[:service])
region = "." * aws[:region]
region = "." * aws.region
if endpoint == "iam" || (endpoint == "sdb" && region == ".us-east-1")
region = ""
end
Expand All @@ -220,7 +283,7 @@ function service_query(aws::AWSConfig; args...)
request = Dict{Symbol,Any}(args)

request[:verb] = "POST"
request[:resource] = get(aws, :resource, "/")
request[:resource] = "/" # get(aws, :resource, "/") XXX how could config ever have that
request[:url] = service_url(aws, request)
request[:headers] = Dict("Content-Type" =>
"application/x-www-form-urlencoded; charset=utf-8")
Expand All @@ -230,7 +293,7 @@ function service_query(aws::AWSConfig; args...)
request[:query]["Version"] = request[:version]

if request[:service] == "iam"
aws = merge(aws, Dict(:region => "us-east-1"))
aws.region = "us-east-1"
end
if request[:service] in ["iam", "sts", "sqs", "sns"]
request[:query]["ContentType"] = "JSON"
Expand Down
33 changes: 4 additions & 29 deletions src/AWSCredentials.jl
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,7 @@ export AWSCredentials,
aws_account_number


"""
When you interact with AWS, you specify your [AWS Security Credentials](http://docs.aws.amazon.com/general/latest/gr/aws-security-credentials.html) to verify who you are and whether you have permission to access the resources that you are requesting. AWS uses the security credentials to authenticate and authorize your requests.
The fields `access_key_id` and `secret_key` hold the access keys used to authenticate API requests (see [Creating, Modifying, and Viewing Access Keys](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html#Using_CreateAccessKey)).
[Temporary Security Credentials](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp.html) require the extra session `token` field.
The `user_arn` and `account_number` fields are used to cache the result of the [`aws_user_arn`](@ref) and [`aws_account_number`](@ref) functions.
The `AWSCredentials()` constructor tries to load local Credentials from
environment variables, `~/.aws/credentials`, `~/.aws/config` or EC2 instance credentials.
To specify the profile to use from `~/.aws/credentials`, do, for example, `AWSCredentials(profile="profile-name")`.
"""
mutable struct AWSCredentials
access_key_id::String
secret_key::String
token::String
user_arn::String
account_number::String

function AWSCredentials(access_key_id, secret_key,
token="", user_arn="", account_number="")
new(access_key_id, secret_key, token, user_arn, account_number)
end
end
# AWSCredentials type defined in src/AWSCore.jl

function Base.show(io::IO,c::AWSCredentials)
println(io, string(c.user_arn,
Expand Down Expand Up @@ -145,7 +120,7 @@ e.g. `"arn:aws:iam::account-ID-without-hyphens:user/Bob"`
"""
function aws_user_arn(aws::AWSConfig)

creds = aws[:creds]
creds = aws.creds

if creds.user_arn == ""

Expand All @@ -164,7 +139,7 @@ end
12-digit [AWS Account Number](http://docs.aws.amazon.com/general/latest/gr/acct-identifiers.html).
"""
function aws_account_number(aws::AWSConfig)
creds = aws[:creds]
creds = aws.creds
if creds.account_number == ""
aws_user_arn(aws)
end
Expand Down Expand Up @@ -322,7 +297,7 @@ function aws_get_role(role::AbstractString, ini::Inifile)
end
credentials = dot_aws_credentials(source_profile)

config = AWSConfig(:creds=>credentials, :region=>aws_get_region(source_profile, ini))
config = AWSConfig(creds=credentials, region=aws_get_region(source_profile, ini))

role = Services.sts(
config,
Expand Down
4 changes: 2 additions & 2 deletions src/names.jl
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export arn, is_arn,
Generate an [Amazon Resource Name](http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html) for `service` and `resource`.
"""
function arn(service, resource,
region=get(default_aws_config(), :region, ""),
region=default_aws_config().region,
account=aws_account_number(default_aws_config()))

if service == "s3"
Expand All @@ -38,7 +38,7 @@ end
function arn(aws::AWSConfig,
service,
resource,
region=get(aws, :region, ""),
region=aws.region,
account=aws_account_number(aws))

arn(service, resource, region, account)
Expand Down
Loading

0 comments on commit 6607543

Please sign in to comment.