|  | 
|  | 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