Skip to content
Merged
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
102 changes: 37 additions & 65 deletions src/lua/api-gateway/aws/AWSIAMCredentials.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,21 @@
-- 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"
local DEFAULT_SECURITY_CREDENTIALS_URL = "/latest/meta-data/iam/security-credentials/"
-- 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,
Expand All @@ -26,8 +31,6 @@ local cache = {
ExpireAtTimestamp = nil
}

local sharedCacheDictInstance

local function tableToString(table_ref)
local s = ""
local o = table_ref or {}
Expand All @@ -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 = {}

---
Expand All @@ -60,73 +78,25 @@ 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)
end
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

---
Expand Down Expand Up @@ -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

Expand Down
49 changes: 49 additions & 0 deletions src/lua/api-gateway/aws/AwsDateConverter.lua
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion test/docker-compose-integration-tests.yml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
25 changes: 18 additions & 7 deletions test/perl/awsIamCredentials.t
Original file line number Diff line number Diff line change
@@ -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);

Expand Down Expand Up @@ -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';
}
Expand Down Expand Up @@ -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';
}
Expand Down Expand Up @@ -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';
}
Expand Down Expand Up @@ -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",
Expand All @@ -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
Expand All @@ -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)

Expand Down