Skip to content

Commit 2ae5328

Browse files
committed
feat(elasticache): add a signer for Elasticache
KAG-7723
1 parent e7b8a8f commit 2ae5328

File tree

4 files changed

+213
-2
lines changed

4 files changed

+213
-2
lines changed

lua-resty-aws-dev-1.rockspec.template

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ build = {
4444
["resty.aws.request.signatures.v4"] = "src/resty/aws/request/signatures/v4.lua",
4545
["resty.aws.request.signatures.presign"] = "src/resty/aws/request/signatures/presign.lua",
4646
["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",
47+
["resty.aws.service.rds.signer"] = "src/resty/aws/service/rds/signer.lua",
48+
["resty.aws.service.elasticache.signer"] = "src/resty/aws/service/elasticache/signer.lua",
4849
["resty.aws.credentials.Credentials"] = "src/resty/aws/credentials/Credentials.lua",
4950
["resty.aws.credentials.ChainableTemporaryCredentials"] = "src/resty/aws/credentials/ChainableTemporaryCredentials.lua",
5051
["resty.aws.credentials.CredentialProviderChain"] = "src/resty/aws/credentials/CredentialProviderChain.lua",
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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 REGION = "ap-northeast-1"
22+
local USER = "test"
23+
local CACHE_NAME = "test-cache"
24+
25+
describe("Elasticache utils", function()
26+
local cache, signer
27+
local origin_time
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+
cache = aws:ElastiCache()
41+
signer = cache:Signer {
42+
cachename = CACHE_NAME,
43+
username = USER,
44+
region = REGION, -- override aws config
45+
}
46+
end)
47+
48+
after_each(function()
49+
cache = nil
50+
signer = nil
51+
end)
52+
53+
it("should generate expected IAM auth token with mock key", function()
54+
local auth_token, err = signer:getAuthToken()
55+
local expected_auth_token = "test-cache/?X-Amz-Signature=606f98b24623c1e25deb70c2e98220cee859c39232ee7585aec51c25cb220882&Action=connect&User=test&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=test_id%2F20221104%2Fap-northeast-1%2Felasticache%2Faws4_request&X-Amz-Date=20221104T062611Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host"
56+
assert.is_nil(err)
57+
assert.same(auth_token, expected_auth_token)
58+
end)
59+
60+
it("should generate expected IAM auth token with mock temporary credential", function()
61+
signer.config.credentials = aws:Credentials {
62+
accessKeyId = "test_id2",
63+
secretAccessKey = "test_key2",
64+
sessionToken = "test_token2",
65+
}
66+
local auth_token, err = signer:getAuthToken()
67+
local expected_auth_token = "test-cache/?X-Amz-Signature=88de038c7918f1b733e23d65eb3bc0ee989214c26ed8124cb730ce6e7d7a3e5c&Action=connect&User=test&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=test_id2%2F20221104%2Fap-northeast-1%2Felasticache%2Faws4_request&X-Amz-Date=20221104T062611Z&X-Amz-Expires=900&X-Amz-Security-Token=test_token2&X-Amz-SignedHeaders=host"
68+
assert.is_nil(err)
69+
assert.same(auth_token, expected_auth_token)
70+
end)
71+
end)

src/resty/aws/init.lua

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -498,12 +498,19 @@ function AWS:new(config)
498498
service_config[k] = service_config[k] or v
499499
end
500500

501+
local signer
502+
if service_id == "RDS" then
503+
signer = require("resty.aws.service.rds.signer")
504+
elseif service_id == "ElastiCache" then
505+
signer = require("resty.aws.service.elasticache.signer")
506+
end
507+
501508
local service_instance = {
502509
aws = aws_instance,
503510
config = service_config,
504511
api = api,
505512
-- Add service specific methods:
506-
Signer = (service_id == "RDS") and require("resty.aws.service.rds.signer") or nil
513+
Signer = signer
507514
}
508515

509516
AWS.configureEndpoint(service_instance)
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
--- Signer class for Elasticache tokens for Valkey and Redis OSS access.
2+
3+
-- Elasticache services created will get a `Signer` method to create an instance.
4+
-- The `Signer` will inherit its configuration from the `AWS` instance.
5+
6+
local httpc = require("resty.luasocket.http")
7+
local presign_awsv4_request = require("resty.aws.request.signatures.presign")
8+
9+
local ELASTICACHE_IAM_AUTH_EXPIRE_TIME = 15 * 60
10+
11+
12+
--- The example shows how to use `getAuthToken` to create an authentication
13+
-- token for connecting to a PostgreSQL database in RDS.
14+
-- @name Signer:getAuthToken
15+
-- @tparam table opts configuration to use, to override the options inherited from the underlying `AWS` instance;
16+
-- @tparam string opts.region The AWS region
17+
-- @tparam string opts.cachename the Elasticache instance name to connect to, eg. `"test-cache"`
18+
-- @tparam string opts.username name of the IAM-enabled user configured in the Elasticache User management page.
19+
-- @tparam boolean opts.is_serverless the deployment mode of the cache cluster.
20+
-- @tparam Credentials opts.credentials aws credentials
21+
-- @return token, err - Returns the token to use as the password for the Redis connection, or nil and error if an error occurs
22+
-- @usage
23+
-- local AWS = require("resty.aws")
24+
-- local AWS_global_config = require("resty.aws.config").global
25+
-- local aws = AWS { region = AWS_global_config.region }
26+
-- local cache = aws:ElastiCache()
27+
-- local redis = require "resty.redis"
28+
--
29+
-- local hostname = "HOSTNAME, e.g. test-cache-ppl9c2.serverless.apne1.cache.amazonaws.com"
30+
-- local cachename = "CACHE CLUSTER NAME, e.g. test-cache"
31+
-- local port = 6379
32+
-- local name = "Username e.g. test-user"
33+
--
34+
-- local signer = cache:Signer { -- create a signer instance
35+
-- cachename = cachename,
36+
-- username = name,
37+
-- is_serverless = true,
38+
-- region = nil, -- will be inherited from `aws`
39+
-- credentials = nil, -- will be inherited from `aws`
40+
-- }
41+
--
42+
-- -- use the 'signer' to generate the token, whilst overriding some options
43+
-- local auth_token, err = signer:getAuthToken()
44+
--
45+
-- if err then
46+
-- ngx.log(ngx.ERR, "Failed to build auth token: ", err)
47+
-- return
48+
-- end
49+
-- print(auth_token)
50+
--
51+
-- local red = redis:new()
52+
-- --red:set_timeouts(1000, 1000, 1000)
53+
--
54+
-- local ok, err = red:connect(hostname, port, { ssl = true })
55+
-- if not ok then
56+
-- print("failed to connect: ", err)
57+
-- return
58+
-- end
59+
--
60+
-- local res, err = red:auth(name, auth_token)
61+
-- if not res then
62+
-- print("failed to authenticate: ", err)
63+
-- return
64+
-- end
65+
--
66+
-- print("OK")
67+
68+
69+
local function getAuthToken(self, opts) --cachename, region, username, is_serverless)
70+
opts = setmetatable(opts or {}, { __index = self.config }) -- lookup missing params in inherited config
71+
72+
local region = assert(opts.region, "parameter 'region' not set")
73+
local cachename = assert(opts.cachename, "parameter 'cachename' not set")
74+
local username = assert(opts.username, "parameter 'username' not set")
75+
76+
local endpoint = cachename
77+
if endpoint:sub(1,7) ~= "http://" then
78+
endpoint = "http://" .. endpoint
79+
end
80+
81+
local query_args = "Action=connect&User=" .. username
82+
if opts.is_serverless then
83+
query_args = query_args .. "&ResourceType=ServerlessCache"
84+
end
85+
86+
local canonical_request_url = endpoint .. "/?" .. query_args
87+
local scheme, host, port, path, query = unpack(httpc:parse_uri(canonical_request_url, false))
88+
local req_data = {
89+
method = "GET",
90+
scheme = scheme,
91+
tls = scheme == "https",
92+
host = host,
93+
port = port,
94+
path = path,
95+
query = query,
96+
headers = {
97+
["Host"] = host,
98+
},
99+
}
100+
101+
local presigned_request, err = presign_awsv4_request(self.config, req_data, opts.signingName, region, ELASTICACHE_IAM_AUTH_EXPIRE_TIME)
102+
if err then
103+
return nil, err
104+
end
105+
106+
return presigned_request.host .. presigned_request.path .. "?" .. presigned_request.query
107+
end
108+
109+
110+
-- signature: intended to be a method on the Elasticache service object, cache_instance == self in that case
111+
return function(cache_instance, config)
112+
local token_instance = {
113+
config = {},
114+
getAuthToken = getAuthToken, -- injected method for token generation
115+
}
116+
117+
-- first copy the inherited config elements NOTE: inherits from AWS, not the cache_instance!!!
118+
for k,v in pairs(cache_instance.aws.config) do
119+
token_instance.config[k] = v
120+
end
121+
122+
-- service specifics
123+
token_instance.config.signatureVersion = "v4"
124+
token_instance.config.signingName = "elasticache"
125+
126+
-- then add/overwrite with provided config
127+
for k,v in pairs(config or {}) do
128+
token_instance.config[k] = v
129+
end
130+
131+
return token_instance
132+
end

0 commit comments

Comments
 (0)