Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lua-resty-aws-dev-1.rockspec.template
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ build = {
["resty.aws.request.signatures.v4"] = "src/resty/aws/request/signatures/v4.lua",
["resty.aws.request.signatures.presign"] = "src/resty/aws/request/signatures/presign.lua",
["resty.aws.request.signatures.none"] = "src/resty/aws/request/signatures/none.lua",
["resty.aws.service.rds.signer"] = "src/resty/aws/service/rds/signer.lua",
["resty.aws.service.rds.signer"] = "src/resty/aws/service/rds/signer.lua",
["resty.aws.service.elasticache.signer"] = "src/resty/aws/service/elasticache/signer.lua",
["resty.aws.credentials.Credentials"] = "src/resty/aws/credentials/Credentials.lua",
["resty.aws.credentials.ChainableTemporaryCredentials"] = "src/resty/aws/credentials/ChainableTemporaryCredentials.lua",
["resty.aws.credentials.CredentialProviderChain"] = "src/resty/aws/credentials/CredentialProviderChain.lua",
Expand Down
71 changes: 71 additions & 0 deletions spec/04-services/06-elasticache_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
setmetatable(_G, nil)

-- -- hock request sending
-- package.loaded["resty.aws.request.execute"] = function(...)
-- return ...
-- end

local AWS = require("resty.aws")
local AWS_global_config = require("resty.aws.config").global

local config = AWS_global_config
local aws = AWS(config)

aws.config.credentials = aws:Credentials {
accessKeyId = "test_id",
secretAccessKey = "test_key",
}

aws.config.region = "test_region"

local REGION = "ap-northeast-1"
local USER = "test"
local CACHE_NAME = "test-cache"

describe("Elasticache utils", function()
local cache, signer
local origin_time
setup(function()
origin_time = ngx.time
ngx.time = function () --luacheck: ignore
return 1667543171
end
end)

teardown(function ()
ngx.time = origin_time --luacheck: ignore
end)

before_each(function()
cache = aws:ElastiCache()
signer = cache:Signer {
cachename = CACHE_NAME,
username = USER,
region = REGION, -- override aws config
}
end)

after_each(function()
cache = nil
signer = nil
end)

it("should generate expected IAM auth token with mock key", function()
local auth_token, err = signer:getAuthToken()
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"
assert.is_nil(err)
assert.same(auth_token, expected_auth_token)
end)

it("should generate expected IAM auth token with mock temporary credential", function()
signer.config.credentials = aws:Credentials {
accessKeyId = "test_id2",
secretAccessKey = "test_key2",
sessionToken = "test_token2",
}
local auth_token, err = signer:getAuthToken()
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"
assert.is_nil(err)
assert.same(auth_token, expected_auth_token)
end)
end)
9 changes: 8 additions & 1 deletion src/resty/aws/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -498,12 +498,19 @@ function AWS:new(config)
service_config[k] = service_config[k] or v
end

local signer
if service_id == "RDS" then
signer = require("resty.aws.service.rds.signer")
elseif service_id == "ElastiCache" then
signer = require("resty.aws.service.elasticache.signer")
end

local service_instance = {
aws = aws_instance,
config = service_config,
api = api,
-- Add service specific methods:
Signer = (service_id == "RDS") and require("resty.aws.service.rds.signer") or nil
Signer = signer
}

AWS.configureEndpoint(service_instance)
Expand Down
132 changes: 132 additions & 0 deletions src/resty/aws/service/elasticache/signer.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
--- Signer class for Elasticache tokens for Valkey and Redis OSS access.

-- Elasticache services created will get a `Signer` method to create an instance.
-- The `Signer` will inherit its configuration from the `AWS` instance.

local httpc = require("resty.luasocket.http")
local presign_awsv4_request = require("resty.aws.request.signatures.presign")

local ELASTICACHE_IAM_AUTH_EXPIRE_TIME = 15 * 60


--- The example shows how to use `getAuthToken` to create an authentication
-- token for connecting to a PostgreSQL database in RDS.
-- @name Signer:getAuthToken
-- @tparam table opts configuration to use, to override the options inherited from the underlying `AWS` instance;
-- @tparam string opts.region The AWS region
-- @tparam string opts.cachename the Elasticache instance name to connect to, eg. `"test-cache"`
-- @tparam string opts.username name of the IAM-enabled user configured in the Elasticache User management page.
-- @tparam boolean opts.is_serverless the deployment mode of the cache cluster.
-- @tparam Credentials opts.credentials aws credentials
-- @return token, err - Returns the token to use as the password for the Redis connection, or nil and error if an error occurs
-- @usage
-- local AWS = require("resty.aws")
-- local AWS_global_config = require("resty.aws.config").global
-- local aws = AWS { region = AWS_global_config.region }
-- local cache = aws:ElastiCache()
-- local redis = require "resty.redis"
--
-- local hostname = "HOSTNAME, e.g. test-cache-ppl9c2.serverless.apne1.cache.amazonaws.com"
-- local cachename = "CACHE CLUSTER NAME, e.g. test-cache"
-- local port = 6379
-- local name = "Username e.g. test-user"
--
-- local signer = cache:Signer { -- create a signer instance
-- cachename = cachename,
-- username = name,
-- is_serverless = true,
-- region = nil, -- will be inherited from `aws`
-- credentials = nil, -- will be inherited from `aws`
-- }
--
-- -- use the 'signer' to generate the token, whilst overriding some options
-- local auth_token, err = signer:getAuthToken()
--
-- if err then
-- ngx.log(ngx.ERR, "Failed to build auth token: ", err)
-- return
-- end
-- print(auth_token)
--
-- local red = redis:new()
-- --red:set_timeouts(1000, 1000, 1000)
--
-- local ok, err = red:connect(hostname, port, { ssl = true })
-- if not ok then
-- print("failed to connect: ", err)
-- return
-- end
--
-- local res, err = red:auth(name, auth_token)
-- if not res then
-- print("failed to authenticate: ", err)
-- return
-- end
--
-- print("OK")


local function getAuthToken(self, opts) --cachename, region, username, is_serverless)
opts = setmetatable(opts or {}, { __index = self.config }) -- lookup missing params in inherited config

