94 | Creates a new AWS instance.
95 | By default the instance will get a CredentialProviderChain set of
96 | credentials, which can be overridden.
97 |
98 |
Note that the AWS objects as well as the Service objects are expensive to
99 | create, so you might want to reuse them.
100 |
101 |
102 |
Parameters:
103 |
104 |
config
105 | (optional) the config table to be copied into the instance as the global aws_instance.config
106 |
107 |
108 |
109 |
110 |
111 |
112 |
Usage:
113 |
114 |
-- in the "init" phase initialize the configuration
115 | local _ = require("resty.aws.config").global
116 |
-- In your code
117 | local AWS = require("resty.aws")
118 | local AWS_global_config = require("resty.aws.config").global
119 |
120 | local config = { region = AWS_global_config.region }
121 |
122 | local aws = AWS(config)
123 |
124 | -- Override default "CredentialProviderChain" credentials.
125 | -- This is optional, the defaults should work with AWS-IAM.
126 | local my_creds = aws:Credentials {
127 | accessKeyId = "access",
128 | secretAccessKey = "secret",
129 | sessionToken = "token",
130 | }
131 | aws.config.credentials = my_creds
132 |
133 | -- instantiate a service (optionally overriding the aws-instance config)
134 | local sm = aws:SecretsManager {
135 | region = "us-east-2",
136 | }
137 |
138 | -- Invoke a method.
139 | -- Note this only takes the parameter table, and NOT a callback as the
140 | -- JS sdk requires. Instead this call will directly return the results.
141 | local results, err = sm:getSecretValue {
142 | SecretId = "arn:aws:secretsmanager:us-east-2:238406704566:secret:test-HN1F1k",
143 | }
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 | generated by LDoc 1.5.0
154 | Last updated 2024-09-23 09:29:37
155 |
opt options table, additional fields to the Credentials class:
100 |
101 |
params
102 | params table for the assumeRole function, or array of those
103 | tables in case of a chain of roles to assume.
104 |
105 |
aws
106 | AWS instance, required when creating a chain.
107 |
108 |
masterCredentials
109 | Credentials instance to use when assuming the
110 | role. Defaults to sts.config.credentials or aws.config.credentials in that
111 | order.
112 |
113 |
sts
114 | the STS service instance to use for fetching the credentials.
115 | Defaults to a new instance created as aws:STS().
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
Usage:
124 |
125 |
-- creating a chain of assumed roles
126 | local aws = AWS() -- provides the masterCredentials
127 | local role1 = { ... } -- parameters to assume role1, from the masterCredentials
128 | local role2 = { ... } -- parameters to assume role2, from the role1 credentials
129 | local role3 = { ... } -- parameters to assume role3, from the role2 credentials
130 |
131 | local creds = aws:ChainableTemporaryCredentials {
132 | params = { role1, role2, role3 },
133 | }
134 |
135 | -- Get credentials for role3
136 | local success, id, key, token, expiretime = creds:get()
137 | ifnot success then
138 | returnnil, id
139 | end
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 | generated by LDoc 1.5.0
150 | Last updated 2024-09-23 09:29:37
151 |
expiryWindow
121 | number (default 15) of seconds before expiry to start refreshing
122 |
123 |
accessKeyId
124 | (optional) only specify if you manually specify credentials
125 |
126 |
secretAccessKey
127 | (optional) only specify if you manually specify credentials
128 |
129 |
sessionToken
130 | (optional) only specify if you manually specify credentials
131 |
132 |
expireTime
133 | (optional, number (epoch) or string (rfc3339)). This should
134 | not be specified. Default: If any of the 3 secrets are given; 10yrs, otherwise 0
135 | (forcing a refresh on the first call to get).
136 |
94 | Constructor, inherits from Credentials.
95 |
96 |
Note: this class will fetch the credentials upon instantiation. So it can be
97 | instantiated in the init phase where there is still access to the environment
98 | variables. The standard prefixes AWS and AMAZON are covered by the config
99 | module, so in case those are used, only the config module needs to be loaded
100 | in the init phase.
101 |
102 |
103 |
Parameters:
104 |
105 |
opt options table, additional fields to the Credentials class:
106 |
107 |
envPrefix
108 | prefix to use when looking for environment variables, defaults to "AWS".
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 | generated by LDoc 1.5.0
125 | Last updated 2024-09-23 09:29:37
126 |
100 | Auto detects the current AWS region.
101 | It will try the following options (in order);
102 |
103 |
104 |
environment variable AWS_REGION
105 |
environment variable AWS_DEFAULT_REGION
106 |
ECS metadata V4 (parse region from "AvailabilityZone") if the environment
107 | variable ECS_CONTAINER_METADATA_URI_V4 is available
108 |
ECS metadata V3 (parse region from "AvailabilityZone") if the environment
109 | variable ECS_CONTAINER_METADATA_URI is available
110 |
IDMSv2 metadata (only if AWS_EC2_METADATA_DISABLED hasn't been set to true)
111 |
112 |
113 |
The IDMSv2 call makes a call to an IP endpoint, and hence could timeout
114 | (timeout is 5 seconds) if called on anything not being an EC2 or EKS instance.
115 |
116 |
Note: the result is cached so any consecutive calls will not perform any IO.
117 |
118 |
119 |
120 |
135 | Fetches ECS Task Metadata. Both for Fargate as well as EC2 based ECS.
136 | Support version 2, 3, and 4 (version 2 is NOT available on Fargate).
137 | V3 and V4 will return an error if no url is found in the related environment variable, V2 will make a request to
138 | the IP address and hence might timeout if ran on anything else than an EC2-based ECS container.
139 |
140 |
141 |
Parameters:
142 |
143 |
subpath
144 | (optional) path to return data from (default "/metadata" for V2, nothing for V3+)
145 |
146 |
version
147 | (optional) metadata version to get "V2", "V3", or "V4" (case insensitive, default "V4")
148 |
149 |
150 |
151 |
Returns:
152 |
153 |
154 | body & content-type (if json, the body will be decoded to a Lua table), or nil+err
155 |
156 |
157 |
158 |
159 |
160 |
166 | Fetches IDMS Metadata (EC2 and EKS).
167 | Will make a call to the IP address and hence might timeout if ran on anything
168 | else than an EC2-instance.
169 | Calling this function will make the calls, it will not honor the AWS_EC2_METADATA_DISABLED setting.
170 |
171 |
172 |
Parameters:
173 |
174 |
subpath
175 | (optional) subpath to return data from, default /latest/meta-data/
176 |
177 |
version
178 | (optional) version of IDMS to use, either "V1" or "V2" (case insensitive, default "V2")
179 |
180 |
181 |
182 |
Returns:
183 |
184 |
185 | body & content-type (if json, the body will be decoded to a Lua table), or nil+err
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 | generated by LDoc 1.5.0
199 | Last updated 2024-09-23 09:29:37
200 |
201 |
202 |
203 |
204 |
--------------------------------------------------------------------------------
/lua-resty-aws-dev-1.rockspec.template:
--------------------------------------------------------------------------------
1 | local package_name = "lua-resty-aws"
2 | local package_version = "dev"
3 | local rockspec_revision = "1"
4 | local github_account_name = "Kong"
5 | local github_repo_name = package_name
6 | local git_checkout = package_version == "dev" and "main" or package_version
7 |
8 | package = package_name
9 | version = package_version .. "-" .. rockspec_revision
10 |
11 | source = {
12 | url = "git://github.com/"..github_account_name.."/"..github_repo_name..".git",
13 | branch = git_checkout
14 | }
15 |
16 | description = {
17 | summary = "AWS SDK for OpenResty",
18 | detailed = [[
19 | AWS SDK generated from the same data as the AWS JavaScript SDK.
20 | ]],
21 | license = "Apache 2.0",
22 | homepage = "https://"..github_account_name..".github.io/"..github_repo_name.."/topics/README.md.html"
23 | }
24 |
25 | dependencies = {
26 | "penlight ~> 1",
27 | "lua-resty-http >= 0.16",
28 | "lua-resty-luasocket ~> 1",
29 | "lua-resty-openssl >= 0.8.17",
30 | "luaexpat >= 1.5.1",
31 | }
32 |
33 | build = {
34 | type = "builtin",
35 | modules = {
36 | ["resty.aws.init"] = "src/resty/aws/init.lua",
37 | ["resty.aws.utils"] = "src/resty/aws/utils.lua",
38 | ["resty.aws.config"] = "src/resty/aws/config.lua",
39 | ["resty.aws.request.validate"] = "src/resty/aws/request/validate.lua",
40 | ["resty.aws.request.build"] = "src/resty/aws/request/build.lua",
41 | ["resty.aws.request.sign"] = "src/resty/aws/request/sign.lua",
42 | ["resty.aws.request.execute"] = "src/resty/aws/request/execute.lua",
43 | ["resty.aws.request.signatures.utils"] = "src/resty/aws/request/signatures/utils.lua",
44 | ["resty.aws.request.signatures.v4"] = "src/resty/aws/request/signatures/v4.lua",
45 | ["resty.aws.request.signatures.presign"] = "src/resty/aws/request/signatures/presign.lua",
46 | ["resty.aws.request.signatures.none"] = "src/resty/aws/request/signatures/none.lua",
47 | ["resty.aws.service.rds.signer"] = "src/resty/aws/service/rds/signer.lua",
48 | ["resty.aws.credentials.Credentials"] = "src/resty/aws/credentials/Credentials.lua",
49 | ["resty.aws.credentials.ChainableTemporaryCredentials"] = "src/resty/aws/credentials/ChainableTemporaryCredentials.lua",
50 | ["resty.aws.credentials.CredentialProviderChain"] = "src/resty/aws/credentials/CredentialProviderChain.lua",
51 | ["resty.aws.credentials.EC2MetadataCredentials"] = "src/resty/aws/credentials/EC2MetadataCredentials.lua",
52 | ["resty.aws.credentials.EnvironmentCredentials"] = "src/resty/aws/credentials/EnvironmentCredentials.lua",
53 | ["resty.aws.credentials.SharedFileCredentials"] = "src/resty/aws/credentials/SharedFileCredentials.lua",
54 | ["resty.aws.credentials.RemoteCredentials"] = "src/resty/aws/credentials/RemoteCredentials.lua",
55 | ["resty.aws.credentials.TokenFileWebIdentityCredentials"] = "src/resty/aws/credentials/TokenFileWebIdentityCredentials.lua",
56 |
57 | -- AWS SDK files
58 | -- Do not modify anything between the start and end markers, that part is generated
59 | ["resty.aws.raw-api.region_config_data"] = "src/resty/aws/raw-api/region_config_data.lua",
60 | ["resty.aws.raw-api.table_of_contents"] = "src/resty/aws/raw-api/table_of_contents.lua",
61 | --START-MARKER--
62 |
63 | This will be replaced by the actual file list imported from the AWS SDK
64 |
65 | --END-MARKER--
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/spec/01-generic/01-config_spec.lua:
--------------------------------------------------------------------------------
1 | local pl_path = require("pl.path")
2 | local pl_utils = require("pl.utils")
3 | local d = require("pl.text").dedent
4 | local restore = require "spec.helpers"
5 |
6 |
7 | describe("config loader", function()
8 |
9 | local config_info = d[[
10 | [default]
11 | region=eu-central-1
12 | max_attempts=99
13 |
14 | [profile tieske]
15 | region=us-west-1
16 | ]]
17 | local config_filename = pl_path.tmpname()
18 |
19 | local config
20 | before_each(function()
21 | restore()
22 | restore.setenv("HOME")
23 | pl_utils.writefile(config_filename, config_info)
24 | end)
25 |
26 | after_each(function()
27 | restore()
28 | config = nil
29 | os.remove(config_filename)
30 | end)
31 |
32 | it("sets defaults", function()
33 | restore.setenv("AWS_CONFIG_FILE", config_filename)
34 | restore.setenv("AWS_DEFAULT_REGION", "eu-west-1")
35 |
36 | os.remove(config_filename) -- delete the file so we revert to defaults
37 | config = require "resty.aws.config"
38 | local conf = assert(config.get_config())
39 |
40 | -- just calling out as separate test; picking default region
41 | assert.equal("eu-west-1", conf.region)
42 |
43 | -- all defaults
44 | assert.same({
45 | AWS_DEFAULT_REGION = "eu-west-1",
46 | region = "eu-west-1",
47 | AWS_CONFIG_FILE = config_filename,
48 | AWS_EC2_METADATA_DISABLED = true,
49 | AWS_PROFILE = 'default',
50 | AWS_SHARED_CREDENTIALS_FILE = '~/.aws/credentials',
51 | cli_timestamp_format = 'iso8601',
52 | AWS_CLI_TIMESTAMP_FORMAT = 'iso8601',
53 | duration_seconds = 3600,
54 | AWS_DURATION_SECONDS = 3600,
55 | max_attempts = 5,
56 | AWS_MAX_ATTEMPTS = 5,
57 | parameter_validation = true,
58 | AWS_PARAMETER_VALIDATION = true,
59 | retry_mode = 'standard',
60 | AWS_RETRY_MODE = 'standard',
61 | sts_regional_endpoints = 'regional',
62 | AWS_STS_REGIONAL_ENDPOINTS = 'regional',
63 | }, conf)
64 | end)
65 |
66 | it("loads the configuration; default profile", function()
67 | restore.setenv("AWS_CONFIG_FILE", config_filename)
68 | config = require "resty.aws.config"
69 | local conf = assert(config.get_config())
70 |
71 | -- from the config file; default profile
72 | assert.equal("eu-central-1", conf.region)
73 | assert.equal(99, conf.max_attempts)
74 |
75 | assert.same({
76 | AWS_CONFIG_FILE = config_filename,
77 | AWS_EC2_METADATA_DISABLED = true,
78 | AWS_PROFILE = 'default',
79 | AWS_SHARED_CREDENTIALS_FILE = '~/.aws/credentials',
80 | cli_timestamp_format = 'iso8601',
81 | AWS_CLI_TIMESTAMP_FORMAT = 'iso8601',
82 | duration_seconds = 3600,
83 | AWS_DURATION_SECONDS = 3600,
84 | max_attempts = 99,
85 | AWS_MAX_ATTEMPTS = 5,
86 | region = "eu-central-1",
87 | parameter_validation = true,
88 | AWS_PARAMETER_VALIDATION = true,
89 | retry_mode = 'standard',
90 | AWS_RETRY_MODE = 'standard',
91 | sts_regional_endpoints = 'regional',
92 | AWS_STS_REGIONAL_ENDPOINTS = 'regional',
93 | }, conf)
94 | end)
95 |
96 | it("loads the configuration; 'tieske' profile", function()
97 | restore.setenv("AWS_CONFIG_FILE", config_filename)
98 | restore.setenv("AWS_PROFILE", "tieske")
99 | config = require "resty.aws.config"
100 | local conf = assert(config.get_config())
101 |
102 | -- from the config file; profile 'tieske'
103 | assert.equal("us-west-1", conf.region)
104 |
105 | assert.same({
106 | AWS_CONFIG_FILE = config_filename,
107 | AWS_EC2_METADATA_DISABLED = true,
108 | AWS_PROFILE = 'tieske',
109 | AWS_SHARED_CREDENTIALS_FILE = '~/.aws/credentials',
110 | cli_timestamp_format = 'iso8601',
111 | AWS_CLI_TIMESTAMP_FORMAT = 'iso8601',
112 | duration_seconds = 3600,
113 | AWS_DURATION_SECONDS = 3600,
114 | max_attempts = 5,
115 | AWS_MAX_ATTEMPTS = 5,
116 | region = "us-west-1",
117 | parameter_validation = true,
118 | AWS_PARAMETER_VALIDATION = true,
119 | retry_mode = 'standard',
120 | AWS_RETRY_MODE = 'standard',
121 | sts_regional_endpoints = 'regional',
122 | AWS_STS_REGIONAL_ENDPOINTS = 'regional',
123 | }, conf)
124 | end)
125 |
126 | it("global field returns the global configuration", function()
127 | config = require "resty.aws.config"
128 | local conf = config.global
129 |
130 | assert.same({
131 | region = nil, -- detection should fail
132 | AWS_CONFIG_FILE = "~/.aws/config",
133 | AWS_EC2_METADATA_DISABLED = true,
134 | AWS_PROFILE = 'default',
135 | AWS_SHARED_CREDENTIALS_FILE = '~/.aws/credentials',
136 | cli_timestamp_format = 'iso8601',
137 | AWS_CLI_TIMESTAMP_FORMAT = 'iso8601',
138 | duration_seconds = 3600,
139 | AWS_DURATION_SECONDS = 3600,
140 | max_attempts = 5,
141 | AWS_MAX_ATTEMPTS = 5,
142 | parameter_validation = true,
143 | AWS_PARAMETER_VALIDATION = true,
144 | retry_mode = 'standard',
145 | AWS_RETRY_MODE = 'standard',
146 | sts_regional_endpoints = 'regional',
147 | AWS_STS_REGIONAL_ENDPOINTS = 'regional',
148 | }, conf)
149 |
150 | end)
151 |
152 | end)
153 |
--------------------------------------------------------------------------------
/spec/01-generic/02-aws_spec.lua:
--------------------------------------------------------------------------------
1 | describe("AWS main instance", function()
2 |
3 |
4 | local AWS
5 |
6 | setup(function()
7 | package.loaded["resty.aws.request.execute"] = function ()
8 | return {
9 | status = 200,
10 | reason = "OK",
11 | headers = {},
12 | body = ""
13 | }
14 | end
15 | AWS = require "resty.aws"
16 | -- execute_request = require "resty.aws.request.execute"
17 | end)
18 |
19 | teardown(function()
20 | AWS = nil
21 | package.loaded["resty.aws"] = nil
22 | end)
23 |
24 |
25 | it("gets default config #only", function()
26 | local aws = AWS()
27 | assert.is.table(aws.config)
28 | assert.same({
29 | apiVersion = "latest",
30 | credentials = aws:CredentialProviderChain(),
31 | }, aws.config)
32 | end)
33 |
34 |
35 | it("overrides default config", function()
36 | local aws = AWS({
37 | region = "eu-central-1",
38 | apiVersion = "2020-09-29",
39 | })
40 | assert.is.table(aws.config)
41 | assert.same({
42 | region = "eu-central-1",
43 | apiVersion = "2020-09-29",
44 | credentials = require("resty.aws.credentials.CredentialProviderChain"):new({ aws = aws }),
45 | }, aws.config)
46 | end)
47 |
48 |
49 | it("allows custom config", function()
50 | local aws = AWS({
51 | unknown_property = "hi!",
52 | })
53 | assert.is.table(aws.config)
54 | assert.equal("hi!", aws.config.unknown_property)
55 | end)
56 |
57 |
58 | it("gets methods for services", function()
59 | local aws = AWS()
60 | assert.is.Function(aws.STS)
61 | end)
62 |
63 |
64 | it("gets methods for services, spaces removed from serviceId", function()
65 | local aws = AWS()
66 | assert.is.Function(aws.AppMesh) -- serviceId = "App Mesh"
67 | end)
68 |
69 | it("support sts regional endpoint inject and only inject once", function()
70 | local aws = AWS({
71 | region = "eu-central-1",
72 | stsRegionalEndpoints = "regional",
73 | })
74 |
75 | aws.config.credentials = aws:Credentials {
76 | accessKeyId = "test_id",
77 | secretAccessKey = "test_key",
78 | }
79 |
80 | assert.is.table(aws.config)
81 | local sts, _ = aws:STS()
82 | local _, _ = sts:assumeRole {
83 | RoleArn = "aws:arn::XXXXXXXXXXXXXXXXX:test123",
84 | RoleSessionName = "aws-test",
85 | }
86 | assert.same("https://sts.eu-central-1.amazonaws.com", sts.config.endpoint)
87 |
88 | local _, _ = sts:assumeRole {
89 | RoleArn = "aws:arn::XXXXXXXXXXXXXXXXX:test123",
90 | RoleSessionName = "aws-test",
91 | }
92 | assert.same("https://sts.eu-central-1.amazonaws.com", sts.config.endpoint)
93 | end)
94 |
95 | it("do not inject sts region info for sts vpc endpoint url", function()
96 | local aws = AWS({
97 | region = "eu-central-1",
98 | stsRegionalEndpoints = "regional",
99 | })
100 |
101 | aws.config.credentials = aws:Credentials {
102 | accessKeyId = "test_id",
103 | secretAccessKey = "test_key",
104 | }
105 |
106 | assert.is.table(aws.config)
107 |
108 | local regional_vpc_endpoint_url = "https://vpce-abcdefg-hijklmn-eu-central-1a.sts.eu-central-1.vpce.amazonaws.com"
109 |
110 | local sts, _ = aws:STS({
111 | endpoint = regional_vpc_endpoint_url,
112 | })
113 | local _, _ = sts:assumeRole {
114 | RoleArn = "aws:arn::XXXXXXXXXXXXXXXXX:test123",
115 | RoleSessionName = "aws-test",
116 | }
117 |
118 | assert.same(regional_vpc_endpoint_url, sts.config.endpoint)
119 |
120 | local _, _ = sts:assumeRole {
121 | RoleArn = "aws:arn::XXXXXXXXXXXXXXXXX:test123",
122 | RoleSessionName = "aws-test",
123 | }
124 | assert.same(regional_vpc_endpoint_url, sts.config.endpoint)
125 | end)
126 |
127 |
128 | end)
129 |
--------------------------------------------------------------------------------
/spec/01-generic/03-service_spec.lua:
--------------------------------------------------------------------------------
1 | local tablex = require "pl.tablex"
2 |
3 | describe("service generator", function()
4 |
5 |
6 | local AWS
7 |
8 | setup(function()
9 | AWS = require "resty.aws"
10 | end)
11 |
12 | teardown(function()
13 | AWS = nil
14 | package.loaded["resty.aws"] = nil
15 | end)
16 |
17 |
18 | it("creates a service", function()
19 | local sts = AWS():STS()
20 | assert.is.table(sts)
21 | assert.is.table(sts.config)
22 | assert.is.table(sts.api)
23 | assert.equal("STS", sts.api.metadata.serviceId)
24 | assert.equal("v4", sts.api.metadata.signatureVersion)
25 | assert.equal("AWS Security Token Service", sts.api.metadata.serviceFullName)
26 | end)
27 |
28 |
29 | it("sets the parent aws instance", function()
30 | local aws = AWS()
31 | local sts = aws:STS()
32 | assert.equal(aws, sts.config.aws)
33 | end)
34 |
35 |
36 | it("creates a specific service version", function()
37 | -- App Mesh has 2 versions, test both to make sure we do
38 | -- not hit a default
39 | local mesh = assert(AWS():AppMesh({ apiVersion = "2019-01-25", region = "us-east-1" }))
40 | assert.equal("2019-01-25", mesh.config.apiVersion)
41 |
42 | local mesh = assert(AWS():AppMesh({ apiVersion = "2018-10-01", region = "us-east-1" }))
43 | assert.equal("2018-10-01", mesh.config.apiVersion)
44 | end)
45 |
46 |
47 | it("'latest' indicates the most recent service version", function()
48 | -- Find latest version from table of contents
49 | local list = require("resty.aws.raw-api.table_of_contents")
50 | local service_list = tablex.filter(list, function(v) return v:find("DynamoDB:") end)
51 | table.sort(service_list)
52 | local _, _, latest_version = service_list[#service_list]:match("^(.-)%:(.-)%-(%d%d%d%d%-%d%d%-%d%d)$")
53 |
54 | local dynamoDB = assert(AWS():DynamoDB({ apiVersion = "latest", region = "us-east-1" }))
55 | assert.equal(latest_version, dynamoDB.config.apiVersion)
56 |
57 | -- use latest version by default
58 | dynamoDB = assert(AWS():DynamoDB({ region = "us-east-1" }))
59 | assert.equal(latest_version, dynamoDB.config.apiVersion)
60 | end)
61 |
62 |
63 | it("creates methods for operations", function()
64 | local sts = AWS():STS()
65 | assert.is.Function(sts.assumeRole)
66 | end)
67 |
68 |
69 | it("generated operations validate input 1", function()
70 | local sts = assert(AWS():STS())
71 | --print(require("pl.pretty").write(sts.config))
72 | local ok, err = sts:assumeRole({
73 | RoleSessionName = "just_a_name",
74 | })
75 | assert.equal("STS:assumeRole() validation error: params.RoleArn is required but missing", err)
76 | assert.is_nil(ok)
77 | end)
78 |
79 |
80 | it("generated operations validate input 2", function()
81 | local sm = assert(AWS():SecretsManager({region = "us-east-1"}))
82 | --print(require("pl.pretty").write(sm.config))
83 | local ok, err = sm:getSecretValue({
84 | RoleSessionName = "just_a_name",
85 | })
86 | assert.equal("SecretsManager:getSecretValue() validation error: params.SecretId is required but missing", err)
87 | assert.is_nil(ok)
88 | end)
89 |
90 |
91 | -- just for debugging, always fails
92 | --[[
93 | it("creates a service", function()
94 | local sts = AWS().STS()
95 | assert.equal({}, sts.api.operations.assumeRole)
96 | end)
97 | --]]
98 |
99 | end)
100 |
--------------------------------------------------------------------------------
/spec/02-requests/03-execute_spec.lua:
--------------------------------------------------------------------------------
1 | local restore = require "spec.helpers".restore
2 | local cjson = require "cjson"
3 |
4 | describe("request execution", function()
5 | local AWS, Credentials
6 |
7 | local mock_request_response = {
8 | ["s3.amazonaws.com"] = {
9 | ["/"] = {
10 | GET = {
11 | status = 200,
12 | headers = {
13 | ["x-amz-id-2"] = "test",
14 | ["x-amz-request-id"] = "test",
15 | ["Date"] = "test",
16 | ["Content-Type"] = "application/json",
17 | ["Server"] = "AmazonS3",
18 | },
19 | body = [[{"ListAllMyBucketsResult":{"Buckets":[]}}]]
20 | }
21 | }
22 | }
23 | }
24 |
25 | setup(function()
26 | restore()
27 | local http = require "resty.luasocket.http"
28 | http.connect = function(...) return true end
29 | http.request = function(self, req)
30 | return { has_body = true,
31 | status = mock_request_response[req.headers.Host][req.path][req.method].status,
32 | headers = mock_request_response[req.headers.Host][req.path][req.method].headers,
33 | read_body = function()
34 | local resp = mock_request_response[req.headers.Host][req.path][req.method].body
35 | return resp
36 | end
37 | }
38 | end
39 | http.set_timeout = function(...) return true end
40 | http.set_keepalive = function(...) return true end
41 | http.close = function(...) return true end
42 | AWS = require "resty.aws"
43 | Credentials = require "resty.aws.credentials.Credentials"
44 | end)
45 |
46 | teardown(function()
47 | package.loaded["resty.luasocket.http"] = nil
48 | AWS = nil
49 | package.loaded["resty.aws"] = nil
50 | end)
51 |
52 | it("tls defaults to true", function ()
53 | local config = {
54 | region = "us-east-1"
55 | }
56 |
57 | config.credentials = Credentials:new({
58 | accessKeyId = "teqst_id",
59 | secretAccessKey = "test_key",
60 | })
61 |
62 | local aws = AWS(config)
63 | aws.config.dry_run = true
64 |
65 | local s3 = aws:S3()
66 |
67 | assert.same(type(s3.getObject), "function")
68 | local request, err = s3:getObject({
69 | Bucket = "test-bucket",
70 | Key = "test-key",
71 | })
72 |
73 | assert.same(err, nil)
74 | assert.same(request.tls, true)
75 | end)
76 |
77 | it("support configuring tls false", function ()
78 | local config = {
79 | region = "us-east-1"
80 | }
81 |
82 | config.credentials = Credentials:new({
83 | accessKeyId = "teqst_id",
84 | secretAccessKey = "test_key",
85 | })
86 |
87 | local aws = AWS(config)
88 | aws.config.tls = false
89 | aws.config.dry_run = true
90 |
91 | local s3 = aws:S3()
92 |
93 | assert.same(type(s3.getObject), "function")
94 | local request, err = s3:getObject({
95 | Bucket = "test-bucket",
96 | Key = "test-key",
97 | })
98 |
99 | assert.same(err, nil)
100 | assert.same(request.port, 80)
101 | assert.same(request.tls, false)
102 | end)
103 |
104 | it("support configuring ssl verify false", function ()
105 | local config = {
106 | region = "us-east-1"
107 | }
108 |
109 | config.credentials = Credentials:new({
110 | accessKeyId = "teqst_id",
111 | secretAccessKey = "test_key",
112 | })
113 |
114 | local aws = AWS(config)
115 | aws.config.dry_run = true
116 | aws.config.ssl_verify = false
117 |
118 | local s3 = aws:S3()
119 |
120 | assert.same(type(s3.getObject), "function")
121 | local request, err = s3:getObject({
122 | Bucket = "test-bucket",
123 | Key = "test-key",
124 | })
125 |
126 | assert.same(err, nil)
127 | assert.same(request.ssl_verify, false)
128 | end)
129 |
130 | it("support configure timeout", function ()
131 | local config = {
132 | region = "us-east-1"
133 | }
134 |
135 | config.credentials = Credentials:new({
136 | accessKeyId = "teqst_id",
137 | secretAccessKey = "test_key",
138 | })
139 |
140 | local aws = AWS(config)
141 | aws.config.dry_run = true
142 | aws.config.timeout = 123456000
143 |
144 | local s3 = aws:S3()
145 |
146 | assert.same(type(s3.getObject), "function")
147 | local request, err = s3:getObject({
148 | Bucket = "test-bucket",
149 | Key = "test-key",
150 | })
151 |
152 | assert.same(err, nil)
153 | assert.same(request.timeout, 123456000)
154 | end)
155 |
156 | it("support configure keepalive idle timeout", function ()
157 | local config = {
158 | region = "us-east-1"
159 | }
160 |
161 | config.credentials = Credentials:new({
162 | accessKeyId = "teqst_id",
163 | secretAccessKey = "test_key",
164 | })
165 |
166 | local aws = AWS(config)
167 | aws.config.dry_run = true
168 | aws.config.keepalive_idle_timeout = 123456000
169 |
170 | local s3 = aws:S3()
171 |
172 | assert.same(type(s3.getObject), "function")
173 | local request, err = s3:getObject({
174 | Bucket = "test-bucket",
175 | Key = "test-key",
176 | })
177 |
178 | assert.same(err, nil)
179 | assert.same(request.keepalive_idle_timeout, 123456000)
180 | end)
181 |
182 | it("support set proxy options", function ()
183 | local config = {
184 | region = "us-east-1"
185 | }
186 |
187 | config.credentials = Credentials:new({
188 | accessKeyId = "teqst_id",
189 | secretAccessKey = "test_key",
190 | })
191 |
192 | local proxy_opts = {
193 | http_proxy = 'http://test-http-proxy:1234',
194 | https_proxy = 'http://test-https-proxy:4321',
195 | no_proxy = '127.0.0.1,localhost'
196 | }
197 |
198 | local aws = AWS(config)
199 | aws.config.dry_run = true
200 | aws.config.http_proxy = proxy_opts.http_proxy
201 | aws.config.https_proxy = proxy_opts.https_proxy
202 | aws.config.no_proxy = proxy_opts.no_proxy
203 |
204 | local s3 = aws:S3()
205 |
206 | assert.same(type(s3.getObject), "function")
207 | local request, _ = s3:getObject({
208 | Bucket = "test-bucket",
209 | Key = "test-key",
210 | })
211 |
212 | assert.same(type(request.proxy_opts), "table")
213 | for k, v in pairs(proxy_opts) do
214 | assert.same(request.proxy_opts[k], v)
215 | end
216 | end)
217 |
218 | it("decoded json body should have array metatable", function ()
219 | local config = {
220 | region = "us-east-1"
221 | }
222 |
223 | config.credentials = Credentials:new({
224 | accessKeyId = "teqst_id",
225 | secretAccessKey = "test_key",
226 | })
227 |
228 | local aws = AWS(config)
229 |
230 | local s3 = aws:S3()
231 |
232 | assert.same(type(s3.listBuckets), "function")
233 | local resp = s3:listBuckets()
234 |
235 | assert.is_not_nil(resp.body)
236 | assert.same([[{"ListAllMyBucketsResult":{"Buckets":[]}}]], cjson.encode(resp.body))
237 | end)
238 | end)
239 |
--------------------------------------------------------------------------------
/spec/02-requests/04-sign_v4_spec.lua:
--------------------------------------------------------------------------------
1 | pending("No tests for signing v4 yet", function()
2 |
3 | end)
4 |
--------------------------------------------------------------------------------
/spec/02-requests/05-presign_v4_spec.lua:
--------------------------------------------------------------------------------
1 | setmetatable(_G, nil)
2 |
3 | -- -- hock request sending
4 | -- package.loaded["resty.aws.request.execute"] = function(...)
5 | -- return ...
6 | -- end
7 |
8 | local AWS = require("resty.aws")
9 | local AWS_global_config = require("resty.aws.config").global
10 |
11 | local presign = require("resty.aws.request.signatures.presign")
12 |
13 | local config = AWS_global_config
14 | local aws = AWS(config)
15 |
16 |
17 | aws.config.credentials = aws:Credentials {
18 | accessKeyId = "test_id",
19 | secretAccessKey = "test_key",
20 | }
21 |
22 | aws.config.region = "test_region"
23 |
24 | describe("Presign request", function()
25 | local presigned_request_data
26 | local origin_time
27 |
28 | setup(function()
29 | origin_time = ngx.time
30 | ngx.time = function () --luacheck: ignore
31 | return 1667543171
32 | end
33 | end)
34 |
35 | teardown(function ()
36 | ngx.time = origin_time --luacheck: ignore
37 | end)
38 |
39 | before_each(function()
40 | local request_data = {
41 | method = "GET",
42 | scheme = "https",
43 | tls = true,
44 | host = "test_host",
45 | port = 443,
46 | path = "/",
47 | query = "Action=TestAction",
48 | headers = {
49 | ["Host"] = "test_host:443",
50 | },
51 | }
52 |
53 | presigned_request_data = presign(aws.config, request_data, "test_service", "test_region", 900)
54 | end)
55 |
56 | after_each(function()
57 | presigned_request_data = nil
58 | end)
59 |
60 | it("should have correct signed request host header", function()
61 | assert.same(presigned_request_data.headers["Host"], "test_host:443")
62 | assert.same(presigned_request_data.host, "test_host")
63 | assert.same(presigned_request_data.port, 443)
64 | end)
65 |
66 | it("should have correct signed request path", function ()
67 | assert.same(presigned_request_data.path, "/")
68 | end)
69 |
70 | it("should have correct signed query parameters", function ()
71 | local query_params = {}
72 | for k, v in presigned_request_data.query:gmatch("([^&=]+)=?([^&]*)") do
73 | query_params[ngx.unescape_uri(k)] = ngx.unescape_uri(v)
74 | end
75 | assert.same(query_params["X-Amz-Algorithm"], "AWS4-HMAC-SHA256")
76 | assert.same(query_params["Action"], "TestAction")
77 | assert.same(query_params["X-Amz-Date"], "20221104T062611Z")
78 | assert.same(query_params["X-Amz-Expires"], "900")
79 | assert.same(query_params["X-Amz-SignedHeaders"], "host")
80 | assert.same(query_params["X-Amz-Credential"], "test_id/20221104/test_region/test_service/aws4_request")
81 | end)
82 | end)
83 |
--------------------------------------------------------------------------------
/spec/03-credentials/01-Credentials_spec.lua:
--------------------------------------------------------------------------------
1 | local restore = require "spec.helpers"
2 |
3 | describe("Credentials base-class", function()
4 |
5 | local AWS, Credentials
6 |
7 | before_each(function()
8 | restore()
9 | local _ = require("resty.aws.config").global -- load config before anything else
10 | AWS = require "resty.aws"
11 | Credentials = require "resty.aws.credentials.Credentials"
12 | end)
13 |
14 | after_each(function()
15 | restore()
16 | end)
17 |
18 |
19 |
20 | describe("Class inheritance", function()
21 | local EnvironmentCredentials
22 |
23 | before_each(function()
24 | restore()
25 | restore.setenv("ABC_ACCESS_KEY_ID", "access")
26 | restore.setenv("ABC_SECRET_ACCESS_KEY", "secret")
27 | restore.setenv("ABC_SESSION_TOKEN", "token")
28 | local _ = require("resty.aws.config").global -- load config before anything else
29 |
30 | EnvironmentCredentials = require "resty.aws.credentials.EnvironmentCredentials"
31 | end)
32 |
33 |
34 |
35 | it("instance does not modify class", function()
36 | local cred = Credentials:new { expiryWindow = 20 }
37 | assert.equal(20, cred.expiryWindow)
38 | assert.Not.equal(20, Credentials.expiryWindow)
39 | end)
40 |
41 |
42 | it("sub-class instance does not modify class nor sub-class", function()
43 | local cred = EnvironmentCredentials:new { expiryWindow = 20, envPrefix = "ABC" }
44 |
45 | assert.equal(20, cred.expiryWindow)
46 | assert.Not.equal(20, Credentials.expiryWindow)
47 | assert.Not.equal(20, EnvironmentCredentials.expiryWindow)
48 |
49 | assert.equal("ABC", cred.envPrefix)
50 | assert.Not.equal("ABC", Credentials.envPrefix)
51 | assert.Not.equal("ABC", EnvironmentCredentials.envPrefix)
52 |
53 | cred:get() -- refreshes and sets values
54 | assert.is.equal("access", cred.accessKeyId)
55 | assert.is.Nil(Credentials.accessKeyId)
56 | assert.is.Nil(EnvironmentCredentials.accessKeyId)
57 | end)
58 |
59 | end)
60 |
61 |
62 |
63 | it("new() accepts credentials", function()
64 | local exp = ngx.now() + 60
65 | local cred = Credentials:new {
66 | accessKeyId = "access",
67 | secretAccessKey = "secret",
68 | sessionToken = "token",
69 | expireTime = exp
70 | }
71 | assert.is_false(cred:needsRefresh())
72 | assert.same({true, "access", "secret", "token", exp}, {cred:get()})
73 | end)
74 |
75 |
76 | it("instantiation from aws instance", function()
77 | local aws = AWS()
78 | local exp = ngx.now() + 60
79 | local cred = aws:Credentials({
80 | accessKeyId = "access",
81 | secretAccessKey = "secret",
82 | sessionToken = "token",
83 | expireTime = exp
84 | })
85 | assert.is_false(cred:needsRefresh())
86 | assert.same({true, "access", "secret", "token", exp}, {cred:get()})
87 | assert.equals(aws, cred.aws)
88 | end)
89 |
90 |
91 | it("new() expireTime defaults to 10 years if creds provided", function()
92 | local cred = Credentials:new {
93 | accessKeyId = "access",
94 | secretAccessKey = "secret",
95 | sessionToken = "token",
96 | }
97 | assert.is_false(cred:needsRefresh())
98 |
99 | local get = {cred:get()}
100 | assert.is.near(ngx.now() + 10*365*24*60*60, 30, get[5]) -- max delta = 30 seconds
101 |
102 | get[5] = nil
103 | assert.same({true, "access", "secret", "token"}, get)
104 | end)
105 |
106 |
107 | it("new() only setting expireTime defaults to 0", function()
108 | local cred = Credentials:new {
109 | expireTime = ngx.now() + 60
110 | }
111 | assert.is_true(cred:needsRefresh())
112 | end)
113 |
114 |
115 | it("needsRefresh()", function()
116 | local cred = Credentials:new()
117 | assert.is_true(cred:needsRefresh())
118 |
119 | cred:set(1,2,3,ngx.now() + 60*60)
120 | assert.is_false(cred:needsRefresh())
121 | end)
122 |
123 |
124 | it("needsRefresh() accounts for expiryWindow", function()
125 | local expWindow = 20
126 | local cred = Credentials:new { expiryWindow = expWindow }
127 |
128 | cred:set(1,2,3,ngx.now() + expWindow + 0.1)
129 | assert.is_false(cred:needsRefresh())
130 |
131 | cred:set(1,2,3,ngx.now() + expWindow - 0.1)
132 | assert.is_true(cred:needsRefresh())
133 | end)
134 |
135 |
136 | it("get() returns properties", function()
137 | local cred = Credentials:new()
138 | local exp = ngx.now() + 60
139 | cred:set(1,2,3,exp)
140 |
141 | assert.are.same({true, 1,2,3,exp}, {cred:get()})
142 | end)
143 |
144 |
145 | it("get() invokes refresh() when expired", function()
146 | local cred = Credentials:new()
147 |
148 | stub(cred, "refresh")
149 |
150 | cred:get()
151 | assert.stub(cred.refresh).was.called()
152 | end)
153 |
154 |
155 | it("set() sets properties", function()
156 | local cred = Credentials:new()
157 | local exp = ngx.now() + 60
158 | cred:set(1,2,3,exp)
159 |
160 | assert.are.same({true,1,2,3,exp}, {cred:get()})
161 | end)
162 |
163 |
164 | it("set() accepts rfc3339 dates for expireTime", function()
165 | local cred = Credentials:new()
166 | local exp = "2030-01-01T20:00:00Z"
167 |
168 | assert.has.no.error(function()
169 | cred:set(1,2,3,exp)
170 | end)
171 |
172 | local _, _, _, _, t = cred:get()
173 | assert.is.number(t)
174 | assert(ngx.now() < t)
175 | end)
176 |
177 | end)
178 |
--------------------------------------------------------------------------------
/spec/03-credentials/02-EC2MetadataCredentials_spec.lua:
--------------------------------------------------------------------------------
1 | local json = require("cjson.safe").new()
2 |
3 | require "resty.aws.config" -- load before mocking the http lib
4 |
5 | -- Mock for HTTP client
6 | local response = {} -- override in tests
7 |
8 | describe("EC2MetadataCredentials ~ v2", function()
9 | local http = {
10 | new = function()
11 | return {
12 | connect = function() return true end,
13 | close = function() return true end,
14 | set_timeout = function() return true end,
15 | set_timeouts = function() return true end,
16 | request = function(self, opts)
17 | if opts.path == "/latest/meta-data/iam/security-credentials/" then
18 | return { -- the response for requesting the role name
19 | status = 200,
20 | read_body = function() return "the_role_name" end,
21 | }
22 | elseif opts.path == "/latest/meta-data/iam/security-credentials/the_role_name" then
23 | return { -- the response for the credentials for the role
24 | status = (response or {}).status or 200,
25 | read_body = function() return json.encode {
26 | AccessKeyId = (response or {}).AccessKeyId or "access",
27 | SecretAccessKey = (response or {}).SecretAccessKey or "secret",
28 | Token = (response or {}).Token or "token",
29 | Expiration = (response or {}).Expiration or "2030-01-01T20:00:00Z",
30 | }
31 | end,
32 | }
33 | elseif opts.path == "/latest/api/token" and opts.method == "PUT" then
34 | return {
35 | status = 200,
36 | read_body = function() return "the_token" end,
37 | }
38 | else
39 | error("bad test path provided??? " .. tostring(opts.path))
40 | end
41 | end,
42 | }
43 | end,
44 | }
45 |
46 | local EC2MetadataCredentials
47 |
48 | setup(function()
49 | package.loaded["resty.luasocket.http"] = http
50 | end)
51 |
52 | teardown(function()
53 | package.loaded["resty.luasocket.http"] = nil
54 | end)
55 |
56 | before_each(function()
57 | package.loaded["resty.aws.credentials.EC2MetadataCredentials"] = nil
58 | EC2MetadataCredentials = require "resty.aws.credentials.EC2MetadataCredentials"
59 | end)
60 |
61 |
62 |
63 | it("fetches credentials", function()
64 | local cred = EC2MetadataCredentials:new()
65 | local success, key, secret, token = cred:get()
66 | assert.equal("access", key)
67 | assert.equal(true, success)
68 | assert.equal("secret", secret)
69 | assert.equal("token", token)
70 | end)
71 |
72 | end)
73 |
74 | describe("EC2MetadataCredentials ~ v1", function()
75 | local http = {
76 | new = function()
77 | return {
78 | connect = function() return true end,
79 | close = function() return true end,
80 | set_timeout = function() return true end,
81 | set_timeouts = function() return true end,
82 | request = function(self, opts)
83 | if opts.path == "/latest/meta-data/iam/security-credentials/" then
84 | return { -- the response for requesting the role name
85 | status = 200,
86 | read_body = function() return "the_role_name" end,
87 | }
88 | elseif opts.path == "/latest/meta-data/iam/security-credentials/the_role_name" then
89 | return { -- the response for the credentials for the role
90 | status = (response or {}).status or 200,
91 | read_body = function() return json.encode {
92 | AccessKeyId = (response or {}).AccessKeyId or "access",
93 | SecretAccessKey = (response or {}).SecretAccessKey or "secret",
94 | Token = (response or {}).Token or "token",
95 | Expiration = (response or {}).Expiration or "2030-01-01T20:00:00Z",
96 | }
97 | end,
98 | }
99 | elseif opts.path == "/latest/api/token" and opts.method == "PUT" then
100 | return {
101 | status = 400,
102 | read_body = function() return "fake" end,
103 | }
104 | else
105 | error("bad test path provided??? " .. tostring(opts.path))
106 | end
107 | end,
108 | }
109 | end,
110 | }
111 |
112 | local EC2MetadataCredentials
113 |
114 | setup(function()
115 | package.loaded["resty.luasocket.http"] = http
116 | end)
117 |
118 | teardown(function()
119 | package.loaded["resty.luasocket.http"] = nil
120 | end)
121 |
122 | before_each(function()
123 | package.loaded["resty.aws.credentials.EC2MetadataCredentials"] = nil
124 | EC2MetadataCredentials = require "resty.aws.credentials.EC2MetadataCredentials"
125 | end)
126 |
127 |
128 |
129 | it("fetches credentials", function()
130 | local cred = EC2MetadataCredentials:new()
131 | local success, key, secret, token = cred:get()
132 | assert.equal("access", key)
133 | assert.equal(true, success)
134 | assert.equal("secret", secret)
135 | assert.equal("token", token)
136 | end)
137 |
138 | end)
139 |
--------------------------------------------------------------------------------
/spec/03-credentials/03-EnvironmentCredentials_spec.lua:
--------------------------------------------------------------------------------
1 | local restore = require "spec.helpers"
2 |
3 | describe("EnvironmentCredentials", function()
4 |
5 | local EnvironmentCredentials
6 |
7 | before_each(function()
8 | restore()
9 | restore.setenv("ABC_ACCESS_KEY_ID", "access")
10 | restore.setenv("ABC_SECRET_ACCESS_KEY", "secret")
11 | restore.setenv("ABC_SESSION_TOKEN", "token")
12 | local _ = require("resty.aws.config").global -- load config before anything else
13 |
14 | EnvironmentCredentials = require "resty.aws.credentials.EnvironmentCredentials"
15 | end)
16 |
17 | after_each(function()
18 | restore()
19 | end)
20 |
21 |
22 |
23 | it("gets environment variables", function()
24 | local cred = EnvironmentCredentials:new { envPrefix = "ABC" }
25 | assert.is_false(cred:needsRefresh()) -- false; because we fetch upon instanciation
26 |
27 | local get = {cred:get()}
28 | assert.is.near(ngx.now() + 10*365*24*60*60, 30, get[5]) -- max delta = 30 seconds
29 |
30 | get[5] = nil
31 | assert.same({true, "access", "secret", "token"}, get)
32 | end)
33 |
34 | end)
35 |
--------------------------------------------------------------------------------
/spec/03-credentials/04-RemoteCredentials_spec.lua:
--------------------------------------------------------------------------------
1 | local json = require("cjson.safe").new()
2 | local restore = require "spec.helpers"
3 |
4 | local old_pl_utils = require("pl.utils")
5 |
6 | -- Mock for HTTP client
7 | local response = {} -- override in tests
8 | local http_records = {} -- record requests for assertions
9 | local http = {
10 | new = function()
11 | return {
12 | connect = function() return true end,
13 | set_timeout = function() return true end,
14 | set_timeouts = function() return true end,
15 | request = function(self, opts)
16 | if opts.path == "/test/path" then
17 | table.insert(http_records, opts)
18 | return { -- the response for the credentials
19 | status = (response or {}).status or 200,
20 | headers = opts and opts.headers or {},
21 | read_body = function() return json.encode {
22 | AccessKeyId = (response or {}).AccessKeyId or "access",
23 | SecretAccessKey = (response or {}).SecretAccessKey or "secret",
24 | Token = (response or {}).Token or "token",
25 | Expiration = (response or {}).Expiration or "2030-01-01T20:00:00Z",
26 | }
27 | end,
28 | }
29 | else
30 | error("bad test path provided??? " .. tostring(opts.path))
31 | end
32 | end,
33 | }
34 | end,
35 | }
36 |
37 |
38 | describe("RemoteCredentials", function()
39 |
40 | local RemoteCredentials
41 | local pl_utils_readfile = old_pl_utils.readfile
42 |
43 | before_each(function()
44 | pl_utils_readfile = old_pl_utils.readfile
45 | old_pl_utils.readfile = function()
46 | return "testtokenabc123"
47 | end
48 | restore()
49 | restore.setenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", "https://localhost/test/path")
50 |
51 | local _ = require("resty.aws.config").global -- load config before mocking http client
52 | package.loaded["resty.luasocket.http"] = http
53 |
54 | RemoteCredentials = require "resty.aws.credentials.RemoteCredentials"
55 | end)
56 |
57 | after_each(function()
58 | old_pl_utils.readfile = pl_utils_readfile
59 | restore()
60 | end)
61 |
62 |
63 | it("fetches credentials", function()
64 | local cred = RemoteCredentials:new()
65 | local success, key, secret, token = cred:get()
66 | assert.equal(true, success)
67 | assert.equal("access", key)
68 | assert.equal("secret", secret)
69 | assert.equal("token", token)
70 | end)
71 |
72 | end)
73 |
74 |
75 | describe("RemoteCredentials with customized full URI", function ()
76 | it("fetches credentials", function ()
77 | local RemoteCredentials
78 |
79 | restore()
80 | restore.setenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", "http://localhost:12345/test/path")
81 |
82 | local _ = require("resty.aws.config").global -- load config before mocking http client
83 | package.loaded["resty.luasocket.http"] = http
84 |
85 | RemoteCredentials = require "resty.aws.credentials.RemoteCredentials"
86 | finally(function()
87 | restore()
88 | end)
89 |
90 | local cred = RemoteCredentials:new()
91 | local success, key, secret, token = cred:get()
92 | assert.equal(true, success)
93 | assert.equal("access", key)
94 | assert.equal("secret", secret)
95 | assert.equal("token", token)
96 | end)
97 | end)
98 |
99 | describe("RemoteCredentials with full URI and token file", function ()
100 | local pl_utils_readfile
101 | before_each(function()
102 | pl_utils_readfile = old_pl_utils.readfile
103 | old_pl_utils.readfile = function()
104 | return "testtokenabc123"
105 | end
106 | end)
107 | after_each(function()
108 | old_pl_utils.readfile = pl_utils_readfile
109 | end)
110 | it("fetches credentials", function ()
111 | local RemoteCredentials
112 |
113 | restore()
114 | restore.setenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", "http://localhost:12345/test/path")
115 | restore.setenv("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE", "/var/run/secrets/pods.eks.amazonaws.com/serviceaccount/eks-pod-identity-token")
116 |
117 | local _ = require("resty.aws.config").global -- load config before mocking http client
118 | package.loaded["resty.luasocket.http"] = http
119 |
120 | RemoteCredentials = require "resty.aws.credentials.RemoteCredentials"
121 | finally(function()
122 | restore()
123 | end)
124 |
125 | local cred = RemoteCredentials:new()
126 | local success, key, secret, token = cred:get()
127 | assert.equal(true, success)
128 | assert.equal("access", key)
129 | assert.equal("secret", secret)
130 | assert.equal("token", token)
131 |
132 | assert.not_nil(http_records[#http_records].headers)
133 | assert.equal(http_records[#http_records].headers["Authorization"], "testtokenabc123")
134 | end)
135 | end)
136 |
137 | describe("RemoteCredentials with full URI and token and token file, file takes higher precedence", function ()
138 | local pl_utils_readfile
139 | before_each(function()
140 | pl_utils_readfile = old_pl_utils.readfile
141 | old_pl_utils.readfile = function()
142 | return "testtokenabc123"
143 | end
144 | end)
145 | after_each(function()
146 | old_pl_utils.readfile = pl_utils_readfile
147 | end)
148 | it("fetches credentials", function ()
149 | local RemoteCredentials
150 |
151 | restore()
152 | restore.setenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", "http://localhost:12345/test/path")
153 | restore.setenv("AWS_CONTAINER_AUTHORIZATION_TOKEN", "testtoken")
154 | restore.setenv("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE", "/var/run/secrets/pods.eks.amazonaws.com/serviceaccount/eks-pod-identity-token")
155 |
156 | local _ = require("resty.aws.config").global -- load config before mocking http client
157 | package.loaded["resty.luasocket.http"] = http
158 |
159 | RemoteCredentials = require "resty.aws.credentials.RemoteCredentials"
160 | finally(function()
161 | restore()
162 | end)
163 |
164 | local cred = RemoteCredentials:new()
165 | local success, key, secret, token = cred:get()
166 | assert.equal(true, success)
167 | assert.equal("access", key)
168 | assert.equal("secret", secret)
169 | assert.equal("token", token)
170 |
171 | assert.not_nil(http_records[#http_records].headers)
172 | assert.equal(http_records[#http_records].headers["Authorization"], "testtokenabc123")
173 | end)
174 | end)
175 |
176 |
--------------------------------------------------------------------------------
/spec/03-credentials/05-CredentialProviderChain_spec.lua:
--------------------------------------------------------------------------------
1 | local restore = require "spec.helpers"
2 |
3 | describe("CredentialProviderChain", function()
4 |
5 | local CredentialProviderChain
6 |
7 | before_each(function()
8 | restore()
9 | restore.setenv("ABC_ACCESS_KEY_ID", "access-1")
10 | restore.setenv("ABC_SECRET_ACCESS_KEY", "secret-1")
11 | restore.setenv("ABC_SESSION_TOKEN", "token-1")
12 | local _ = require("resty.aws.config").global -- load config before anything else
13 |
14 | CredentialProviderChain = require "resty.aws.credentials.CredentialProviderChain"
15 | end)
16 |
17 | after_each(function()
18 | restore()
19 | end)
20 |
21 |
22 |
23 | it("gets environment variables which are first", function()
24 | local cred = CredentialProviderChain:new {
25 | providers = {
26 | require("resty.aws.credentials.EnvironmentCredentials"):new { envPrefix = "ABC" },
27 | require("resty.aws.credentials.EnvironmentCredentials"):new { envPrefix = "AWS" },
28 | require("resty.aws.credentials.Credentials"):new {
29 | accessKeyId = "access-2",
30 | secretAccessKey = "secret-2",
31 | sessionToken = "token-2",
32 | },
33 | }
34 | }
35 | assert.is_true(cred:needsRefresh())
36 |
37 | local get = {cred:get()}
38 | get[5] = nil -- drop the expireTime
39 |
40 | assert.same({true, "access-1", "secret-1", "token-1"}, get)
41 | end)
42 |
43 |
44 | it("gets plain credentials which are last", function()
45 | -- clear env vars such that the first 2 providers both fail
46 | restore.setenv("ABC_ACCESS_KEY_ID", nil)
47 | restore.setenv("ABC_SECRET_ACCESS_KEY", nil)
48 | restore.setenv("ABC_SESSION_TOKEN", nil)
49 |
50 | local cred = CredentialProviderChain:new {
51 | providers = {
52 | require("resty.aws.credentials.EnvironmentCredentials"):new { envPrefix = "ABC" },
53 | require("resty.aws.credentials.EnvironmentCredentials"):new { envPrefix = "AWS" },
54 | require("resty.aws.credentials.Credentials"):new {
55 | accessKeyId = "access-2",
56 | secretAccessKey = "secret-2",
57 | sessionToken = "token-2",
58 | },
59 | }
60 | }
61 | assert.is_true(cred:needsRefresh())
62 |
63 | local get = {cred:get()}
64 | get[5] = nil -- drop the expireTime
65 |
66 | assert.same({true, "access-2", "secret-2", "token-2"}, get)
67 | end)
68 |
69 |
70 | it("gets default providers if not specified", function()
71 | local cred = CredentialProviderChain:new()
72 | assert.is.not_nil(cred.providers[1])
73 | end)
74 |
75 | end)
76 |
--------------------------------------------------------------------------------
/spec/03-credentials/06-ChainableTemporaryCredentials_spec.lua:
--------------------------------------------------------------------------------
1 |
2 | describe("ChainableTemporaryCredentials", function()
3 |
4 | local AWS = require "resty.aws"
5 | local ChainableTemporaryCredentials = require "resty.aws.credentials.ChainableTemporaryCredentials"
6 |
7 | setup(function()
8 | --setup
9 | end)
10 |
11 | teardown(function()
12 | --teardown
13 | end)
14 |
15 | describe("new()", function()
16 |
17 | it("creates a new instance when providing AWS-instance", function()
18 | local aws = AWS()
19 | local params = {}
20 | local creds
21 | assert.has.no.error(function()
22 | creds = assert(ChainableTemporaryCredentials:new {
23 | params = params,
24 | aws = aws,
25 | })
26 | end)
27 | assert.is_function(creds.sts.assumeRole)
28 | assert.are.equal(aws.config.credentials, creds.masterCredentials)
29 | assert.are.equal(params, creds.params)
30 | end)
31 |
32 |
33 | it("accepts params as a single entry array", function()
34 | local aws = AWS()
35 | local params = {}
36 | local creds
37 | assert.has.no.error(function()
38 | creds = assert(ChainableTemporaryCredentials:new {
39 | params = { params },
40 | aws = aws,
41 | })
42 | end)
43 | assert.is_function(creds.sts.assumeRole)
44 | assert.are.equal(aws.config.credentials, creds.masterCredentials)
45 | assert.are.equal(params, creds.params)
46 | end)
47 |
48 |
49 | it("creates a new instance when providing STS-instance", function()
50 | local sts = AWS():STS()
51 | local params = {}
52 | local creds
53 | assert.has.no.error(function()
54 | creds = assert(ChainableTemporaryCredentials:new {
55 | params = params,
56 | sts = sts,
57 | })
58 | end)
59 | assert.is_function(creds.sts.assumeRole)
60 | assert.are.equal(sts.config.credentials, creds.masterCredentials)
61 | assert.are.equal(params, creds.params)
62 | end)
63 |
64 |
65 | it("creates chained credentials from params-array", function()
66 | local aws = AWS()
67 | local params_start, params_middle, params_final = {}, {}, {}
68 | local creds_final
69 | assert.has.no.error(function()
70 | creds_final = assert(ChainableTemporaryCredentials:new {
71 | params = { params_start, params_middle, params_final },
72 | aws = aws,
73 | })
74 | end)
75 |
76 | assert.are.equal(params_final, creds_final.params)
77 |
78 | local creds_middle = creds_final.masterCredentials
79 | assert.are.equal(params_middle, creds_middle.params)
80 |
81 | local creds_start = creds_middle.masterCredentials
82 | assert.are.equal(params_start, creds_start.params)
83 |
84 | assert.are.equal(aws.config.credentials, creds_start.masterCredentials)
85 |
86 | end)
87 | end)
88 |
89 | end)
90 |
--------------------------------------------------------------------------------
/spec/03-credentials/07-TokenFileWebIdentityCredentials_spec.lua:
--------------------------------------------------------------------------------
1 | local restore = require "spec.helpers"
2 |
3 | describe("TokenFileWebIdentityCredentials", function()
4 |
5 | local TokenFileWebIdentityCredentials
6 |
7 | before_each(function()
8 | restore()
9 | restore.setenv("AWS_ROLE_ARN", "arn:abc123")
10 | restore.setenv("AWS_WEB_IDENTITY_TOKEN_FILE", "/some/file")
11 | local _ = require("resty.aws.config").global -- load config before anything else
12 |
13 | TokenFileWebIdentityCredentials = require "resty.aws.credentials.TokenFileWebIdentityCredentials"
14 | end)
15 |
16 | after_each(function()
17 | restore()
18 | end)
19 |
20 |
21 |
22 | it("gets the relevant environment variables", function()
23 | local cred = TokenFileWebIdentityCredentials:new()
24 | assert.is_true(cred:needsRefresh()) -- true; because we only get env vars
25 |
26 | assert.same("arn:abc123", cred.role_arn)
27 | assert.same("/some/file", cred.token_file)
28 | assert.same("session@lua-resty-aws", cred.session_name)
29 | end)
30 |
31 | it("options override defaults", function()
32 | local cred = TokenFileWebIdentityCredentials:new {
33 | token_file = "another/file",
34 | role_arn = "another arn",
35 | session_name = "i like sessions",
36 | }
37 | assert.same("another arn", cred.role_arn)
38 | assert.same("another/file", cred.token_file)
39 | assert.same("i like sessions", cred.session_name)
40 | end)
41 |
42 | end)
43 |
--------------------------------------------------------------------------------
/spec/03-credentials/08-SharedFileCredentials_spec.lua:
--------------------------------------------------------------------------------
1 | local pl_path = require "pl.path"
2 | local pl_config = require "pl.config"
3 | local tbl_clear = require "table.clear"
4 | local restore = require "spec.helpers"
5 |
6 | local hooked_file = {}
7 |
8 | local origin_read = pl_config.read
9 | local origin_isfile = pl_path.isfile
10 |
11 | pl_config.read = function(name, ...)
12 | return hooked_file[name] or origin_read(name, ...)
13 | end
14 |
15 | pl_path.isfile = function(name)
16 | return hooked_file[name] and true or origin_isfile(name)
17 | end
18 |
19 | local function hook_config_file(name, content)
20 | hooked_file[name] = content
21 | end
22 |
23 | describe("SharedFileCredentials_spec", function()
24 |
25 | local SharedFileCredentials_spec
26 |
27 | before_each(function()
28 | -- make ci happy
29 | restore.setenv("HOME", "/home/ci-test")
30 | local _ = require("resty.aws.config").global -- load config before anything else
31 |
32 | SharedFileCredentials_spec = require "resty.aws.credentials.SharedFileCredentials"
33 | end)
34 |
35 | after_each(function()
36 | restore()
37 | tbl_clear(hooked_file)
38 | end)
39 |
40 |
41 | it("basical sanity", function()
42 | local cred = SharedFileCredentials_spec:new {}
43 | assert(cred:needsRefresh()) -- true; because we has no file
44 | end)
45 |
46 | it("gets from config", function()
47 | hook_config_file(pl_path.expanduser("~/.aws/config"), {
48 | default = {
49 | aws_access_key_id = "access",
50 | aws_secret_access_key = "secret",
51 | aws_session_token = "token",
52 | }
53 | })
54 | local cred = SharedFileCredentials_spec:new {}
55 | assert.is_false(cred:needsRefresh()) -- false; because we fetch upon instanciation
56 |
57 | local get = {cred:get()}
58 | assert.is.near(ngx.now() + 10*365*24*60*60, 30, get[5]) -- max delta = 30 seconds
59 |
60 | get[5] = nil
61 | assert.same({true, "access", "secret", "token"}, get)
62 | end)
63 |
64 | it("gets from credentials", function()
65 | hook_config_file(pl_path.expanduser("~/.aws/credentials"), {
66 | default = {
67 | aws_access_key_id = "access",
68 | aws_secret_access_key = "secret",
69 | aws_session_token = "token",
70 | }
71 | })
72 |
73 | local cred = SharedFileCredentials_spec:new {}
74 | assert.is_false(cred:needsRefresh()) -- false; because we fetch upon instanciation
75 |
76 | local get = {cred:get()}
77 | assert.is.near(ngx.now() + 10*365*24*60*60, 30, get[5]) -- max delta = 30 seconds
78 |
79 | get[5] = nil
80 | assert.same({true, "access", "secret", "token"}, get)
81 | end)
82 |
83 | end)
84 |
--------------------------------------------------------------------------------
/spec/04-services/01-secret_manager.lua:
--------------------------------------------------------------------------------
1 | setmetatable(_G, nil)
2 |
3 | -- hook request sending
4 | package.loaded["resty.aws.request.execute"] = function(...)
5 | return ...
6 | end
7 |
8 | local AWS = require("resty.aws")
9 | local AWS_global_config = require("resty.aws.config").global
10 |
11 |
12 | local config = AWS_global_config
13 | config.tls = true
14 | local aws = AWS(config)
15 |
16 |
17 | aws.config.credentials = aws:Credentials {
18 | accessKeyId = "test_id",
19 | secretAccessKey = "test_key",
20 | }
21 |
22 | aws.config.region = "test_region"
23 |
24 | describe("Secret Manager service", function()
25 | local sm
26 | local origin_time
27 |
28 | setup(function()
29 | origin_time = ngx.time
30 | ngx.time = function () --luacheck: ignore
31 | return 1667543171
32 | end
33 | end)
34 |
35 | teardown(function ()
36 | ngx.time = origin_time --luacheck: ignore
37 | end)
38 |
39 | before_each(function()
40 | sm = assert(aws:SecretsManager {})
41 | end)
42 |
43 | after_each(function()
44 | end)
45 |
46 | local testcases = {
47 | -- API = { param, expected_result_aws, },
48 | getSecretValue = {
49 | {
50 | SecretId = "test_id",
51 | VersionStage = "AWSCURRENT",
52 | },
53 | {
54 | ['body'] = '{"VersionStage":"AWSCURRENT","SecretId":"test_id"}',
55 | ['headers'] = {
56 | ['Authorization'] = 'AWS4-HMAC-SHA256 Credential=test_id/20221104/test_region/secretsmanager/aws4_request, SignedHeaders=content-length;content-type;host;x-amz-date;x-amz-target, Signature=81618df993cf58510f22d95efb815d03a9bd3cfb7af0ef766e6980f7a99799ff',
57 | ['Content-Length'] = 50,
58 | ['Content-Type'] = 'application/x-amz-json-1.1',
59 | ['Host'] = 'secretsmanager.test_region.amazonaws.com',
60 | ['X-Amz-Date'] = '20221104T062611Z',
61 | ['X-Amz-Target'] = 'secretsmanager.GetSecretValue'
62 | },
63 | ['host'] = 'secretsmanager.test_region.amazonaws.com',
64 | ['method'] = 'POST',
65 | ['path'] = '/',
66 | ['port'] = 443,
67 | ['query'] = {},
68 | ['tls'] = true,
69 | },
70 | },
71 | }
72 |
73 | for api, test in pairs(testcases) do
74 | it("SecretsManager:" .. api, function()
75 | local param = test[1]
76 | local expected_result_aws = test[2]
77 |
78 | local result_aws = assert(sm[api](sm, param))
79 |
80 | assert.same(expected_result_aws, result_aws)
81 | end)
82 | end
83 | end)
84 |
--------------------------------------------------------------------------------
/spec/04-services/02-s3.lua:
--------------------------------------------------------------------------------
1 | setmetatable(_G, nil)
2 |
3 | -- hock request sending
4 | package.loaded["resty.aws.request.execute"] = function(...)
5 | return ...
6 | end
7 |
8 | local AWS = require("resty.aws")
9 | local AWS_global_config = require("resty.aws.config").global
10 |
11 |
12 | local config = AWS_global_config
13 | config.tls = true
14 | local aws = AWS(config)
15 |
16 |
17 | aws.config.credentials = aws:Credentials {
18 | accessKeyId = "test_id",
19 | secretAccessKey = "test_key",
20 | }
21 |
22 | aws.config.region = "test_region"
23 |
24 | describe("S3 service", function()
25 | local s3, s3_3rd
26 | local origin_time
27 |
28 | setup(function()
29 | origin_time = ngx.time
30 | ngx.time = function () --luacheck: ignore
31 | return 1667543171
32 | end
33 | end)
34 |
35 | teardown(function ()
36 | ngx.time = origin_time --luacheck: ignore
37 | end)
38 |
39 | before_each(function()
40 | s3 = assert(aws:S3 {})
41 | s3_3rd = assert(aws:S3 {
42 | scheme = "http",
43 | endpoint = "testendpoint.com",
44 | port = 443,
45 | tls = false,
46 | })
47 | end)
48 |
49 | after_each(function()
50 | end)
51 |
52 | local testcases = {
53 | -- API = { param, expected_result_aws, expected_result_3rd_patry, },
54 | putObject = {
55 | {
56 | Bucket = "testbucket",
57 | Key = "testkey",
58 | Body = "testbody",
59 | Metadata = {
60 | test = "test",
61 | }
62 | },
63 | {
64 | body = 'testbody',
65 | headers = {
66 | ['Authorization'] = 'AWS4-HMAC-SHA256 Credential=test_id/20221104/test_region/s3/aws4_request, SignedHeaders=content-length;host;x-amz-content-sha256;x-amz-date;x-amz-meta-test, Signature=57e7e544e5bce7cdf6321768d7577212a874a3504031fba4bb97ab2e5245532f',
67 | ['Content-Length'] = 8,
68 | ['Host'] = 'testbucket.s3.test_region.amazonaws.com',
69 | ['X-Amz-Content-Sha256'] = '2417e54e58ac3752d4d82355e13053e0b3d9601d09d4fd5027be26da405b8ccd',
70 | ['X-Amz-Date'] = '20221104T062611Z',
71 | ['X-Amz-Meta-Test'] = 'test',
72 | },
73 | host = 'testbucket.s3.test_region.amazonaws.com',
74 | method = 'PUT',
75 | path = '/testkey',
76 | port = 443,
77 | query = {},
78 | tls = true,
79 | },
80 | {
81 | body = 'testbody',
82 | headers = {
83 | ['Authorization'] = 'AWS4-HMAC-SHA256 Credential=test_id/20221104/test_region/s3/aws4_request, SignedHeaders=content-length;host;x-amz-content-sha256;x-amz-date;x-amz-meta-test, Signature=c821cc6d135ee1abe2efd235d7a8f699fbaa90e979584cc274f1ea1610679f86',
84 | ['Content-Length'] = 8,
85 | ['Host'] = 'testbucket.testendpoint.com',
86 | ['X-Amz-Content-Sha256'] = '2417e54e58ac3752d4d82355e13053e0b3d9601d09d4fd5027be26da405b8ccd',
87 | ['X-Amz-Date'] = '20221104T062611Z',
88 | ['X-Amz-Meta-Test'] = 'test',
89 | },
90 | host = 'testbucket.testendpoint.com',
91 | method = 'PUT',
92 | path = '/testkey',
93 | port = 443,
94 | query = {},
95 | tls = false,
96 | },
97 | },
98 | getObject = {
99 | {
100 | Bucket = "testbucket",
101 | Key = "testkey",
102 | },
103 | {
104 | ['headers'] = {
105 | ['Authorization'] = 'AWS4-HMAC-SHA256 Credential=test_id/20221104/test_region/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=a1cab7c5a3e2ec70af4acaed2dd5382842080af7dbe8f4416540cc99357b322b',
106 | ['Host'] = 'testbucket.s3.test_region.amazonaws.com',
107 | ['X-Amz-Content-Sha256'] = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
108 | ['X-Amz-Date'] = '20221104T062611Z'
109 | },
110 | ['host'] = 'testbucket.s3.test_region.amazonaws.com',
111 | ['method'] = 'GET',
112 | ['path'] = '/testkey',
113 | ['port'] = 443,
114 | ['query'] = {},
115 | ['tls'] = true
116 | },
117 | {
118 | ['headers'] = {
119 | ['Authorization'] = 'AWS4-HMAC-SHA256 Credential=test_id/20221104/test_region/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=f0ba4ea255b0678c5e9339e44a976e3f6547bddfaf5dfe5a86403dc97d891010',
120 | ['Host'] = 'testbucket.testendpoint.com',
121 | ['X-Amz-Content-Sha256'] = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
122 | ['X-Amz-Date'] = '20221104T062611Z'
123 | },
124 | ['host'] = 'testbucket.testendpoint.com',
125 | ['method'] = 'GET',
126 | ['path'] = '/testkey',
127 | ['port'] = 443,
128 | ['query'] = {},
129 | ['tls'] = false,
130 | }
131 | },
132 | getBucketAcl = {
133 | {
134 | Bucket = "testbucket",
135 | },
136 | {
137 | ['headers'] = {
138 | ['Authorization'] = 'AWS4-HMAC-SHA256 Credential=test_id/20221104/test_region/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=69eb892e8f3cae5b3f777c31e4318d946ce0ebba97f8539a5064e1709d8477c6',
139 | ['Host'] = 'testbucket.s3.test_region.amazonaws.com',
140 | ['X-Amz-Content-Sha256'] = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
141 | ['X-Amz-Date'] = '20221104T062611Z'
142 | },
143 | ['host'] = 'testbucket.s3.test_region.amazonaws.com',
144 | ['method'] = 'GET',
145 | ['path'] = '',
146 | ['port'] = 443,
147 | ['query'] = {
148 | ['acl'] = ''
149 | },
150 | ['tls'] = true,
151 | },
152 | {
153 | ['headers'] = {
154 | ['Authorization'] = 'AWS4-HMAC-SHA256 Credential=test_id/20221104/test_region/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=0aac6b456c28cd393ca06a779074c4797155338569fad6f3e95ea348406b16a9',
155 | ['Host'] = 'testbucket.testendpoint.com',
156 | ['X-Amz-Content-Sha256'] = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
157 | ['X-Amz-Date'] = '20221104T062611Z'
158 | },
159 | ['host'] = 'testbucket.testendpoint.com',
160 | ['method'] = 'GET',
161 | ['path'] = '',
162 | ['port'] = 443,
163 | ['query'] = {
164 | ['acl'] = ''
165 | },
166 | ['tls'] = false,
167 | },
168 | },
169 | }
170 |
171 |
172 | for api, test in pairs(testcases) do
173 | it("s3:" .. api, function()
174 | local param = test[1]
175 | local expected_result_aws = test[2]
176 | local expected_result_3rd_patry = test[3]
177 |
178 | local result_aws = assert(s3[api](s3, param))
179 | local result_3rd_patry = assert(s3_3rd[api](s3_3rd, param))
180 |
181 | assert.same(expected_result_aws, result_aws)
182 | assert.same(expected_result_3rd_patry, result_3rd_patry)
183 | end)
184 | end
185 | end)
186 |
--------------------------------------------------------------------------------
/spec/04-services/03-s3_compat_api.lua:
--------------------------------------------------------------------------------
1 | setmetatable(_G, nil)
2 |
3 | -- hock request sending
4 | package.loaded["resty.aws.request.execute"] = function(...)
5 | return ...
6 | end
7 |
8 | local AWS = require("resty.aws")
9 | local AWS_global_config = require("resty.aws.config").global
10 |
11 |
12 | local config = AWS_global_config
13 | config.tls = true
14 | -- old API format
15 | config.s3_bucket_in_path = true
16 | local aws = AWS(config)
17 |
18 |
19 | aws.config.credentials = aws:Credentials {
20 | accessKeyId = "test_id",
21 | secretAccessKey = "test_key",
22 | }
23 |
24 | aws.config.region = "test_region"
25 |
26 | describe("S3 service", function()
27 | local s3, s3_3rd
28 | local origin_time
29 |
30 | setup(function()
31 | origin_time = ngx.time
32 | ngx.time = function() --luacheck: ignore
33 | return 1667543171
34 | end
35 | end)
36 |
37 | teardown(function ()
38 | ngx.time = origin_time --luacheck: ignore
39 | end)
40 |
41 | before_each(function()
42 | s3 = assert(aws:S3 {})
43 | s3_3rd = assert(aws:S3 {
44 | scheme = "http",
45 | endpoint = "testendpoint.com",
46 | port = 443,
47 | tls = false,
48 | })
49 | end)
50 |
51 | after_each(function()
52 | end)
53 |
54 | local testcases = {
55 | -- API = { param, expected_result_aws, expected_result_3rd_patry, },
56 | putObject = {
57 | {
58 | Bucket = "testbucket",
59 | Key = "testkey",
60 | Body = "testbody",
61 | Metadata = {
62 | test = "test",
63 | }
64 | },
65 | {
66 | body = 'testbody',
67 | headers = {
68 | ['Authorization'] = 'AWS4-HMAC-SHA256 Credential=test_id/20221104/test_region/s3/aws4_request, SignedHeaders=content-length;host;x-amz-content-sha256;x-amz-date;x-amz-meta-test, Signature=5d3c4b53bfcecfba7b9c76637f64e832ad35af583d1098df6eabc1b98a5f4c4f',
69 | ['Content-Length'] = 8,
70 | ['Host'] = 's3.test_region.amazonaws.com',
71 | ['X-Amz-Content-Sha256'] = '2417e54e58ac3752d4d82355e13053e0b3d9601d09d4fd5027be26da405b8ccd',
72 | ['X-Amz-Date'] = '20221104T062611Z',
73 | ['X-Amz-Meta-Test'] = 'test',
74 | },
75 | host = 's3.test_region.amazonaws.com',
76 | method = 'PUT',
77 | path = '/testbucket/testkey',
78 | port = 443,
79 | query = {},
80 | tls = true,
81 | },
82 | {
83 | body = 'testbody',
84 | headers = {
85 | ['Authorization'] = 'AWS4-HMAC-SHA256 Credential=test_id/20221104/test_region/s3/aws4_request, SignedHeaders=content-length;host;x-amz-content-sha256;x-amz-date;x-amz-meta-test, Signature=3536e1dcf26a23b78a5345df54ef1f86c796f13251c7fdaa23d78f6d67aeb0be',
86 | ['Content-Length'] = 8,
87 | ['Host'] = 'testendpoint.com',
88 | ['X-Amz-Content-Sha256'] = '2417e54e58ac3752d4d82355e13053e0b3d9601d09d4fd5027be26da405b8ccd',
89 | ['X-Amz-Date'] = '20221104T062611Z',
90 | ['X-Amz-Meta-Test'] = 'test',
91 | },
92 | host = 'testendpoint.com',
93 | method = 'PUT',
94 | path = '/testbucket/testkey',
95 | port = 443,
96 | query = {},
97 | tls = false,
98 | },
99 | },
100 | getObject = {
101 | {
102 | Bucket = "testbucket",
103 | Key = "testkey",
104 | },
105 | {
106 | ['headers'] = {
107 | ['Authorization'] = 'AWS4-HMAC-SHA256 Credential=test_id/20221104/test_region/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=66fc85f53ba4ff3665e1c575c882216b6489442e1bc2822d73b2f43949cca0cf',
108 | ['Host'] = 's3.test_region.amazonaws.com',
109 | ['X-Amz-Content-Sha256'] = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
110 | ['X-Amz-Date'] = '20221104T062611Z'
111 | },
112 | ['host'] = 's3.test_region.amazonaws.com',
113 | ['method'] = 'GET',
114 | ['path'] = '/testbucket/testkey',
115 | ['port'] = 443,
116 | ['query'] = {},
117 | ['tls'] = true
118 | },
119 | {
120 | ['headers'] = {
121 | ['Authorization'] = 'AWS4-HMAC-SHA256 Credential=test_id/20221104/test_region/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=67a06a5f1634a5e9598d576636ef7d6d77a6fe7f07a0e1d5f66df80a3c47e9f4',
122 | ['Host'] = 'testendpoint.com',
123 | ['X-Amz-Content-Sha256'] = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
124 | ['X-Amz-Date'] = '20221104T062611Z'
125 | },
126 | ['host'] = 'testendpoint.com',
127 | ['method'] = 'GET',
128 | ['path'] = '/testbucket/testkey',
129 | ['port'] = 443,
130 | ['query'] = {},
131 | ['tls'] = false,
132 | }
133 | },
134 | getBucketAcl = {
135 | {
136 | Bucket = "testbucket",
137 | },
138 | {
139 | ['headers'] = {
140 | ['Authorization'] = 'AWS4-HMAC-SHA256 Credential=test_id/20221104/test_region/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=19bf55ee1b8db1c5099c8e40cf96881479e47cb2e7f08d191f31733f0335d38d',
141 | ['Host'] = 's3.test_region.amazonaws.com',
142 | ['X-Amz-Content-Sha256'] = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
143 | ['X-Amz-Date'] = '20221104T062611Z'
144 | },
145 | ['host'] = 's3.test_region.amazonaws.com',
146 | ['method'] = 'GET',
147 | ['path'] = '/testbucket',
148 | ['port'] = 443,
149 | ['query'] = {
150 | ['acl'] = ''
151 | },
152 | ['tls'] = true,
153 | },
154 | {
155 | ['headers'] = {
156 | ['Authorization'] = 'AWS4-HMAC-SHA256 Credential=test_id/20221104/test_region/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=8439d7f57d8de5b9ecb6a578983b8e5fb6722ca375530753d4a6f498d2c4194e',
157 | ['Host'] = 'testendpoint.com',
158 | ['X-Amz-Content-Sha256'] = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
159 | ['X-Amz-Date'] = '20221104T062611Z'
160 | },
161 | ['host'] = 'testendpoint.com',
162 | ['method'] = 'GET',
163 | ['path'] = '/testbucket',
164 | ['port'] = 443,
165 | ['query'] = {
166 | ['acl'] = ''
167 | },
168 | ['tls'] = false,
169 | },
170 | },
171 | }
172 |
173 |
174 | for api, test in pairs(testcases) do
175 | it("s3:" .. api, function()
176 | local param = test[1]
177 | local expected_result_aws = test[2]
178 | local expected_result_3rd_patry = test[3]
179 |
180 | local result_aws = assert(s3[api](s3, param))
181 | local result_3rd_patry = assert(s3_3rd[api](s3_3rd, param))
182 |
183 | assert.same(expected_result_aws, result_aws)
184 | assert.same(expected_result_3rd_patry, result_3rd_patry)
185 | end)
186 | end
187 | end)
188 |
--------------------------------------------------------------------------------
/spec/04-services/04-rds-utils_spec.lua:
--------------------------------------------------------------------------------
1 | setmetatable(_G, nil)
2 |
3 | -- -- hock request sending
4 | -- package.loaded["resty.aws.request.execute"] = function(...)
5 | -- return ...
6 | -- end
7 |
8 | local AWS = require("resty.aws")
9 | local AWS_global_config = require("resty.aws.config").global
10 |
11 | local config = AWS_global_config
12 | local aws = AWS(config)
13 |
14 | aws.config.credentials = aws:Credentials {
15 | accessKeyId = "test_id",
16 | secretAccessKey = "test_key",
17 | }
18 |
19 | aws.config.region = "test_region"
20 |
21 | local DB_ENDPOINT = "test_database.test_cluster.us-east-1.rds.amazonaws.com"
22 | local DB_PORT = "443"
23 | local DB_REGION = "us-east-1"
24 | local DB_USER = "test_user"
25 |
26 | describe("RDS utils", function()
27 | local rds, signer
28 | local origin_time
29 | setup(function()
30 | origin_time = ngx.time
31 | ngx.time = function () --luacheck: ignore
32 | return 1667543171
33 | end
34 | end)
35 |
36 | teardown(function ()
37 | ngx.time = origin_time --luacheck: ignore
38 | end)
39 |
40 | before_each(function()
41 | rds = aws:RDS()
42 | signer = rds:Signer {
43 | hostname = DB_ENDPOINT,
44 | port = DB_PORT,
45 | username = DB_USER,
46 | region = DB_REGION, -- override aws config
47 | }
48 | end)
49 |
50 | after_each(function()
51 | rds = nil
52 | signer = nil
53 | end)
54 |
55 | it("should generate expected IAM auth token with mock key", function()
56 | local auth_token, err = signer:getAuthToken()
57 | local expected_auth_token = "test_database.test_cluster.us-east-1.rds.amazonaws.com:443/?X-Amz-Signature=ff72d46f1937c1f5917f69d694929ca814b781619b8d730451c7ffef050059b0&Action=connect&DBUser=test_user&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=test_id%2F20221104%2Fus-east-1%2Frds-db%2Faws4_request&X-Amz-Date=20221104T062611Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host"
58 | assert.is_nil(err)
59 | assert.same(auth_token, expected_auth_token)
60 | end)
61 |
62 | it("should generate expected IAM auth token with mock temporary credential", function()
63 | signer.config.credentials = aws:Credentials {
64 | accessKeyId = "test_id2",
65 | secretAccessKey = "test_key2",
66 | sessionToken = "test_token2",
67 | }
68 | local auth_token, err = signer:getAuthToken()
69 | local expected_auth_token = "test_database.test_cluster.us-east-1.rds.amazonaws.com:443/?X-Amz-Signature=7fcb20a161bb493b405686590604bfb864f8ac68dea84b903cd551e93f850ac5&Action=connect&DBUser=test_user&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=test_id2%2F20221104%2Fus-east-1%2Frds-db%2Faws4_request&X-Amz-Date=20221104T062611Z&X-Amz-Expires=900&X-Amz-Security-Token=test_token2&X-Amz-SignedHeaders=host"
70 | assert.is_nil(err)
71 | assert.same(auth_token, expected_auth_token)
72 | end)
73 | end)
74 |
--------------------------------------------------------------------------------
/spec/helpers.lua:
--------------------------------------------------------------------------------
1 | local ffi = require "ffi"
2 |
3 |
4 | local M = {}
5 |
6 |
7 | ffi.cdef [[
8 | int setenv(const char *name, const char *value, int overwrite);
9 | int unsetenv(const char *name);
10 | ]]
11 |
12 | local function unsetenv(env)
13 | assert(type(env) == "string", "expected env name to be a string")
14 | return ffi.C.unsetenv(env) == 0
15 | end
16 |
17 | local function setenv(env, value)
18 | assert(type(env) == "string", "expected env name to be a string")
19 | if value == nil then
20 | return unsetenv(env)
21 | end
22 | assert(type(value) == "string", "expected value to be a string (or nil to clear)")
23 | return ffi.C.setenv(env, value, 1) == 0
24 | end
25 |
26 | local original_env_values = {}
27 | local nil_sentinel = {}
28 | local function backup_env(name)
29 | original_env_values[name] = original_env_values[name] or os.getenv(name) or nil_sentinel
30 | end
31 |
32 |
33 | -- sets an environment variable, set to nil to remove it
34 | function M.setenv(env, value)
35 | assert(type(env) == "string", "expected env name to be a string")
36 | assert(type(value) == "string" or value == nil, "expected value to be a string (or nil to clear)")
37 | backup_env(env)
38 | setenv(env, value)
39 | end
40 |
41 |
42 | -- unsets an environment variable
43 | function M.unsetenv(env)
44 | assert(type(env) == "string", "expected env name to be a string")
45 | backup_env(env)
46 | unsetenv(env)
47 | end
48 |
49 |
50 | -- gets an environment variable
51 | M.getenv = os.getenv -- for symetry; get/set/unset
52 |
53 |
54 | -- restores all env vars to original values and clears all loaded 'resty.aws' modules
55 | function M.restore()
56 | for name, value in pairs(original_env_values) do
57 | setenv(name, value ~= nil_sentinel and value or nil)
58 | end
59 | for name, mod in pairs(package.loaded) do
60 | if type(name) == "string" and (name:match("^resty%.aws$") or name:match("^resty%.aws%.")) then
61 | package.loaded[name] = nil
62 | end
63 | end
64 | collectgarbage()
65 | collectgarbage()
66 |
67 | -- disable EC2 metadata
68 | setenv("AWS_EC2_METADATA_DISABLED", "true")
69 | end
70 |
71 | return setmetatable(M, {
72 | __call = function(self, ...)
73 | return self.restore(...)
74 | end
75 | })
76 |
--------------------------------------------------------------------------------
/spec/resty-runner.lua:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env resty
2 |
3 | -- script to run Busted tests using Openresty while setting some extra flags.
4 | --
5 | -- This script should be specified as:
6 | -- busted --lua=
7 | --
8 | -- Alternatively specify it in the `.busted` config file
9 |
10 |
11 | -- These flags are passed to `resty` by default, to allow for more connections
12 | -- and disable the Global variable write-guard. Override it by setting the
13 | -- environment variable `BUSTED_RESTY_FLAGS`.
14 | local RESTY_FLAGS=os.getenv("BUSTED_RESTY_FLAGS") or "-c 4096 -e 'setmetatable(_G, nil)'"
15 |
16 | -- rebuild the invoked commandline, while inserting extra resty-flags
17 | local cmd = {
18 | "exec",
19 | arg[-1],
20 | RESTY_FLAGS,
21 | }
22 | for i, param in ipairs(arg) do
23 | table.insert(cmd, "'" .. param .. "'")
24 | end
25 |
26 | local _, _, rc = os.execute(table.concat(cmd, " "))
27 | os.exit(rc)
28 |
--------------------------------------------------------------------------------
/src/resty/aws/credentials/ChainableTemporaryCredentials.lua:
--------------------------------------------------------------------------------
1 | --- ChainableTemporaryCredentials class.
2 | -- @classmod ChainableTemporaryCredentials
3 |
4 | local lom = require("lxp.lom")
5 |
6 |
7 | -- Create class
8 | local Super = require "resty.aws.credentials.Credentials"
9 | local ChainableTemporaryCredentials = setmetatable({}, Super)
10 | ChainableTemporaryCredentials.__index = ChainableTemporaryCredentials
11 |
12 |
13 | --- Constructor, inherits from `Credentials`.
14 | -- @function aws:ChainableTemporaryCredentials
15 | -- @param opt options table, additional fields to the `Credentials` class:
16 | -- @param opt.params params table for the `assumeRole` function, or array of those
17 | -- tables in case of a chain of roles to assume.
18 | -- @param opt.aws `AWS` instance, required when creating a chain.
19 | -- @param opt.masterCredentials `Credentials` instance to use when assuming the
20 | -- role. Defaults to `sts.config.credentials` or `aws.config.credentials` in that
21 | -- order.
22 | -- @param opt.sts the `STS` service instance to use for fetching the credentials.
23 | -- Defaults to a new instance created as `aws:STS()`.
24 | -- @usage -- creating a chain of assumed roles
25 | -- local aws = AWS() -- provides the masterCredentials
26 | -- local role1 = { ... } -- parameters to assume role1, from the masterCredentials
27 | -- local role2 = { ... } -- parameters to assume role2, from the role1 credentials
28 | -- local role3 = { ... } -- parameters to assume role3, from the role2 credentials
29 | --
30 | -- local creds = aws:ChainableTemporaryCredentials {
31 | -- params = { role1, role2, role3 },
32 | -- }
33 | --
34 | -- -- Get credentials for role3
35 | -- local success, id, key, token, expiretime = creds:get()
36 | -- if not success then
37 | -- return nil, id
38 | -- end
39 |
40 | function ChainableTemporaryCredentials:new(opts)
41 | local self = Super:new(opts) -- override 'self' to be the new object/class
42 | setmetatable(self, ChainableTemporaryCredentials)
43 |
44 | opts = opts or {}
45 |
46 | assert(opts.tokenCodeFn == nil, "Option 'opts.tokenCodeFn' is not supported.")
47 |
48 | -- get the master credentials to use
49 | local mCredentials = opts.masterCredentials
50 | if not mCredentials and opts.sts then
51 | mCredentials = ((opts.sts or {}).config or {}).credentials
52 | end
53 | if not mCredentials and opts.aws then
54 | mCredentials = ((opts.aws or {}).config or {}).credentials
55 | end
56 | assert(type(mCredentials) == "table", "No master-credentials provided, either 'opts.masterCredentials', 'opts.aws', or 'opts.sts' options must be set")
57 |
58 | -- get array of params-tables
59 | local params = opts.params
60 | assert(type(params) == "table", "Expected 'opts.params' to be a parameter table/map or an array of parameter tables/maps")
61 | if not params[1] then
62 | -- not an array, so a parameter table/map, make it an array
63 | params = { params }
64 | end
65 |
66 | -- get the STS service instance to use
67 | local sts = opts.sts
68 | if sts then
69 | assert(#params == 1, "Cannot use 'opts.sts' to create a chain, only specify a single 'opts.params' entry, or specify 'opts.aws' instead of 'opts.sts'.")
70 | else
71 | if opts.aws then
72 | local err
73 | sts, err = opts.aws:STS()
74 | if not sts then
75 | error("failed to create STS instance: " .. tostring(err))
76 | end
77 | end
78 | end
79 | assert(type(sts) == "table", "No STS service, either 'opts.sts' or 'opts.aws' option must be set")
80 |
81 | if #params == 1 then
82 | -- there is only 1 role to assume so that is us.
83 | -- self.aws = aws
84 | self.sts = sts
85 | self.params = params[1]
86 | self.masterCredentials = mCredentials
87 | else
88 | -- multiple roles to assume, so pick the last and create a sub-credential
89 | -- self.aws = aws
90 | self.sts = sts
91 | self.params = params[#params]
92 | params[#params] = nil
93 | self.masterCredentials = ChainableTemporaryCredentials:new {
94 | masterCredentials = mCredentials,
95 | params = params,
96 | aws = opts.aws,
97 | }
98 | end
99 |
100 | return self
101 | end
102 |
103 |
104 | -- updates credentials.
105 | -- @return success, or nil+err
106 | function ChainableTemporaryCredentials:refresh()
107 | local response, err = self.sts:assumeRole(self.params)
108 | if not response then
109 | return nil, "Request for token data failed: " .. tostring(err)
110 | end
111 |
112 | if response.status ~= 200 then
113 | return nil, ("request for token returned '%s': %s"):format(tostring(response.status), response.body)
114 | end
115 |
116 | if type(response.body) ~= "string" then
117 | return nil, "request for token returned invalid body: " .. err
118 | end
119 |
120 | local resp_body_lom, err = lom.parse(response.body)
121 | if not resp_body_lom then
122 | return nil, "failed to parse response body: " .. err
123 | end
124 |
125 | local cred_lom = lom.find_elem(lom.find_elem(resp_body_lom, "AssumeRoleResult"), "Credentials")
126 |
127 | local AccessKeyId = lom.find_elem(cred_lom, "AccessKeyId")[1]
128 | local SecretAccessKey = lom.find_elem(cred_lom, "SecretAccessKey")[1]
129 | local SessionToken = lom.find_elem(cred_lom, "SessionToken")[1]
130 | local Expiration = lom.find_elem(cred_lom, "Expiration")[1]
131 |
132 | self:set(AccessKeyId, SecretAccessKey, SessionToken, Expiration)
133 |
134 | return true
135 | end
136 |
137 | return ChainableTemporaryCredentials
138 |
--------------------------------------------------------------------------------
/src/resty/aws/credentials/CredentialProviderChain.lua:
--------------------------------------------------------------------------------
1 | --- CredentialProviderChain class.
2 | -- @classmod CredentialProviderChain
3 |
4 |
5 | -- Create class
6 | local Super = require "resty.aws.credentials.Credentials"
7 | local CredentialProviderChain = setmetatable({}, Super)
8 | CredentialProviderChain.__index = CredentialProviderChain
9 |
10 |
11 | local aws_config = require("resty.aws.config")
12 |
13 |
14 | CredentialProviderChain.defaultProviders = {}
15 |
16 |
17 | local function initialize()
18 | -- while not everything is implemented this will load what we do have without
19 | -- failing on what is missing. Will auto pick up newly added classes afterwards.
20 | local function add_if_exists(name, opts)
21 | local ok, class = pcall(require, "resty.aws.credentials." .. name)
22 | if not ok then
23 | ngx.log(ngx.DEBUG, "AWS credential class '", name, "' not found or failed to load")
24 | return
25 | end
26 | -- instantiate and add
27 | local ok, instance = pcall(class.new, class, opts)
28 | if not ok then
29 | ngx.log(ngx.DEBUG, "AWS credential class '", name, "' failed to instantiate: ", instance)
30 | return
31 | end
32 | CredentialProviderChain.defaultProviders[#CredentialProviderChain.defaultProviders+1] = instance
33 | end
34 |
35 | -- add the defaults
36 | add_if_exists("EnvironmentCredentials", { envPrefix = 'AWS' })
37 | add_if_exists("EnvironmentCredentials", { envPrefix = 'AMAZON' })
38 | add_if_exists("SharedFileCredentials")
39 | add_if_exists("RemoteCredentials") -- since "ECSCredentials" doesn't exist? and for ECS RemoteCredentials is used???
40 | add_if_exists("ProcessCredentials")
41 | add_if_exists("TokenFileWebIdentityCredentials")
42 | if aws_config.global.AWS_EC2_METADATA_DISABLED then
43 | ngx.log(ngx.DEBUG, "AWS_EC2_METADATA_DISABLED is set, skipping EC2MetadataCredentials provider")
44 | else
45 | add_if_exists("EC2MetadataCredentials")
46 | end
47 |
48 | initialize = nil
49 | end
50 |
51 | --- Constructor, inherits from `Credentials`.
52 | --
53 | -- The `providers` array defaults to the following list (in order, not all implemented):
54 | --
55 | -- 1. `EnvironmentCredentials`; envPrefix = 'AWS'
56 | --
57 | -- 2. `EnvironmentCredentials`; envPrefix = 'AMAZON'
58 | --
59 | -- 3. `SharedIniFileCredentials`
60 | --
61 | -- 4. `RemoteCredentials`
62 | --
63 | -- 5. `ProcessCredentials`
64 | --
65 | -- 6. `TokenFileWebIdentityCredentials`
66 | --
67 | -- 7. `EC2MetadataCredentials` (only if `AWS_EC2_METADATA_DISABLED` hasn't been set to `true`)
68 | --
69 | -- @function aws:CredentialProviderChain
70 | -- @param opt options table, additional fields to the `Credentials` class:
71 | -- @param opt.providers array of `Credentials` objects or functions (functions must return a `Credentials` object)
72 | function CredentialProviderChain:new(opts)
73 | if initialize then
74 | initialize()
75 | end
76 |
77 | local self = Super:new(opts) -- override 'self' to be the new object/class
78 | setmetatable(self, CredentialProviderChain)
79 |
80 | opts = opts or {}
81 |
82 | self.providers = opts.providers
83 | if not self.providers then
84 | self.providers = {}
85 | for i, provider in ipairs(CredentialProviderChain.defaultProviders) do
86 | self.providers[i] = provider
87 | end
88 | end
89 |
90 | assert(type(self.providers) == "table", "expected opts.providers to be an array of 'Credentials' objects or functions returning Credentials")
91 |
92 | return self
93 | end
94 |
95 | -- updates credentials.
96 | -- @return true
97 | function CredentialProviderChain:refresh()
98 | for i, provider in ipairs(self.providers) do
99 | if type(provider) == "function" then
100 | -- lazily create Credential
101 | local p, err = provider()
102 | if not p then
103 | ngx.log(ngx.ERR, "failed to instantiate Credential from provider function: ", tostring(err))
104 | else
105 | -- store succesful created credential, replacing the previous function
106 | self.providers[i] = p
107 | provider = p
108 | end
109 | end
110 |
111 | local success, accessKeyId, secretAccessKey, sessionToken, expireTime = provider:get()
112 | if not success then
113 | ngx.log(ngx.DEBUG, "Provider failed: ", accessKeyId)
114 | else
115 | -- success, store results and exit
116 | self:set(accessKeyId, secretAccessKey, sessionToken, expireTime)
117 | return true
118 | end
119 | end
120 | return nil, "none of the providers succeeded, no credentials available"
121 | end
122 |
123 | return CredentialProviderChain
124 |
--------------------------------------------------------------------------------
/src/resty/aws/credentials/Credentials.lua:
--------------------------------------------------------------------------------
1 | --- Credentials class.
2 | -- Manually sets credentials.
3 | -- Also the base class for all credential classes.
4 | -- @classmod Credentials
5 | local parse_date = require("luatz").parse.rfc_3339
6 | local semaphore = require "ngx.semaphore"
7 |
8 |
9 | local SEMAPHORE_TIMEOUT = 30 -- semaphore timeout in seconds
10 |
11 | -- Executes a xpcall but returns hard-errors as Lua 'nil+err' result.
12 | -- Handles max of 10 return values.
13 | -- @param f function to execute
14 | -- @param ... parameters to pass to the function
15 | local function safe_call(f, ...)
16 | local ok, result, err, r3, r4, r5, r6, r7, r8, r9, r10 = xpcall(f, debug.traceback, ...)
17 | if ok then
18 | return result, err, r3, r4, r5, r6, r7, r8, r9, r10
19 | end
20 | return nil, result
21 | end
22 |
23 |
24 | local Credentials = {}
25 | Credentials.__index = Credentials
26 |
27 | --- Constructor.
28 | -- @function aws:Credentials
29 | -- @param opt options table
30 | -- @param opt.expiryWindow number (default 15) of seconds before expiry to start refreshing
31 | -- @param opt.accessKeyId (optional) only specify if you manually specify credentials
32 | -- @param opt.secretAccessKey (optional) only specify if you manually specify credentials
33 | -- @param opt.sessionToken (optional) only specify if you manually specify credentials
34 | -- @param opt.expireTime (optional, number (epoch) or string (rfc3339)). This should
35 | -- not be specified. Default: If any of the 3 secrets are given; 10yrs, otherwise 0
36 | -- (forcing a refresh on the first call to `get`).
37 | -- @usage
38 | -- local my_creds = aws:Credentials {
39 | -- accessKeyId = "access",
40 | -- secretAccessKey = "secret",
41 | -- sessionToken = "token",
42 | -- }
43 | --
44 | -- local success, id, secret, token = my_creds:get()
45 | function Credentials:new(opts)
46 | local self = {} -- override 'self' to be the new object/class
47 | setmetatable(self, Credentials)
48 |
49 | opts = opts or {}
50 | if opts.aws then
51 | if getmetatable(opts.aws) ~= require("resty.aws") then
52 | error("'opts.aws' must be set to an AWS instance or nil")
53 | end
54 | self.aws = opts.aws
55 | end
56 |
57 | if opts.accessKeyId or opts.secretAccessKey or opts.sessionToken then
58 | -- credentials provided, if no expire given then use 10 yrs
59 | self:set(opts.accessKeyId, opts.secretAccessKey, opts.sessionToken,
60 | opts.expireTime or (ngx.now() + 10*365*24*60*60))
61 | else
62 | self.accessKeyId = nil
63 | self.secretAccessKey = nil
64 | self.sessionToken = nil
65 | self.expireTime = 0 -- force refresh on next "get"
66 | end
67 | -- self.expired -- not implemented
68 | self.expiryWindow = opts.expiryWindow or 15 -- time in seconds befoer expireTime creds should be refreshed
69 |
70 | return self
71 | end
72 |
73 | --- checks whether credentials have expired.
74 | -- @return boolean
75 | function Credentials:needsRefresh()
76 | return (self.expireTime or 0) < (ngx.now() + self.expiryWindow)
77 | end
78 |
79 | --- Gets credentials, refreshes if required.
80 | -- Returns credentials, doesn't take a callback like AWS SDK.
81 | --
82 | -- When a refresh is executed, it will be done within a semaphore to prevent
83 | -- many simultaneous refreshes.
84 | -- @return success(true) + accessKeyId + secretAccessKey + sessionToken + expireTime or success(false) + error
85 | function Credentials:get()
86 | while self:needsRefresh() do
87 | if self.semaphore then
88 | -- an update is in progress
89 | local ok, err = self.semaphore:wait(SEMAPHORE_TIMEOUT)
90 | if not ok then
91 | ngx.log(ngx.ERR, "[Credentials ", self.type, "] waiting for semaphore failed: ", err)
92 | return nil, "waiting for semaphore failed: " .. tostring(err)
93 | end
94 | else
95 | -- no update in progress
96 | local sema, err = semaphore.new()
97 | self.semaphore = sema
98 | if not sema then
99 | return nil, "create semaphore failed: " .. tostring(err)
100 | end
101 |
102 | local ok, err = safe_call(self.refresh, self)
103 |
104 | -- release all waiting threads
105 | self.semaphore = nil
106 | sema:post(math.abs(sema:count())+1)
107 |
108 | if not ok then return
109 | nil, err
110 | end
111 | break
112 | end
113 | end
114 | -- we always return a boolean successvalue, if we would rely on standard Lua
115 | -- "nil + err" behaviour, then if the accessKeyId happens to be 'nil' for some
116 | -- reason, we risk logging the secretAccessKey as the error message in some
117 | -- client code.
118 | return true, self.accessKeyId, self.secretAccessKey, self.sessionToken, self.expireTime
119 | end
120 |
121 | --- Sets credentials.
122 | -- additional to AWS SDK
123 | -- @param accessKeyId
124 | -- @param secretAccessKey
125 | -- @param sessionToken
126 | -- @param expireTime (optional) number (unix epoch based), or string (valid rfc 3339)
127 | -- @return true
128 | function Credentials:set(accessKeyId, secretAccessKey, sessionToken, expireTime)
129 | -- TODO: should we be parsing the token (if given) to get the expireTime?
130 | local expiration
131 | if type(expireTime) == "string" then
132 | expiration = parse_date(expireTime):timestamp()
133 | end
134 | if type(expireTime) == "number" then
135 | expiration = expireTime
136 | end
137 | if not expiration then
138 | error("expected expireTime to be a number (unix epoch based), or string (valid rfc 3339)", 2)
139 | end
140 |
141 | self.expireTime = expiration
142 | self.accessKeyId = accessKeyId
143 | self.secretAccessKey = secretAccessKey
144 | self.sessionToken = sessionToken
145 | return true
146 | end
147 |
148 | --- updates credentials.
149 | -- override in subclasses, should call `set` to set the properties.
150 | -- @return success, or nil+err
151 | function Credentials:refresh()
152 | error("Not implemented")
153 | end
154 |
155 | -- not implemented
156 | function Credentials:getPromise()
157 | error("Not implemented")
158 | end
159 | function Credentials:refreshPromise()
160 | error("Not implemented")
161 | end
162 |
163 | return Credentials
164 |
--------------------------------------------------------------------------------
/src/resty/aws/credentials/EC2MetadataCredentials.lua:
--------------------------------------------------------------------------------
1 | --- EC2MetadataCredentials class.
2 | -- @classmod EC2MetadataCredentials
3 |
4 | local http = require "resty.luasocket.http"
5 | local json = require("cjson.safe").new()
6 | local log = ngx.log
7 | local DEBUG = ngx.DEBUG
8 |
9 |
10 |
11 | local METADATA_SERVICE_SCHEME = "http"
12 | local METADATA_SERVICE_PORT = 80
13 | local METADATA_SERVICE_REQUEST_TIMEOUT = 5000 -- milliseconds
14 | local METADATA_SERVICE_HOST = "169.254.169.254"
15 | local METADATA_SERVICE_TOKEN_TTL = 300 -- seconds
16 |
17 |
18 |
19 | -- Create class
20 | local Super = require "resty.aws.credentials.Credentials"
21 | local EC2MetadataCredentials = setmetatable({}, Super)
22 | EC2MetadataCredentials.__index = EC2MetadataCredentials
23 |
24 |
25 | --- Constructor, inherits from `Credentials`.
26 | -- @function aws:EC2MetadataCredentials
27 | -- @param opt options table, no additional fields to the `Credentials` class.
28 | function EC2MetadataCredentials:new(opts)
29 | local self = Super:new(opts) -- override 'self' to be the new object/class
30 | setmetatable(self, EC2MetadataCredentials)
31 |
32 | return self
33 | end
34 |
35 | -- updates credentials.
36 | -- @return success, or nil+err
37 | function EC2MetadataCredentials:refresh()
38 | local client = http.new()
39 | client:set_timeout(METADATA_SERVICE_REQUEST_TIMEOUT)
40 |
41 | local ok, err = client:connect {
42 | scheme = METADATA_SERVICE_SCHEME,
43 | host = METADATA_SERVICE_HOST,
44 | port = METADATA_SERVICE_PORT,
45 | }
46 | if not ok then
47 | return nil, "Could not connect to EC2 metadata service: " .. tostring(err)
48 | end
49 |
50 | local imds_token
51 |
52 | local token_request_res, err = client:request {
53 | method = "PUT",
54 | path = "/latest/api/token",
55 | headers = {
56 | ["X-aws-ec2-metadata-token-ttl-seconds"] = METADATA_SERVICE_TOKEN_TTL,
57 | },
58 | }
59 |
60 | if not token_request_res then
61 | log(DEBUG, "Could not fetch token from EC2 metadata service: " .. tostring(err))
62 | elseif token_request_res.status ~= 200 then
63 | log(DEBUG, "Failed to fetch token from EC2 metadata service: " .. tostring(err))
64 | else
65 | imds_token = tostring(token_request_res:read_body())
66 | end
67 |
68 | -- recycle the client, because the luasocket/ngx.socket compatibility is not
69 | -- solid enough to reuse the httrp client
70 | client:close()
71 | client = http.new()
72 | client:set_timeout(METADATA_SERVICE_REQUEST_TIMEOUT)
73 |
74 | local ok, err = client:connect {
75 | scheme = METADATA_SERVICE_SCHEME,
76 | host = METADATA_SERVICE_HOST,
77 | port = METADATA_SERVICE_PORT,
78 | }
79 | if not ok then
80 | return nil, "Could not connect to EC2 metadata service: " .. tostring(err)
81 | end
82 |
83 | local role_name_request_res, err = client:request {
84 | method = "GET",
85 | path = "/latest/meta-data/iam/security-credentials/",
86 | headers = {
87 | ["X-aws-ec2-metadata-token"] = imds_token,
88 | }
89 | }
90 |
91 | if not role_name_request_res then
92 | return nil, "Could not fetch role name from EC2 metadata service: " .. tostring(err)
93 | end
94 |
95 | if role_name_request_res.status ~= 200 then
96 | return nil, "Fetching role name from EC2 metadata service returned status code " ..
97 | role_name_request_res.status .. " with body: " .. role_name_request_res:read_body()
98 | end
99 |
100 | local iam_role_name = role_name_request_res:read_body()
101 | log(DEBUG, "Found EC2 IAM role on instance with name: ", iam_role_name)
102 |
103 |
104 | -- recycle the client, because the luasocket/ngx.socket compatibility is not
105 | -- solid enough to reuse the httrp client
106 | client:close()
107 | client = http.new()
108 | client:set_timeout(METADATA_SERVICE_REQUEST_TIMEOUT)
109 |
110 |
111 | local ok, err = client:connect {
112 | scheme = METADATA_SERVICE_SCHEME,
113 | host = METADATA_SERVICE_HOST,
114 | port = METADATA_SERVICE_PORT,
115 | }
116 | if not ok then
117 | return nil, "Could not connect to EC2 metadata service: " .. tostring(err)
118 | end
119 |
120 | local iam_security_token_request, err = client:request {
121 | method = "GET",
122 | path = "/latest/meta-data/iam/security-credentials/" .. iam_role_name,
123 | headers = {
124 | ["X-aws-ec2-metadata-token"] = imds_token,
125 | }
126 | }
127 |
128 | if not iam_security_token_request then
129 | return nil, "Failed to request EC2 IAM credentials for role " .. iam_role_name ..
130 | " Request returned error: " .. tostring(err)
131 | end
132 |
133 | if iam_security_token_request.status == 404 then
134 | return nil, "Unable to request EC2 IAM credentials for role " .. iam_role_name ..
135 | " Request returned status code 404."
136 | end
137 |
138 | if iam_security_token_request.status ~= 200 then
139 | return nil, "Unable to request EC2 IAM credentials for role" .. iam_role_name ..
140 | " Request returned status code " .. iam_security_token_request.status ..
141 | " " .. tostring(iam_security_token_request:read_body())
142 | end
143 |
144 | local iam_security_token_request_body = iam_security_token_request:read_body()
145 | local iam_security_token_data = json.decode(iam_security_token_request_body)
146 |
147 | if not iam_security_token_data.AccessKeyId then
148 | return nil, "Unable to request EC2 IAM credentials for role " .. iam_role_name ..
149 | " Request returned unexpected result " .. iam_security_token_request_body
150 | end
151 |
152 | log(DEBUG, "Received temporary IAM credential from EC2 metadata service for role '",
153 | iam_role_name, "' with session token: ",
154 | string.sub(iam_security_token_data.Token, 1 , 10),
155 | "...")
156 |
157 | self:set(iam_security_token_data.AccessKeyId,
158 | iam_security_token_data.SecretAccessKey,
159 | iam_security_token_data.Token,
160 | iam_security_token_data.Expiration)
161 |
162 | return true
163 | end
164 |
165 | return EC2MetadataCredentials
166 |
--------------------------------------------------------------------------------
/src/resty/aws/credentials/EnvironmentCredentials.lua:
--------------------------------------------------------------------------------
1 | --- EnvironmentCredentials class.
2 | -- @classmod EnvironmentCredentials
3 |
4 |
5 | local aws_config = require("resty.aws.config")
6 |
7 |
8 | -- Create class
9 | local Super = require "resty.aws.credentials.Credentials"
10 | local EnvironmentCredentials = setmetatable({}, Super)
11 | EnvironmentCredentials.__index = EnvironmentCredentials
12 |
13 |
14 | --- Constructor, inherits from `Credentials`.
15 | --
16 | -- _Note_: this class will fetch the credentials upon instantiation. So it can be
17 | -- instantiated in the `init` phase where there is still access to the environment
18 | -- variables. The standard prefixes `AWS` and `AMAZON` are covered by the `config`
19 | -- module, so in case those are used, only the `config` module needs to be loaded
20 | -- in the `init` phase.
21 | -- @function aws:EnvironmentCredentials
22 | -- @param opt options table, additional fields to the `Credentials` class:
23 | -- @param opt.envPrefix prefix to use when looking for environment variables, defaults to "AWS".
24 | function EnvironmentCredentials:new(opts)
25 | local self = Super:new(opts) -- override 'self' to be the new object/class
26 | setmetatable(self, EnvironmentCredentials)
27 |
28 | opts = opts or {}
29 | self.envPrefix = opts.envPrefix or "AWS"
30 |
31 | self:get() -- force immediate refresh
32 |
33 | return self
34 | end
35 |
36 | -- updates credentials.
37 | -- @return success, or nil+err
38 | function EnvironmentCredentials:refresh()
39 | local global_config = aws_config.global
40 |
41 | local access = os.getenv(self.envPrefix .. "_ACCESS_KEY_ID") or global_config[self.envPrefix .. "_ACCESS_KEY_ID"]
42 | if not access then
43 | -- Note: nginx workers do not have access to env vars. initialize in init phase
44 | -- or enable access for any prefix other than "AWS" and "AMAZON" which are covered
45 | -- by the 'config' module.
46 | return nil, "Couldn't find " .. self.envPrefix .. "_ACCESS_KEY_ID env variable"
47 | end
48 | local secret = os.getenv(self.envPrefix .. "_SECRET_ACCESS_KEY") or global_config[self.envPrefix .. "_SECRET_ACCESS_KEY"]
49 | local token = os.getenv(self.envPrefix .. "_SESSION_TOKEN") or global_config[self.envPrefix .. "_SESSION_TOKEN"]
50 | local expire = ngx.now() + 10 * 365 * 24 * 60 * 60 -- static, so assume 10 year validity
51 | self:set(access, secret, token, expire)
52 | return true
53 | end
54 |
55 | return EnvironmentCredentials
56 |
--------------------------------------------------------------------------------
/src/resty/aws/credentials/README.md:
--------------------------------------------------------------------------------
1 | # Credential classes
2 |
3 | see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Credentials.html
4 |
5 | ## Classes implemented
6 |
7 | Credentials
8 | CredentialProviderChain
9 | EC2MetadataCredentials
10 | EnvironmentCredentials
11 | RemoteCredentials
12 | TokenFileWebIdentityCredentials
13 | ChainableTemporaryCredentials --> to be tested
14 |
15 | ## Classes not yet implemented
16 |
17 | CognitoIdentityCredentials
18 | FileSystemCredentials
19 | ProcessCredentials
20 | SAMLCredentials
21 | SharedIniFileCredentials
22 | TemporaryCredentials (superseeded by ChainableTemporaryCredentials)
23 | WebIdentityCredentials
24 |
--------------------------------------------------------------------------------
/src/resty/aws/credentials/RemoteCredentials.lua:
--------------------------------------------------------------------------------
1 | --- RemoteCredentials class.
2 | -- @classmod RemoteCredentials
3 |
4 |
5 | -- This code is reverse engineered from the original AWS sdk. Specifically:
6 | -- https://github.com/aws/aws-sdk-js/blob/c175cb2b89576f01c08ebf39b232584e4fa2c0e0/lib/credentials/remote_credentials.js
7 |
8 | local log = ngx.log
9 | local DEBUG = ngx.DEBUG
10 |
11 | local DEFAULT_SERVICE_REQUEST_TIMEOUT = 5000
12 |
13 | local url = require "socket.url"
14 | local http = require "resty.luasocket.http"
15 | local json = require "cjson"
16 | local readfile = require("pl.utils").readfile
17 |
18 |
19 | local FullUri
20 | local AuthToken
21 | local AuthTokenFile
22 |
23 |
24 | local function initialize()
25 | -- construct the URL
26 | local function makeset(t)
27 | for i = 1, #t do
28 | t[t[i]] = true
29 | end
30 | return t
31 | end
32 |
33 | local aws_config = require("resty.aws.config")
34 |
35 | local FULL_URI_UNRESTRICTED_PROTOCOLS = makeset { "https" }
36 | local FULL_URI_ALLOWED_PROTOCOLS = makeset { "http", "https" }
37 | local FULL_URI_ALLOWED_HOSTNAMES = makeset { "localhost", "127.0.0.1", "169.254.170.23" }
38 | local RELATIVE_URI_HOST = '169.254.170.2'
39 |
40 | local function getFullUri()
41 | if aws_config.global.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI then
42 | return 'http://' .. RELATIVE_URI_HOST .. aws_config.global.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI
43 |
44 | elseif aws_config.global.AWS_CONTAINER_CREDENTIALS_FULL_URI then
45 | local parsed_url = url.parse(aws_config.global.AWS_CONTAINER_CREDENTIALS_FULL_URI)
46 |
47 | if not FULL_URI_ALLOWED_PROTOCOLS[parsed_url.scheme] then
48 | return nil, 'Unsupported protocol, must be one of '
49 | .. table.concat(FULL_URI_ALLOWED_PROTOCOLS, ',') .. '. Got: '
50 | .. parsed_url.scheme
51 | end
52 |
53 | if (not FULL_URI_UNRESTRICTED_PROTOCOLS[parsed_url.scheme]) and
54 | (not FULL_URI_ALLOWED_HOSTNAMES[parsed_url.host]) then
55 | return nil, 'Unsupported hostname: AWS.RemoteCredentials only supports '
56 | .. table.concat(FULL_URI_ALLOWED_HOSTNAMES, ',') .. ' for '
57 | .. parsed_url.scheme .. '; ' .. parsed_url.scheme .. '://'
58 | .. parsed_url.host .. ' requested.'
59 | end
60 |
61 | return aws_config.global.AWS_CONTAINER_CREDENTIALS_FULL_URI
62 |
63 | else
64 | return nil, 'Environment variable AWS_CONTAINER_CREDENTIALS_RELATIVE_URI or '
65 | .. 'AWS_CONTAINER_CREDENTIALS_FULL_URI must be set to use AWS.RemoteCredentials.'
66 | end
67 | end
68 |
69 |
70 | local err
71 | FullUri, err = getFullUri()
72 | if not FullUri then
73 | log(DEBUG, "Failed to construct RemoteCredentials url: ", err)
74 |
75 | else
76 | -- parse it and set a default port if omitted
77 | FullUri = url.parse(FullUri)
78 | FullUri.port = FullUri.port or
79 | ({ http = 80, https = 443 })[FullUri.scheme]
80 | end
81 |
82 | -- get auth token/file
83 | AuthToken = aws_config.global.AWS_CONTAINER_AUTHORIZATION_TOKEN
84 | AuthTokenFile = aws_config.global.AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE
85 |
86 | initialize = nil
87 | end
88 |
89 |
90 | -- Create class
91 | local Super = require "resty.aws.credentials.Credentials"
92 | local RemoteCredentials = setmetatable({}, Super)
93 | RemoteCredentials.__index = RemoteCredentials
94 |
95 |
96 | --- Constructor, inherits from `Credentials`.
97 | -- @function aws:RemoteCredentials
98 | -- @param opt options table, no additional fields to the `Credentials` class.
99 | function RemoteCredentials:new(opts)
100 | local self = Super:new(opts) -- override 'self' to be the new object/class
101 | setmetatable(self, RemoteCredentials)
102 |
103 | return self
104 | end
105 |
106 | -- updates credentials.
107 | -- @return success, or nil+err
108 | function RemoteCredentials:refresh()
109 | if initialize then
110 | initialize()
111 | end
112 |
113 | if not FullUri then
114 | return nil, "No URI environment variables found for RemoteCredentials"
115 | end
116 |
117 |
118 | local headers = {}
119 |
120 | if AuthToken then
121 | headers["Authorization"] = AuthToken
122 | end
123 |
124 | if AuthTokenFile then
125 | local token, err = readfile(AuthTokenFile)
126 | if not token then
127 | return nil, "Failed reading AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE: " .. err
128 | end
129 |
130 | headers["Authorization"] = token
131 | end
132 |
133 | local client = http.new()
134 | client:set_timeout(DEFAULT_SERVICE_REQUEST_TIMEOUT)
135 |
136 | local ok, err = client:connect {
137 | scheme = FullUri.scheme,
138 | host = FullUri.host,
139 | port = FullUri.port,
140 | }
141 | if not ok then
142 | return nil, "Could not connect to RemoteCredentials metadata service: " .. tostring(err)
143 | end
144 |
145 | local response, err = client:request {
146 | method = "GET",
147 | path = FullUri.path,
148 | headers = headers,
149 | }
150 |
151 | if not response then
152 | return nil, "Failed to request RemoteCredentials request returned error: " .. tostring(err)
153 | end
154 |
155 | if response.status ~= 200 then
156 | return nil, "Unable to request RemoteCredentials request returned status code " ..
157 | response.status .. " " .. tostring(response:read_body())
158 | end
159 |
160 | local credentials = json.decode(response:read_body())
161 |
162 | log(DEBUG, "Received temporary IAM credential from RemoteCredentials " ..
163 | "service with session token: ", credentials.Token)
164 |
165 | self:set(credentials.AccessKeyId,
166 | credentials.SecretAccessKey,
167 | credentials.Token,
168 | credentials.Expiration)
169 | return true
170 | end
171 |
172 | return RemoteCredentials
173 |
--------------------------------------------------------------------------------
/src/resty/aws/credentials/SharedFileCredentials.lua:
--------------------------------------------------------------------------------
1 | --- SharedFileCredentials class.
2 | -- @classmod SharedFileCredentials
3 |
4 |
5 | -- Create class
6 | local Super = require "resty.aws.credentials.Credentials"
7 | local config = require "resty.aws.config"
8 | local SharedFileCredentials = setmetatable({}, Super)
9 | SharedFileCredentials.__index = SharedFileCredentials
10 |
11 |
12 | --- Constructor, inherits from `Credentials`.
13 | --
14 | -- @function aws:SharedFileCredentials
15 | -- @param opt options table, additional fields to the `Credentials` class:
16 | function SharedFileCredentials:new(opts)
17 | local self = Super:new(opts) -- override 'self' to be the new object/class
18 | setmetatable(self, SharedFileCredentials)
19 |
20 | self:get() -- force immediate refresh
21 |
22 | return self
23 | end
24 |
25 | -- updates credentials.
26 | -- @return success, or nil+err
27 | function SharedFileCredentials:refresh()
28 | local cred = config.load_credentials()
29 |
30 | if not (cred.aws_access_key_id or cred.aws_session_token) then
31 | return false, "no credentials found"
32 | end
33 |
34 | local expire = ngx.now() + 10 * 365 * 24 * 60 * 60 -- static, so assume 10 year validity
35 | self:set(cred.aws_access_key_id, cred.aws_secret_access_key, cred.aws_session_token, expire)
36 | return true
37 | end
38 |
39 | return SharedFileCredentials
40 |
--------------------------------------------------------------------------------
/src/resty/aws/credentials/TokenFileWebIdentityCredentials.lua:
--------------------------------------------------------------------------------
1 | --- TokenFileWebIdentityCredentials class.
2 | -- @classmod TokenFileWebIdentityCredentials
3 |
4 | local readfile = require("pl.utils").readfile
5 | local lom = require("lxp.lom")
6 |
7 | local aws_config = require("resty.aws.config")
8 |
9 |
10 | -- Create class
11 | local Super = require "resty.aws.credentials.Credentials"
12 | local TokenFileWebIdentityCredentials = setmetatable({}, Super)
13 | TokenFileWebIdentityCredentials.__index = TokenFileWebIdentityCredentials
14 |
15 |
16 | --- Constructor, inherits from `Credentials`.
17 | -- @function aws:TokenFileWebIdentityCredentials
18 | -- @tparam table opts options table, only listing additional fields to the `Credentials` class.
19 | -- @tparam[opt=AWS_WEB_IDENTITY_TOKEN_FILE env var] string opts.token_file filename of the token file
20 | -- @tparam[opt=AWS_ROLE_ARN env var] string opts.role_arn arn of the role to assume
21 | -- @tparam[opt=AWS_ROLE_SESSION_NAME env var or 'session@lua-resty-aws'] string opts.session_name session name
22 | function TokenFileWebIdentityCredentials:new(opts)
23 | local self = Super:new(opts) -- override 'self' to be the new object/class
24 | setmetatable(self, TokenFileWebIdentityCredentials)
25 |
26 | opts = opts or {}
27 | self.token_file = assert(
28 | opts.token_file or aws_config.global.AWS_WEB_IDENTITY_TOKEN_FILE,
29 | "either 'opts.token_file' or environment variable 'AWS_WEB_IDENTITY_TOKEN_FILE' must be set"
30 | )
31 | self.role_arn = assert(
32 | opts.role_arn or aws_config.global.AWS_ROLE_ARN,
33 | "either 'opts.role_arn' or environment variable 'AWS_ROLE_ARN' must be set"
34 | )
35 | self.session_name = opts.session_name or aws_config.global.AWS_ROLE_SESSION_NAME or "session@lua-resty-aws"
36 |
37 | return self
38 | end
39 |
40 | -- updates credentials.
41 | -- @return success, or nil+err
42 | function TokenFileWebIdentityCredentials:refresh()
43 | if not self.sts then
44 | -- instantiate on first use. Cannot do this in the constructor, since the
45 | -- constructor is called when instantiating an AWS instance (creating a loop).
46 | -- That's because this credentials class is part of the "CredentialProviderChain"
47 | local AWS = require "resty.aws"
48 | local aws = AWS {
49 | region = aws_config.global.region,
50 | stsRegionalEndpoints = aws_config.global.sts_regional_endpoints,
51 | }
52 | local sts, err = aws:STS()
53 | if not sts then
54 | error("failed to construct AWS.STS instance: " .. tostring(err))
55 | end
56 | self.sts = sts
57 | end
58 |
59 | local token, err = readfile(self.token_file)
60 | if not token then
61 | return nil, "failed reading token file: " .. err
62 | end
63 |
64 | local response, err = self.sts:assumeRoleWithWebIdentity {
65 | RoleArn = self.role_arn,
66 | RoleSessionName = self.session_name,
67 | WebIdentityToken = token
68 | }
69 |
70 | if not response then
71 | return nil, "Request for token data failed: " .. tostring(err)
72 | end
73 |
74 | if response.status ~= 200 then
75 | return nil, ("request for token returned '%s': %s"):format(tostring(response.status), response.body)
76 | end
77 |
78 | if type(response.body) ~= "string" then
79 | return nil, "request for token returned invalid body: " .. err
80 | end
81 |
82 | local resp_body_lom, err = lom.parse(response.body)
83 | if not resp_body_lom then
84 | return nil, "failed to parse response body: " .. err
85 | end
86 |
87 | local cred_lom = lom.find_elem(lom.find_elem(resp_body_lom, "AssumeRoleWithWebIdentityResult"), "Credentials")
88 |
89 | local AccessKeyId = lom.find_elem(cred_lom, "AccessKeyId")[1]
90 | local SecretAccessKey = lom.find_elem(cred_lom, "SecretAccessKey")[1]
91 | local SessionToken = lom.find_elem(cred_lom, "SessionToken")[1]
92 | local Expiration = lom.find_elem(cred_lom, "Expiration")[1]
93 |
94 | self:set(AccessKeyId, SecretAccessKey, SessionToken, Expiration)
95 |
96 | return true
97 | end
98 |
99 | return TokenFileWebIdentityCredentials
100 |
--------------------------------------------------------------------------------
/src/resty/aws/request/execute.lua:
--------------------------------------------------------------------------------
1 | local http = require "resty.luasocket.http"
2 |
3 | local json_safe = require("cjson.safe").new()
4 | json_safe.decode_array_with_array_mt(true)
5 | local json_decode = json_safe.decode
6 |
7 | -- TODO: retries and back-off: https://docs.aws.amazon.com/general/latest/gr/api-retries.html
8 |
9 | -- implement AWS api protocols.
10 | -- returns a response table;
11 | -- * status: status code
12 | -- * reason: status description
13 | -- * headers: table with response headers
14 | -- * body: string with the raw body
15 | --
16 | -- Input parameters:
17 | -- * signed_request table
18 | local function execute_request(signed_request)
19 |
20 | local httpc = http.new()
21 | httpc:set_timeout(signed_request.timeout or 60000)
22 |
23 | local ok, err = httpc:connect {
24 | host = signed_request.host,
25 | port = signed_request.port,
26 | scheme = signed_request.tls and "https" or "http",
27 | ssl_server_name = signed_request.host,
28 | ssl_verify = signed_request.ssl_verify,
29 | proxy_opts = signed_request.proxy_opts,
30 | }
31 | if not ok then
32 | return nil, ("failed to connect to '%s://%s:%s': %s"):format(
33 | signed_request.tls and "https" or "http",
34 | tostring(signed_request.host),
35 | tostring(signed_request.port),
36 | tostring(err))
37 | end
38 |
39 | local response, err = httpc:request({
40 | path = signed_request.path,
41 | method = signed_request.method,
42 | headers = signed_request.headers,
43 | query = signed_request.query,
44 | body = signed_request.body,
45 | })
46 | if not response then
47 | return nil, ("failed sending request to '%s:%s': %s"):format(
48 | tostring(signed_request.host),
49 | tostring(signed_request.port),
50 | tostring(err))
51 | end
52 |
53 |
54 | local body do
55 | if response.has_body then
56 | body, err = response:read_body()
57 | if not body then
58 | return nil, ("failed reading response body from '%s:%s': %s"):format(
59 | tostring(signed_request.host),
60 | tostring(signed_request.port),
61 | tostring(err))
62 | end
63 | end
64 | end
65 |
66 | if signed_request.keepalive_idle_timeout then
67 | httpc:set_keepalive(signed_request.keepalive_idle_timeout)
68 | else
69 | httpc:close()
70 | end
71 |
72 | local ct = response.headers["Content-Type"]
73 | if (ct and ct:lower():match("application/.*json")) then
74 | -- json body, let's decode
75 | local ok = json_decode(body)
76 | if ok then
77 | body = ok
78 | end
79 | end
80 |
81 | return {
82 | status = response.status,
83 | reason = response.reason,
84 | headers = response.headers,
85 | body = body
86 | }
87 | end
88 |
89 |
90 | return execute_request
91 |
--------------------------------------------------------------------------------
/src/resty/aws/request/sign.lua:
--------------------------------------------------------------------------------
1 |
2 | -- table with signature functions, loaded on demand. Additional signatures can be
3 | -- implemented as modules. Typically the key would be "v4", "v3", etc.
4 | local signatures = setmetatable({}, {
5 | __index = function(self, key)
6 | -- if we do not have a specific signature version, then load it
7 | assert(type(key) == "string", "the signature type must be a string")
8 | if key == "s3" then
9 | key = "v4"
10 | end
11 |
12 | local ok, mod = pcall(require, "resty.aws.request.signatures." .. key)
13 | if not ok then
14 | return error("AWS signature version '"..key.."' does not exist or hasn't been implemented")
15 | end
16 | rawset(self, key, mod)
17 | return mod
18 | end
19 | })
20 |
21 |
22 | return function(config, request)
23 | -- the 'nil' string is to ensure the __index method of 'signatures' throws the
24 | -- proper error message.
25 | return signatures[config.signatureVersion or "nil"](config, request)
26 | end
27 |
--------------------------------------------------------------------------------
/src/resty/aws/request/signatures/none.lua:
--------------------------------------------------------------------------------
1 | -- signature module for "not signing"
2 |
3 |
4 | -- config to contain:
5 | -- config.endpoint: hostname to connect to
6 | --
7 | -- tbl to contain:
8 | -- tbl.domain: optional, defaults to "amazon.com"
9 | -- tbl.region: amazon region identifier, eg. "us-east-1"
10 | -- tbl.service: amazon service targetted, eg. "lambda"
11 | -- tbl.method: GET/POST/etc
12 | -- tbl.path: path to invoke, defaults to 'canonicalURI' if given, or otherwise "/"
13 | -- tbl.query: string with the query parameters, defaults to 'canonical_querystring'
14 | -- tbl.canonical_querystring: if given will be used and override 'query'
15 | -- tbl.headers: table of headers for the request
16 | -- note: for headers "Host" and "Authorization"; they will be used if
17 | -- provided, and not be overridden by the generated ones
18 | -- tbl.body: string, defaults to ""
19 | -- tbl.tls: defaults to true (if nil)
20 | -- tbl.port: defaults to 443 or 80 depending on 'tls'
21 | -- tbl.global_endpoint: if true, then use "us-east-1" as signing region and different
22 | -- hostname template: see https://github.com/aws/aws-sdk-js/blob/ae07e498e77000e55da70b20996dc8fd2f8b3051/lib/region_config_data.json
23 | local function prepare_request(config, request_data)
24 | local host = request_data.host
25 | local port = request_data.port
26 | local timestamp = ngx.time()
27 | local req_date = os.date("!%Y%m%dT%H%M%SZ", timestamp)
28 |
29 | local timeout = config.timeout
30 | local keepalive_idle_timeout = config.keepalive_idle_timeout
31 | local tls = config.tls
32 | local ssl_verify = config.ssl_verify
33 | local proxy_opts = {
34 | http_proxy = config.http_proxy,
35 | https_proxy = config.https_proxy,
36 | no_proxy = config.no_proxy,
37 | }
38 |
39 | local headers = {
40 | ["X-Amz-Date"] = req_date,
41 | ["Host"] = host,
42 | }
43 | for k, v in pairs(request_data.headers or {}) do
44 | headers[k] = v
45 | end
46 |
47 | return {
48 | --url = url, -- "https://lambda.us-east-1.amazon.com:443/some/path?query1=val1"
49 | host = host, -- "lambda.us-east-1.amazon.com"
50 | port = port, -- 443
51 | timeout = timeout, -- 60000
52 | keepalive_idle_timeout = keepalive_idle_timeout, -- 60000
53 | tls = tls, -- true
54 | ssl_verify = ssl_verify, -- true
55 | proxy_opts = proxy_opts, -- table
56 | path = request_data.path, -- "/some/path"
57 | method = request_data.method, -- "GET"
58 | query = request_data.query, -- "query1=val1"
59 | headers = headers, -- table
60 | body = request_data.body, -- string
61 | --target = target, -- "/some/path?query1=val1"
62 | }
63 | end
64 |
65 | return prepare_request
66 |
--------------------------------------------------------------------------------
/src/resty/aws/request/signatures/utils.lua:
--------------------------------------------------------------------------------
1 | -- AWS requests signing utils
2 |
3 | local resty_sha256 = require "resty.sha256"
4 | local openssl_hmac = require "resty.openssl.hmac"
5 |
6 |
7 | local CHAR_TO_HEX = {};
8 | for i = 0, 255 do
9 | local char = string.char(i)
10 | local hex = string.format("%02x", i)
11 | CHAR_TO_HEX[char] = hex
12 | end
13 |
14 |
15 | local function hmac(secret, data)
16 | return openssl_hmac.new(secret, "sha256"):final(data)
17 | end
18 |
19 |
20 | local function hash(str)
21 | local sha256 = resty_sha256:new()
22 | sha256:update(str)
23 | return sha256:final()
24 | end
25 |
26 |
27 | local function hex_encode(str) -- From prosody's util.hex
28 | return (str:gsub(".", CHAR_TO_HEX))
29 | end
30 |
31 |
32 | local function percent_encode(char)
33 | return string.format("%%%02X", string.byte(char))
34 | end
35 |
36 |
37 | local function canonicalise_path(path)
38 | local segments = {}
39 | for segment in path:gmatch("/([^/]*)") do
40 | if segment == "" or segment == "." then
41 | segments = segments -- do nothing and avoid lint
42 | elseif segment == " .. " then
43 | -- intentionally discards components at top level
44 | segments[#segments] = nil
45 | else
46 | segments[#segments+1] = ngx.unescape_uri(segment):gsub("[^%w%-%._~]",
47 | percent_encode)
48 | end
49 | end
50 | local len = #segments
51 | if len == 0 then
52 | return "/"
53 | end
54 | -- If there was a slash on the end, keep it there.
55 | if path:sub(-1, -1) == "/" then
56 | len = len + 1
57 | segments[len] = ""
58 | end
59 | segments[0] = ""
60 | segments = table.concat(segments, "/", 0, len)
61 | return segments
62 | end
63 |
64 |
65 | local function canonicalise_query_string(query)
66 | local q = {}
67 | if type(query) == "string" then
68 | for key, val in query:gmatch("([^&=]+)=?([^&]*)") do
69 | key = ngx.unescape_uri(key):gsub("[^%w%-%._~]", percent_encode)
70 | val = ngx.unescape_uri(val):gsub("[^%w%-%._~]", percent_encode)
71 | q[#q+1] = key .. "=" .. val
72 | end
73 |
74 | elseif type(query) == "table" then
75 | for key, val in pairs(query) do
76 | key = ngx.unescape_uri(key):gsub("[^%w%-%._~]", percent_encode)
77 | val = ngx.unescape_uri(val):gsub("[^%w%-%._~]", percent_encode)
78 | q[#q+1] = key .. "=" .. val
79 | end
80 |
81 | else
82 | error("bad query type, expected string or table, got: ".. type(query))
83 | end
84 |
85 | table.sort(q)
86 | return table.concat(q, "&")
87 | end
88 |
89 |
90 | local function add_args_to_query_string(query_args, query_string, sort)
91 | local q = {}
92 | if type(query_args) == "string" then
93 | for key, val in query_args:gmatch("([^&=]+)=?([^&]*)") do
94 | key = tostring(key):gsub("[^%w%-%._~]", percent_encode)
95 | val = tostring(val):gsub("[^%w%-%._~]", percent_encode)
96 | q[#q+1] = key .. "=" .. val
97 | end
98 |
99 | elseif type(query_args) == "table" then
100 | for key, val in pairs(query_args) do
101 | key = tostring(key):gsub("[^%w%-%._~]", percent_encode)
102 | val = tostring(val):gsub("[^%w%-%._~]", percent_encode)
103 | q[#q+1] = key .. "=" .. val
104 | end
105 |
106 | else
107 | error("bad query type, expected string or table, got: ".. type(query_args))
108 | end
109 |
110 | for key, val in query_string:gmatch("([^&=]+)=?([^&]*)") do
111 | key = ngx.unescape_uri(key):gsub("[^%w%-%._~]", percent_encode)
112 | val = ngx.unescape_uri(val):gsub("[^%w%-%._~]", percent_encode)
113 | q[#q+1] = key .. "=" .. val
114 | end
115 |
116 | if sort then
117 | table.sort(q)
118 | end
119 |
120 | return table.concat(q, "&")
121 | end
122 |
123 |
124 | local function derive_signing_key(kSecret, date, region, service)
125 | -- TODO: add an LRU cache to cache the generated keys?
126 | local kDate = hmac("AWS4" .. kSecret, date)
127 | local kRegion = hmac(kDate, region)
128 | local kService = hmac(kRegion, service)
129 | local kSigning = hmac(kService, "aws4_request")
130 | return kSigning
131 | end
132 |
133 |
134 | return {
135 | hmac = hmac,
136 | hash = hash,
137 | hex_encode = hex_encode,
138 | percent_encode = percent_encode,
139 | canonicalise_path = canonicalise_path,
140 | canonicalise_query_string = canonicalise_query_string,
141 | derive_signing_key = derive_signing_key,
142 | add_args_to_query_string = add_args_to_query_string,
143 | }
144 |
--------------------------------------------------------------------------------
/src/resty/aws/request/signatures/v4.lua:
--------------------------------------------------------------------------------
1 | -- Performs AWSv4 Signing
2 | -- http://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
3 |
4 | local pl_string = require "pl.stringx"
5 |
6 | local utils = require "resty.aws.request.signatures.utils"
7 | local hmac = utils.hmac
8 | local hash = utils.hash
9 | local hex_encode = utils.hex_encode
10 | local canonicalise_path = utils.canonicalise_path
11 | local canonicalise_query_string = utils.canonicalise_query_string
12 | local derive_signing_key = utils.derive_signing_key
13 |
14 |
15 | local ALGORITHM = "AWS4-HMAC-SHA256"
16 |
17 |
18 | -- config to contain:
19 | -- config.endpoint: hostname to connect to
20 | -- config.credentials: the Credentials class to use
21 | --
22 | -- tbl to contain:
23 | -- tbl.domain: optional, defaults to "amazon.com"
24 | -- tbl.region: amazon region identifier, eg. "us-east-1"
25 | -- tbl.service: amazon service targetted, eg. "lambda"
26 | -- tbl.method: GET/POST/etc
27 | -- tbl.path: path to invoke, defaults to 'canonicalURI' if given, or otherwise "/"
28 | -- tbl.canonicalURI: if given will be used and override 'path'
29 | -- tbl.query: string with the query parameters, defaults to 'canonical_querystring'
30 | -- tbl.canonical_querystring: if given will be used and override 'query'
31 | -- tbl.headers: table of headers for the request
32 | -- note: for headers "Host" and "Authorization"; they will be used if
33 | -- provided, and not be overridden by the generated ones
34 | -- tbl.body: string, defaults to ""
35 | -- tbl.timeout: number socket timeout (in ms), defaults to 60000
36 | -- tbl.keepalive_idle_timeout: number keepalive idle timeout (in ms), no keepalive if nil
37 | -- tbl.tls: defaults to true (if nil)
38 | -- tbl.ssl_verify: defaults to true (if nil)
39 | -- tbl.port: defaults to 443 or 80 depending on 'tls'
40 | -- tbl.timestamp: number defaults to 'ngx.time()''
41 | -- tbl.global_endpoint: if true, then use "us-east-1" as signing region and different
42 | -- hostname template: see https://github.com/aws/aws-sdk-js/blob/ae07e498e77000e55da70b20996dc8fd2f8b3051/lib/region_config_data.json
43 | local function prepare_awsv4_request(config, request_data)
44 | local region = config.signingRegion or config.region
45 | local service = config.endpointPrefix or config.targetPrefix -- TODO: targetPrefix as fallback, correct???
46 | local request_method = request_data.method -- TODO: should this get a fallback/default??
47 |
48 | local canonicalURI = request_data.canonicalURI
49 | local path = request_data.path
50 | if path and not canonicalURI then
51 | canonicalURI = canonicalise_path(path)
52 | elseif canonicalURI == nil or canonicalURI == "" then
53 | canonicalURI = "/"
54 | end
55 |
56 | local canonical_querystring = request_data.canonical_querystring
57 | local query = request_data.query
58 | if query and not canonical_querystring then
59 | canonical_querystring = canonicalise_query_string(query)
60 | end
61 |
62 | local req_headers = request_data.headers
63 | local req_payload = request_data.body
64 |
65 | -- get credentials
66 | local access_key, secret_key, session_token do
67 | if not config.credentials then
68 | return nil, "cannot sign request without 'config.credentials'"
69 | end
70 | local success
71 | success, access_key, secret_key, session_token = config.credentials:get()
72 | if not success then
73 | return nil, "failed to get credentials: " .. tostring(access_key)
74 | end
75 | end
76 |
77 | local timeout = config.timeout
78 | local keepalive_idle_timeout = config.keepalive_idle_timeout
79 | local tls = config.tls
80 | local ssl_verify = config.ssl_verify
81 | local proxy_opts = {
82 | http_proxy = config.http_proxy,
83 | https_proxy = config.https_proxy,
84 | no_proxy = config.no_proxy,
85 | }
86 |
87 | local host = request_data.host
88 | local port = request_data.port
89 | local timestamp = ngx.time()
90 | local req_date = os.date("!%Y%m%dT%H%M%SZ", timestamp)
91 | local date = os.date("!%Y%m%d", timestamp)
92 |
93 | local headers = {
94 | ["X-Amz-Date"] = req_date,
95 | ["Host"] = host,
96 | ["X-Amz-Security-Token"] = session_token,
97 | }
98 |
99 | local S3 = config.signatureVersion == "s3"
100 |
101 | local hashed_payload = hex_encode(hash(req_payload or ""))
102 |
103 | -- Special handling of S3
104 | -- https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html#:~:text=Unsigned%20payload%20option
105 | if S3 then
106 | headers["X-Amz-Content-Sha256"] = hashed_payload
107 | end
108 |
109 | local add_auth_header = true
110 | for k, v in pairs(req_headers) do
111 | k = k:gsub("%f[^%z-]%w", string.upper) -- convert to standard header title case
112 | if k == "Authorization" then
113 | add_auth_header = false
114 | elseif v == false then -- don't allow a default value for this header
115 | v = nil
116 | end
117 | headers[k] = v
118 | end
119 |
120 | -- Task 1: Create a Canonical Request For Signature Version 4
121 | -- http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
122 | local canonical_headers, signed_headers do
123 | -- We structure this code in a way so that we only have to sort once.
124 | canonical_headers, signed_headers = {}, {}
125 | local i = 0
126 | for name, value in pairs(headers) do
127 | if value then -- ignore headers with 'false', they are used to override defaults
128 | i = i + 1
129 | local name_lower = name:lower()
130 | signed_headers[i] = name_lower
131 | if canonical_headers[name_lower] ~= nil then
132 | return nil, "header collision"
133 | end
134 | canonical_headers[name_lower] = pl_string.strip(tostring(value))
135 | end
136 | end
137 | table.sort(signed_headers)
138 | for j=1, i do
139 | local name = signed_headers[j]
140 | local value = canonical_headers[name]
141 | canonical_headers[j] = name .. ":" .. value .. "\n"
142 | end
143 | signed_headers = table.concat(signed_headers, ";", 1, i)
144 | canonical_headers = table.concat(canonical_headers, nil, 1, i)
145 | end
146 |
147 | local canonical_request =
148 | request_method .. '\n' ..
149 | canonicalURI .. '\n' ..
150 | (canonical_querystring or "") .. '\n' ..
151 | canonical_headers .. '\n' ..
152 | signed_headers .. '\n' ..
153 | hashed_payload
154 |
155 | local hashed_canonical_request = hex_encode(hash(canonical_request))
156 |
157 | -- Task 2: Create a String to Sign for Signature Version 4
158 | -- http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
159 | local credential_scope = date .. "/" .. region .. "/" .. service .. "/aws4_request"
160 | local string_to_sign =
161 | ALGORITHM .. '\n' ..
162 | req_date .. '\n' ..
163 | credential_scope .. '\n' ..
164 | hashed_canonical_request
165 |
166 | -- Task 3: Calculate the AWS Signature Version 4
167 | -- http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
168 | local signing_key = derive_signing_key(secret_key, date, region, service)
169 | local signature = hex_encode(hmac(signing_key, string_to_sign))
170 |
171 | -- Task 4: Add the Signing Information to the Request
172 | -- http://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html
173 | local authorization = ALGORITHM
174 | .. " Credential=" .. access_key .. "/" .. credential_scope
175 | .. ", SignedHeaders=" .. signed_headers
176 | .. ", Signature=" .. signature
177 | if add_auth_header then
178 | headers.Authorization = authorization
179 | end
180 |
181 | -- local target = path or canonicalURI
182 | -- if query or canonical_querystring then
183 | -- target = target .. "?" .. (query or canonical_querystring)
184 | -- end
185 | -- local scheme = tls and "https" or "http"
186 | -- local url = scheme .. "://" .. host_header .. target
187 |
188 | return {
189 | --url = url, -- "https://lambda.us-east-1.amazon.com:443/some/path?query1=val1"
190 | host = host, -- "lambda.us-east-1.amazon.com"
191 | port = port, -- 443
192 | timeout = timeout, -- 60000
193 | keepalive_idle_timeout = keepalive_idle_timeout, -- 60000
194 | tls = tls, -- true
195 | ssl_verify = ssl_verify, -- true
196 | proxy_opts = proxy_opts, -- table
197 | path = path or canonicalURI, -- "/some/path"
198 | method = request_method, -- "GET"
199 | query = query or canonical_querystring, -- "query1=val1"
200 | headers = headers, -- table
201 | body = req_payload, -- string
202 | --target = target, -- "/some/path?query1=val1"
203 | }
204 | end
205 |
206 | return prepare_awsv4_request
207 |
--------------------------------------------------------------------------------
/src/resty/aws/service/rds/signer.lua:
--------------------------------------------------------------------------------
1 | --- Signer class for RDS tokens for RDS DB access.
2 | --
3 | -- See [IAM database authentication for MariaDB, MySQL, and PostgreSQL](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.html)
4 | -- for more information on using IAM database authentication with RDS.
5 | --
6 | -- RDS services created will get a `Signer` method to create an instance. The `Signer` will
7 | -- inherit its configuration from the `AWS` instance (not from the RDS instance!).
8 |
9 | local httpc = require("resty.luasocket.http")
10 | local presign_awsv4_request = require("resty.aws.request.signatures.presign")
11 |
12 | local RDS_IAM_AUTH_EXPIRE_TIME = 15 * 60
13 |
14 | --- Return an authorization token used as the password for a RDS DB connection.
15 | -- The example shows how to use `getAuthToken` to create an authentication
16 | -- token for connecting to a PostgreSQL database in RDS.
17 | -- @name Signer:getAuthToken
18 | -- @tparam table opts configuration to use, to override the options inherited from the underlying `AWS` instance;
19 | -- @tparam string opts.region The AWS region
20 | -- @tparam string opts.hostname the DB hostname to connect to, eg. `"DB_INSTANCE.DB_CLUSTER.us-east-1.rds.amazonaws.com"`
21 | -- @tparam number opts.port the port for the DB connection
22 | -- @tparam string opts.username username of the account in the database to sign in with
23 | -- @tparam Credentials opts.credentials aws credentials
24 | -- @return token, err - Returns the token to use as the password for the DB connection, or nil and error if an error occurs
25 | -- @usage
26 | -- local pgmoon = require "pgmoon"
27 | -- local AWS = require("resty.aws")
28 | -- local AWS_global_config = require("resty.aws.config").global
29 | -- local aws = AWS { region = AWS_global_config.region }
30 | -- local rds = aws:RDS()
31 | --
32 | --
33 | -- local db_hostname = "DB_INSTANCE.DB_CLUSTER.us-east-1.rds.amazonaws.com"
34 | -- local db_port = 5432
35 | -- local db_name = "DB_NAME"
36 | --
37 | -- local signer = rds:Signer { -- create a signer instance
38 | -- hostname = db_hostname,
39 | -- username = "db_user",
40 | -- port = db_port,
41 | -- region = nil, -- will be inherited from `aws`
42 | -- credentials = nil, -- will be inherited from `aws`
43 | -- }
44 | --
45 | -- -- use the 'signer' to generate the token, whilst overriding some options
46 | -- local auth_token, err = signer:getAuthToken {
47 | -- username = "another_user" -- this overrides the earlier provided config above
48 | -- }
49 | --
50 | -- if err then
51 | -- ngx.log(ngx.ERR, "Failed to build auth token: ", err)
52 | -- return
53 | -- end
54 | --
55 | -- local pg = pgmoon.new({
56 | -- host = db_hostname,
57 | -- port = db_port,
58 | -- database = db_name,
59 | -- user = "another_user",
60 | -- password = auth_token,
61 | -- ssl = true,
62 | -- })
63 | --
64 | -- local flag, err = pg:connect()
65 | -- if err then
66 | -- ngx.log(ngx.ERR, "Failed to connect to database: ", err)
67 | -- return
68 | -- end
69 | --
70 | -- -- Test query
71 | -- assert(pg:query("select * from users where status = 'active' limit 20"))
72 | local function getAuthToken(self, opts) --endpoint, region, db_user)
73 | opts = setmetatable(opts or {}, { __index = self.config }) -- lookup missing params in inherited config
74 |
75 | local region = assert(opts.region, "parameter 'region' not set")
76 | local hostname = assert(opts.hostname, "parameter 'hostname' not set")
77 | local port = assert(opts.port, "parameter 'port' not set")
78 | local username = assert(opts.username, "parameter 'username' not set")
79 |
80 | local endpoint = hostname .. ":" .. port
81 | if endpoint:sub(1,7) ~= "http://" and endpoint:sub(1,8) ~= "https://" then
82 | endpoint = "https://" .. endpoint
83 | end
84 |
85 | local query_args = "Action=connect&DBUser=" .. username
86 |
87 | local canonical_request_url = endpoint .. "/?" .. query_args
88 | local scheme, host, port, path, query = unpack(httpc:parse_uri(canonical_request_url, false))
89 | local req_data = {
90 | method = "GET",
91 | scheme = scheme,
92 | tls = scheme == "https",
93 | host = host,
94 | port = port,
95 | path = path,
96 | query = query,
97 | headers = {
98 | ["Host"] = host .. ":" .. port,
99 | },
100 | }
101 |
102 | local presigned_request, err = presign_awsv4_request(self.config, req_data, opts.signingName, region, RDS_IAM_AUTH_EXPIRE_TIME)
103 | if err then
104 | return nil, err
105 | end
106 |
107 | return presigned_request.host .. ":" .. presigned_request.port .. presigned_request.path .. "?" .. presigned_request.query
108 | end
109 |
110 |
111 | -- signature: intended to be a method on the RDS service object, rds_instance == self in that case
112 | return function(rds_instance, config)
113 | local token_instance = {
114 | config = {},
115 | getAuthToken = getAuthToken, -- injected method for token generation
116 | }
117 |
118 | -- first copy the inherited config elements NOTE: inherits from AWS, not the rds_instance!!!
119 | for k,v in pairs(rds_instance.aws.config) do
120 | token_instance.config[k] = v
121 | end
122 |
123 | -- service specifics
124 | -- see https://github.com/aws/aws-sdk-js/blob/9295e45fdcda93b62f8c1819e924cdb4fb378199/lib/rds/signer.js#L11-L15
125 | token_instance.config.signatureVersion = "v4"
126 | token_instance.config.signingName = "rds-db"
127 |
128 | -- then add/overwrite with provided config
129 | for k,v in pairs(config or {}) do
130 | token_instance.config[k] = v
131 | end
132 |
133 | return token_instance
134 | end
135 |
--------------------------------------------------------------------------------
/test2.lua:
--------------------------------------------------------------------------------
1 | setmetatable(_G, nil) -- disable global warnings
2 |
3 | -- make sure we can use dev code
4 | package.path = "./src/?.lua;./src/?/init.lua;"..package.path
5 |
6 | -- quick debug dump function
7 | local dump = function(...)
8 | local t = { n = select("#", ...), ...}
9 | if t.n == 1 and type(t[1]) == "table" then t = t[1] end
10 | print(require("pl.pretty").write(t))
11 | end
12 |
13 |
14 |
15 |
16 |
17 | local AWS = require("resty.aws")
18 | local aws = AWS()
19 | local secretsmanager = aws:SecretsManager { region = "us-east-2" }
20 |
21 | dump(secretsmanager:getSecretValue {
22 | SecretId = "arn:aws:secretsmanager:us-east-2:238406704566:secret:test2-HN1F1k",
23 | VersionStage = "AWSCURRENT",
24 | })
25 |
26 |
27 |
28 | --[[
29 | local secret = assert(credentials.fetch_secret(creds, {
30 | SecretId = "arn:aws:secretsmanager:us-east-2:238406704566:secret:test2-HN1F1k",
31 | }))
32 |
33 | dump(secret)
34 |
35 | local secret = assert(credentials.fetch_secret(creds, {
36 | SecretId = "arn:aws:secretsmanager:us-east-2:238406704566:secret:KEY_VALUE_test-IHwf2S",
37 | }))
38 |
39 | dump(secret)
40 |
41 | local secret = assert(credentials.fetch_secret(creds, {
42 | SecretId = "arn:aws:secretsmanager:us-east-2:238406704566:secret:test3_plain_text-PDPBwp",
43 | }))
44 |
45 | dump(secret)
46 | --]]
47 |
48 | --require "resty.credentials.aws.api"
49 |
--------------------------------------------------------------------------------
/update_api_files.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # This can be run from the Makefile.
4 |
5 | # script to update the AWS SDK from it's source repository (the JS sdk)
6 | # It will convert the service descriptions of the specified SDK version
7 | # (see SDK_VERSION_TAG) into Lua modules and generate a rockspec.
8 |
9 | SDK_VERSION_TAG=v2.751.0
10 |
11 | # ----------- nothing to customize below -----------
12 | TARGET=./src/resty/aws/raw-api
13 | SOURCE=./delete-me
14 | TFILE=$(mktemp)
15 | set -e
16 | pushd "$(dirname "$(realpath "$0")")" > /dev/null
17 |
18 |
19 | # clone repo at requested version
20 | if [ -d $SOURCE ]; then
21 | echo "directory $SOURCE already exists, delete before updating"
22 | exit 1
23 | fi
24 | git clone --branch=$SDK_VERSION_TAG --depth=1 https://github.com/aws/aws-sdk-js.git $SOURCE
25 |
26 |
27 | # get a list of API files
28 | file_list=()
29 | pushd $SOURCE/apis/ > /dev/null
30 | for file_name in `ls -v *.normal.json` ; do
31 | file_list+=("${file_name%.normal.json}")
32 | done
33 | popd > /dev/null
34 |
35 | # remove existing files
36 | echo "removing: $TARGET"
37 | rm -rf "$TARGET"
38 | echo "creating: $TARGET"
39 | mkdir -p "$TARGET"
40 |
41 | # Create destination file in Lua format with hardcoded json in there
42 | echo "adding: $TARGET/_README.md"
43 | cat < $TARGET/_README.md
44 | # WARNING
45 |
46 | Everything in this directory is generated, do not modify, changes will be lost.
47 |
48 | To regenerate use the update script in the top directory of this repo.
49 | EOF
50 |
51 |
52 | # create TOC
53 | FILENAME=$TARGET/table_of_contents.lua
54 | echo "adding: $FILENAME"
55 | echo "return {" >> $FILENAME
56 | for f in "${file_list[@]}"; do
57 | source_file=$SOURCE/apis/$f.normal.json
58 | service_id=$(jq -r '.metadata.serviceId' $source_file | tr -d ' ')
59 | # replace . with - since . can't be in a Lua module name
60 | f=${f//./-}
61 | echo ' "'"$service_id:$f"'",' >> $FILENAME
62 | done
63 | echo "}" >> $FILENAME
64 |
65 |
66 | # copy region config file
67 | FILENAME=$TARGET/region_config_data.lua
68 | echo "adding: $FILENAME"
69 | echo 'local decode = require("cjson").new().decode' >> "$FILENAME"
70 | echo "return assert(decode([===[" >> "$FILENAME"
71 | cat $SOURCE/lib/region_config_data.json >> "$FILENAME"
72 | echo "" >> "$FILENAME"
73 | echo "]===]))" >> "$FILENAME"
74 |
75 | # Copy the individual API files
76 | for f in "${file_list[@]}"; do
77 | source_file=$SOURCE/apis/$f.normal.json
78 | # remove example keys from documentation to prevent security reports from being triggered
79 | jq 'walk( if (type == "object") and has("documentation") and (.documentation|contains("wJalrXUtnFEMI")) then del(.documentation) else . end )' "$source_file" >| "$TFILE"
80 | mv -f "$TFILE" "$source_file"; touch "$TFILE"
81 | # replace . with - since . can't be in a Lua module name
82 | target_file=$TARGET/${f//./-}.lua
83 | echo "adding: $target_file"
84 | echo 'local decode = require("cjson").new().decode' >> "$target_file"
85 | echo 'return assert(decode([===[' >> "$target_file"
86 | cat "$source_file" >> "$target_file"
87 | echo "" >> "$target_file"
88 | echo "]===]))" >> "$target_file"
89 | done
90 |
91 | # update the rockspec
92 | echo "writing rockspec file"
93 | rockspec=lua-resty-aws-dev-1.rockspec
94 | if [ -f $rockspec ]; then
95 | rm $rockspec
96 | fi
97 |
98 | echo "-- do not edit this file, it is generated and will be overwritten" >> $rockspec
99 | while IFS= read -r line; do
100 | echo "$line" >> $rockspec
101 | if [[ "$line" =~ "--START-MARKER--" ]]; then
102 | break
103 | fi
104 | done < lua-resty-aws-dev-1.rockspec.template
105 |
106 | for f in "${file_list[@]}"; do
107 | target_file=${f//./-}
108 | echo " [\"resty.aws.raw-api.$target_file\"] = \"src/resty/aws/raw-api/$target_file.lua\"," >> $rockspec
109 | done
110 |
111 | foundmarker=false
112 | while IFS= read -r line; do
113 | if [[ "$line" =~ "--END-MARKER--" ]]; then
114 | foundmarker=true
115 | fi
116 | if [[ $foundmarker == true ]]; then
117 | echo "$line" >> $rockspec
118 | fi
119 | done < lua-resty-aws-dev-1.rockspec.template
120 |
121 | rm -rf $SOURCE
122 | popd > /dev/null
123 |
124 | echo "Update complete"
125 |
--------------------------------------------------------------------------------
/upload.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Use "make upload" to invoke this script
4 |
5 | ROCK_VERSION=$1-1
6 | LR_API_KEY=$2
7 | #LR_API_KEY=INfSIgkuArccxH9zq9M7enqackTiYtgRM6c9l6Y4
8 |
9 |
10 | ROCK_FILE=lua-resty-aws-$ROCK_VERSION.src.rock
11 | ROCKSPEC_FILE=lua-resty-aws-$ROCK_VERSION.rockspec
12 |
13 | if [ "$ROCK_VERSION" == "-1" ]; then
14 | echo "First argument (version) is missing."
15 | exit 1
16 | fi
17 | if [ "$LR_API_KEY" == "" ]; then
18 | echo "Second argument (LuaRocks api-key) is missing."
19 | exit 1
20 | fi
21 | if [ ! -f "$ROCKSPEC_FILE" ]; then
22 | echo "File '$ROCKSPEC_FILE' not found"
23 | exit 1
24 | fi
25 | if [ ! -f "$ROCK_FILE" ]; then
26 | echo "File '$ROCK_FILE' not found"
27 | exit 1
28 | fi
29 |
30 | echo "Uploading $ROCKSPEC_FILE..."
31 | curl -f -k -L --silent \
32 | --user-agent "lua-resty-aws upload script via curl" \
33 | --form "rockspec_file=@$ROCKSPEC_FILE" \
34 | --connect-timeout 30 \
35 | "https://luarocks.org/api/1/$LR_API_KEY/upload" \
36 | -o "./upload1.json"
37 |
38 | LR_ROCK_VERSION_ID=$(jq .version.id < upload1.json)
39 | jq < upload1.json
40 | rm ./upload1.json
41 | echo "Rock ID: $LR_ROCK_VERSION_ID"
42 |
43 | echo "Uploading $ROCK_FILE..."
44 | curl -f -k -L --silent \
45 | --user-agent "lua-resty-aws upload script via curl" \
46 | --form "rock_file=@$ROCK_FILE" \
47 | --connect-timeout 30 \
48 | "https://luarocks.org/api/1/$LR_API_KEY/upload_rock/$LR_ROCK_VERSION_ID" \
49 | -o "./upload2.json"
50 |
51 | jq < upload2.json
52 | rm ./upload2.json
53 |
--------------------------------------------------------------------------------