diff --git a/src/lua/api-gateway/aws/AWSIAMCredentials.lua b/src/lua/api-gateway/aws/AWSIAMCredentials.lua index 2ad793c..a9fe652 100644 --- a/src/lua/api-gateway/aws/AWSIAMCredentials.lua +++ b/src/lua/api-gateway/aws/AWSIAMCredentials.lua @@ -6,9 +6,11 @@ -- To change this template use File | Settings | File Templates. -- -local cjson = require"cjson" -local http = require"api-gateway.aws.httpclient.http" -local url = require"api-gateway.aws.httpclient.url" +local cjson = require "cjson" +local http = require "api-gateway.aws.httpclient.http" +local url = require "api-gateway.aws.httpclient.url" +local awsDate = require "api-gateway.aws.AwsDateConverter" +local cacheCls = require "api-gateway.cache.cache" local DEFAULT_SECURITY_CREDENTIALS_HOST = "169.254.169.254" local DEFAULT_SECURITY_CREDENTIALS_PORT = "80" @@ -16,6 +18,9 @@ local DEFAULT_SECURITY_CREDENTIALS_URL = "/latest/meta-data/iam/security-credent -- use GET /latest/meta-data/iam/security-credentials/ to auto-discover the IAM Role local DEFAULT_TOKEN_EXPIRATION = 60*60*24 -- in seconds +-- configur cache Manager for IAM crendentials +local iamCache = cacheCls:new() + -- per nginx process cache to store IAM credentials local cache = { IamUser = nil, @@ -26,8 +31,6 @@ local cache = { ExpireAtTimestamp = nil } -local sharedCacheDictInstance - local function tableToString(table_ref) local s = "" local o = table_ref or {} @@ -37,6 +40,21 @@ local function tableToString(table_ref) return s end +local function initIamCache(shared_cache_dict) + local localCache = require "api-gateway.cache.store.localCache":new({ + dict = shared_cache_dict, + ttl = function (value) + local value_o = cjson.decode(value) + ngx.log(ngx.DEBUG, "ExpireAt=", tostring(value_o.ExpireAt)) + local expiryTimeUTC = value.ExpireAtTimestamp or awsDate.convertDateStringToTimestamp(value_o.ExpireAt, true) + local expiryTimeInSeconds = expiryTimeUTC - os.time() + return math.min(DEFAULT_TOKEN_EXPIRATION, expiryTimeInSeconds) + end + }) + + iamCache:addStore(localCache) +end + local AWSIAMCredentials = {} --- @@ -60,7 +78,7 @@ function AWSIAMCredentials:new(o) self.security_credentials_url = o.security_credentials_url or DEFAULT_SECURITY_CREDENTIALS_URL self.shared_cache_dict = o.shared_cache_dict if (o.shared_cache_dict ~= nil) then - sharedCacheDictInstance = ngx.shared[o.shared_cache_dict] + initIamCache(o.shared_cache_dict) end local s = tableToString(o) ngx.log(ngx.DEBUG, "Initializing AWSIAMCredentials with object:", s) @@ -68,65 +86,17 @@ function AWSIAMCredentials:new(o) return o end -local function getTimestamp(dateString, convertToUTC) - local pattern = "(%d+)%-(%d+)%-(%d+)T(%d+):(%d+):(%d+)Z" - local xyear, xmonth, xday, xhour, xminute, - xseconds, xoffset, xoffsethour, xoffsetmin = dateString:match(pattern) - - -- the converted timestamp is in the local timezone - local convertedTimestamp = os.time({ - year = xyear, - month = xmonth, - day = xday, - hour = xhour, - min = xminute, - sec = xseconds - }) - if (convertToUTC == true) then - local offset = os.time() - os.time(os.date("!*t")) - convertedTimestamp = convertedTimestamp + offset - end - return tonumber(convertedTimestamp) -end - -function AWSIAMCredentials:saveCredentialsInSharedDict() - if (sharedCacheDictInstance == nil) then - ngx.log(ngx.WARN, "No shared_cache_dict provided to AWSIAMCredentials. To improve performance please define one.") - return - end - - local expiry_time_utc = getTimestamp(cache.ExpireAt, true) - local expire_in_sec = expiry_time_utc - os.time() - if ( expire_in_sec > 0 ) then - -- set the values and the expiry time - sharedCacheDictInstance:set("AccessKeyId", cache.AccessKeyId, expire_in_sec) - sharedCacheDictInstance:set("SecretAccessKey", cache.SecretAccessKey, expire_in_sec) - sharedCacheDictInstance:set("Token", cache.Token, expire_in_sec) - sharedCacheDictInstance:set("ExpireAt", cache.ExpireAt, expire_in_sec) - sharedCacheDictInstance:set("ExpireAtTimestamp", cache.ExpireAtTimestamp, expire_in_sec) - - ngx.log(ngx.DEBUG, "IAM Credentials cached for ", tostring(expire_in_sec), " seconds in the shared dict=", self.shared_cache_dict) - end -end - function AWSIAMCredentials:loadCredentialsFromSharedDict() - if (sharedCacheDictInstance == nil) then - ngx.log(ngx.WARN, "No shared_cache_dict provided to AWSIAMCredentials. To improve performance please define one.") - return - end - -- see if there's something in the shared dict that didn't expire yet - local accessKeyId = sharedCacheDictInstance:get("AccessKeyId") - if ( accessKeyId == nil ) then - ngx.log(ngx.DEBUG, "nothing found in Shared Cache") - return + local iamCreds = iamCache:get("iam_credentials") + if (iamCreds ~= nil) then + iamCreds = cjson.decode(iamCreds) + cache.AccessKeyId = iamCreds.AccessKeyId + cache.SecretAccessKey = iamCreds.SecretAccessKey + cache.Token = iamCreds.Token + cache.ExpireAt = iamCreds.ExpireAt + cache.ExpireAtTimestamp = iamCreds.ExpireAtTimestamp + ngx.log(ngx.DEBUG, "Cache has been loaded from Shared Cache" ) end - - cache.AccessKeyId = sharedCacheDictInstance:get("AccessKeyId") - cache.SecretAccessKey = sharedCacheDictInstance:get("SecretAccessKey") - cache.Token = sharedCacheDictInstance:get("Token") - cache.ExpireAt = sharedCacheDictInstance:get("ExpireAt") - cache.ExpireAtTimestamp = sharedCacheDictInstance:get("ExpireAtTimestamp") - ngx.log(ngx.DEBUG, "Cache has been loaded from Shared Cache" ) end --- @@ -190,8 +160,10 @@ function AWSIAMCredentials:fetchSecurityCredentialsFromAWS() --local token = url:encodeUrl(aws_response["Token"]) cache.Token = aws_response["Token"] cache.ExpireAt = aws_response["Expiration"] - cache.ExpireAtTimestamp = getTimestamp(cache.ExpireAt, true) - self:saveCredentialsInSharedDict() + cache.ExpireAtTimestamp = awsDate.convertDateStringToTimestamp(cache.ExpireAt, true) + if (cache.ExpireAtTimestamp - os.time() > 0) then + iamCache:put("iam_credentials", cjson.encode(cache)) + end return true end diff --git a/src/lua/api-gateway/aws/AwsDateConverter.lua b/src/lua/api-gateway/aws/AwsDateConverter.lua new file mode 100644 index 0000000..ce28731 --- /dev/null +++ b/src/lua/api-gateway/aws/AwsDateConverter.lua @@ -0,0 +1,49 @@ +--[[ + Copyright 2016 Adobe Systems Incorporated. All rights reserved. + + This file is licensed to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR RESPRESENTATIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + ]] + +-- +-- User: ddascal +-- Date: 19/03/16 +-- Time: 21:40 +-- To change this template use File | Settings | File Templates. +-- + + +local _M = {} + +--- Converts an AWS Date String into a timestamp number +-- @param dateString AWS Date String (i.e. 2016-03-19T06:44:17Z) +-- @param convertToUTC (default false). Boolean value to get the date in UTC or not +-- +local function _convertDateStringToTimestamp(dateString, convertToUTC) + local pattern = "(%d+)%-(%d+)%-(%d+)T(%d+):(%d+):(%d+)Z" + local xyear, xmonth, xday, xhour, xminute, + xseconds, xoffset, xoffsethour, xoffsetmin = dateString:match(pattern) + + -- the converted timestamp is in the local timezone + local convertedTimestamp = os.time({ + year = xyear, + month = xmonth, + day = xday, + hour = xhour, + min = xminute, + sec = xseconds + }) + if (convertToUTC == true) then + local offset = os.time() - os.time(os.date("!*t")) + convertedTimestamp = convertedTimestamp + offset + end + return tonumber(convertedTimestamp) +end + +_M.convertDateStringToTimestamp = _convertDateStringToTimestamp + +return _M diff --git a/test/docker-compose-integration-tests.yml b/test/docker-compose-integration-tests.yml index 3da51f8..cbc6a0d 100644 --- a/test/docker-compose-integration-tests.yml +++ b/test/docker-compose-integration-tests.yml @@ -1,5 +1,5 @@ gateway: - image: adobeapiplatform/apigateway + image: adobeapiplatform/apigateway:latest volumes: - ~/tmp/apiplatform/api-gateway-aws/src/lua/api-gateway/aws:/usr/local/api-gateway/lualib/api-gateway/aws - ~/tmp/apiplatform/api-gateway-aws/test/perl:/tmp/perl diff --git a/test/perl/awsIamCredentials.t b/test/perl/awsIamCredentials.t index 810ddbd..13526fc 100644 --- a/test/perl/awsIamCredentials.t +++ b/test/perl/awsIamCredentials.t @@ -1,5 +1,7 @@ # vim:set ft= ts=4 sw=4 et fdm=marker: use lib 'lib'; +use strict; +use warnings; use Test::Nginx::Socket::Lua; use Cwd qw(cwd); @@ -62,6 +64,8 @@ __DATA__ === TEST 1: test auto discovery of iam user --- http_config eval: $::HttpConfig --- config + error_log ../awsIamCredentials_test1_error.log debug; + location = /latest/meta-data/iam/security-credentials/ { return 200 'test-iam-user'; } @@ -93,6 +97,8 @@ X-Test: test === TEST 2: test Iam can automatically read credentials --- http_config eval: $::HttpConfig --- config + error_log ../awsIamCredentials_test2_error.log debug; + location = /latest/meta-data/iam/security-credentials/ { return 200 'test-iam-user'; } @@ -178,6 +184,8 @@ X-Test: test === TEST 3: test Iam can automatically read credentials with SHARED DICT --- http_config eval: $::HttpConfig --- config + error_log ../awsIamCredentials_test3_error.log debug; + location = /latest/meta-data/iam/security-credentials/ { return 200 'test-iam-user'; } @@ -212,6 +220,7 @@ X-Test: test location /test { content_by_lua ' + local cjson = require "cjson" local IamCredentials = require "api-gateway.aws.AWSIAMCredentials" local iam = IamCredentials:new({ security_credentials_host = "127.0.0.1", @@ -223,7 +232,7 @@ X-Test: test ngx.say("key=" .. key .. ", secret=" .. secret .. ", token=" .. token .. ", date=" .. date .. ", timestamp=" ..timestamp ) local shared_cache = ngx.shared["shared_cache"] - assert( shared_cache:get("AccessKeyId") == nil, "Expired token should not be saved in shared cache") + assert( shared_cache:get("iam_credentials") == nil, "iam_credentials should not be saved in shared cache, but found:" .. tostring(shared_cache:get("iam_credentials"))) -- the previous token should be expired and a new call to fetch credentials should get a new token -- changing the iam_user will cause the IamCredentials to use this one when fetching new credentials @@ -238,12 +247,14 @@ X-Test: test if ( date ~= d ) then error("Dates should match. Got" .. date .. ", Expected: " .. d) end - - assert( shared_cache:get("AccessKeyId") ~= nil, "AccessKeyId should be saved in shared cache") - assert( shared_cache:get("SecretAccessKey") ~= nil, "SecretAccessKey should be saved in shared cache") - assert( shared_cache:get("Token") ~= nil, "Token should be saved in shared cache") - assert( shared_cache:get("ExpireAt") ~= nil, "ExpireAt should be saved in shared cache") - assert( shared_cache:get("ExpireAtTimestamp") ~= nil, "ExpireAtTimestamp should be saved in shared cache") + local cachedIam = shared_cache:get("iam_credentials") + cachedIam = cjson.decode(cachedIam) + assert( cachedIam ~= nil, "iam_credentials should be saved in shared cache") + assert( cachedIam.AccessKeyId ~= nil, "AccessKeyId should be saved in shared cache") + assert( cachedIam.SecretAccessKey ~= nil, "SecretAccessKey should be saved in shared cache") + assert( cachedIam.Token ~= nil, "Token should be saved in shared cache") + assert( cachedIam.ExpireAt ~= nil, "ExpireAt should be saved in shared cache") + assert( cachedIam.ExpireAtTimestamp ~= nil, "ExpireAtTimestamp should be saved in shared cache") ngx.sleep(3)