local region = assert(opts.region, "parameter 'region' not set")
local cachename = assert(opts.cachename, "parameter 'cachename' not set")
local username = assert(opts.username, "parameter 'username' not set")

local endpoint = cachename
if endpoint:sub(1,7) ~= "http://" then
endpoint = "http://" .. endpoint
end

local query_args = "Action=connect&User=" .. username
if opts.is_serverless then
query_args = query_args .. "&ResourceType=ServerlessCache"
end

local canonical_request_url = endpoint .. "/?" .. query_args
local scheme, host, port, path, query = unpack(httpc:parse_uri(canonical_request_url, false))
local req_data = {
method = "GET",
scheme = scheme,
tls = scheme == "https",
host = host,
port = port,
path = path,
query = query,
headers = {
["Host"] = host,
},
}

local presigned_request, err = presign_awsv4_request(self.config, req_data, opts.signingName, region, ELASTICACHE_IAM_AUTH_EXPIRE_TIME)
if err then
return nil, err
end

return presigned_request.host .. presigned_request.path .. "?" .. presigned_request.query
end


-- signature: intended to be a method on the Elasticache service object, cache_instance == self in that case
return function(cache_instance, config)
local token_instance = {
config = {},
getAuthToken = getAuthToken, -- injected method for token generation
}

-- first copy the inherited config elements NOTE: inherits from AWS, not the cache_instance!!!
for k,v in pairs(cache_instance.aws.config) do
token_instance.config[k] = v
end

-- service specifics
token_instance.config.signatureVersion = "v4"
token_instance.config.signingName = "elasticache"

-- then add/overwrite with provided config
for k,v in pairs(config or {}) do
token_instance.config[k] = v
end

return token_instance
end