diff --git a/.gitignore b/.gitignore index f5a1004..eccc287 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ .idea/ target/** dump.rdb +*.stats.out +coverage*.xml +test*.xml +luacov*.out +.DS_Store \ No newline at end of file diff --git a/.luacov b/.luacov new file mode 100644 index 0000000..903950e --- /dev/null +++ b/.luacov @@ -0,0 +1,11 @@ +modules = { + ["redisApiKeyValidator"] = "src/lua/api-gateway/validation/key/redisApiKeyValidator.lua", + ["userProfileValidator"] = "src/lua/api-gateway/validation/oauth2/userProfileValidator.lua", + ["oauthTokenValidator"] = "src/lua/api-gateway/validation/oauth2/oauthTokenValidator.lua", + ["hasher"] = "src/lua/api-gateway/util/hasher.lua", + ["redisHealthCheck"] = "src/lua/api-gateway/redis/redisHealthCheck.lua" +} + +exclude = { + "./test/.*$" +} \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..a576eb8 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,25 @@ +sudo: required + + +services: + - docker + +script: + - ./run_unit_tests.sh + +deploy: + # deploy develop to the staging environment + - provider: script + script: bash ./deploy.sh snapshot + on: + tags: false + all_branches: true + condition: "$TRAVIS_BRANCH != master" + # deploy master to production + - provider: script + script: bash ./deploy.sh production + on: + branch: master +after_deploy: + - if [[ "$TRAVIS_BRANCH" == "master" ]]; then ./tag.sh ; fi + diff --git a/Makefile b/Makefile index 0dfb7d3..2ef85a0 100644 --- a/Makefile +++ b/Makefile @@ -19,12 +19,16 @@ install: all $(INSTALL) -d $(DESTDIR)/$(LUA_LIB_DIR)/api-gateway/validation/key/ $(INSTALL) -d $(DESTDIR)/$(LUA_LIB_DIR)/api-gateway/validation/oauth2/ $(INSTALL) -d $(DESTDIR)/$(LUA_LIB_DIR)/api-gateway/validation/signing/ + $(INSTALL) -d $(DESTDIR)/$(LUA_LIB_DIR)/api-gateway/dogstatsd/ $(INSTALL) -d $(DESTDIR)/$(LUA_LIB_DIR)/api-gateway/redis/ + $(INSTALL) -d $(DESTDIR)/$(LUA_LIB_DIR)/api-gateway/util/ $(INSTALL) src/lua/api-gateway/validation/*.lua $(DESTDIR)/$(LUA_LIB_DIR)/api-gateway/validation/ $(INSTALL) src/lua/api-gateway/validation/key/*.lua $(DESTDIR)/$(LUA_LIB_DIR)/api-gateway/validation/key/ $(INSTALL) src/lua/api-gateway/validation/oauth2/*.lua $(DESTDIR)/$(LUA_LIB_DIR)/api-gateway/validation/oauth2/ $(INSTALL) src/lua/api-gateway/validation/signing/*.lua $(DESTDIR)/$(LUA_LIB_DIR)/api-gateway/validation/signing/ + $(INSTALL) src/lua/api-gateway/dogstatsd/*.lua $(DESTDIR)/$(LUA_LIB_DIR)/api-gateway/dogstatsd/ $(INSTALL) src/lua/api-gateway/redis/*.lua $(DESTDIR)/$(LUA_LIB_DIR)/api-gateway/redis/ + $(INSTALL) src/lua/api-gateway/util/*.lua $(DESTDIR)/$(LUA_LIB_DIR)/api-gateway/util/ test: redis echo "Starting redis server on default port" @@ -50,7 +54,7 @@ redis: all echo " ... using REDIS_SERVER=$(REDIS_SERVER)" test-docker: - echo "running tests with docker ..." + echo "Running tests with docker, using NO password protection for Redis" mkdir -p $(BUILD_DIR) mkdir -p $(BUILD_DIR)/test-logs cp -r test/resources/api-gateway $(BUILD_DIR) @@ -64,8 +68,49 @@ test-docker: cp -r ~/tmp/apiplatform/api-gateway-request-validation/target/ ./target rm -rf ~/tmp/apiplatform/api-gateway-request-validation +test-docker-jenkins: + echo "Running tests with docker, using NO password protection for Redis" + mkdir -p $(BUILD_DIR) + mkdir -p $(BUILD_DIR)/test-logs + cp -r test/resources/api-gateway $(BUILD_DIR) + sed -i '' 's/127\.0\.0\.1/redis\.docker/g' $(BUILD_DIR)/api-gateway/redis-upstream.conf + rm -f $(BUILD_DIR)/test-logs/* + mkdir -p ~/tmp/apiplatform/api-gateway-request-validation + cp -r ./src ~/tmp/apiplatform/api-gateway-request-validation/ + cp -r ./test ~/tmp/apiplatform/api-gateway-request-validation/ + cp -r ./target ~/tmp/apiplatform/api-gateway-request-validation/ + cd ./test && docker-compose -f docker-compose-jenkins.yml up --force-recreate -d + +test-docker-with-password: + echo "running tests with docker, using password protected Redis instance" + mkdir -p $(BUILD_DIR) + mkdir -p $(BUILD_DIR)/test-logs + cp -r test/resources/api-gateway $(BUILD_DIR) + sed -i '' 's/127\.0\.0\.1/redis\.docker/g' $(BUILD_DIR)/api-gateway/redis-upstream.conf + rm -f $(BUILD_DIR)/test-logs/* + mkdir -p ~/tmp/apiplatform/api-gateway-request-validation + cp -r ./src ~/tmp/apiplatform/api-gateway-request-validation/ + cp -r ./test ~/tmp/apiplatform/api-gateway-request-validation/ + cp -r ./target ~/tmp/apiplatform/api-gateway-request-validation/ + cd ./test && docker-compose -f docker-compose-with-password.yml up + cp -r ~/tmp/apiplatform/api-gateway-request-validation/target/ ./target + rm -rf ~/tmp/apiplatform/api-gateway-request-validation + +test-docker-with-password-jenkins: + echo "running tests with docker, using password protected Redis instance" + mkdir -p $(BUILD_DIR) + mkdir -p $(BUILD_DIR)/test-logs + cp -r test/resources/api-gateway $(BUILD_DIR) + sed -i '' 's/127\.0\.0\.1/redis\.docker/g' $(BUILD_DIR)/api-gateway/redis-upstream.conf + rm -f $(BUILD_DIR)/test-logs/* + mkdir -p ~/tmp/apiplatform/api-gateway-request-validation + cp -r ./src ~/tmp/apiplatform/api-gateway-request-validation/ + cp -r ./test ~/tmp/apiplatform/api-gateway-request-validation/ + cp -r ./target ~/tmp/apiplatform/api-gateway-request-validation/ + cd ./test && docker-compose -f docker-compose-with-password-jenkins.yml up --force-recreate -d + package: git archive --format=tar --prefix=api-gateway-request-validation-1.3.0/ -o api-gateway-request-validation-1.3.0.tar.gz -v HEAD clean: all - rm -rf $(BUILD_DIR) \ No newline at end of file + rm -rf $(BUILD_DIR) diff --git a/README.md b/README.md index 0b41d64..d26c07d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -api-gateway-request-validation +api-gateway-request-validation [![Build Status](https://travis-ci.org/adobe-apiplatform/api-gateway-request-validation.svg?branch=master)](https://travis-ci.org/adobe-apiplatform/api-gateway-request-validation) [![Coverage Status](https://coveralls.io/repos/github/adobe-apiplatform/api-gateway-request-validation/badge.svg?branch=HEAD)](https://coveralls.io/github/adobe-apiplatform/api-gateway-request-validation?branch=HEAD) ============================== Lua Module providing a request validation framework in the API Gateway. @@ -109,15 +109,15 @@ Sample usage location /validate-token { internal; set_if_empty $oauth_client_id '--change-me--'; - set_if_empty $oauth_host 'ims-na1.adobelogin.com'; - proxy_pass https://$oauth_host/ims/validate_token/v1?client_id=$oauth_client_id&token=$authtoken; + set_if_empty $oauth_host 'oauth-na1.adobelogin.com'; + proxy_pass https://$oauth_host/oauth/validate_token/v1?client_id=$oauth_client_id&token=$authtoken; proxy_method GET; proxy_pass_request_body off; proxy_pass_request_headers off; } # validators can be combined and even executed in a different order - location /with-api-key-and-ims-token { + location /with-api-key-and-oauth-token { # capture $api_key and $authtoken ... set $validate_api_key "on; order=2; "; @@ -320,8 +320,8 @@ location /validate_oauth_token { location /validate-token { internal; set_if_empty $oauth_client_id '--change-me--'; - set_if_empty $oauth_host 'ims-na1.adobelogin.com'; - proxy_pass https://$oauth_host/ims/validate_token/v1?client_id=$oauth_client_id&token=$authtoken; + set_if_empty $oauth_host 'oauth-na1.adobelogin.com'; + proxy_pass https://$oauth_host/oauth/validate_token/v1?client_id=$oauth_client_id&token=$authtoken; proxy_method GET; proxy_pass_request_body off; proxy_pass_request_headers off; @@ -369,8 +369,8 @@ location /validate-user { internal; #resolver 8.8.8.8; set_if_empty $oauth_client_id '--change-me--'; - set_if_empty $oauth_host 'ims-na1-stg1.adobelogin.com'; - proxy_pass https://$oauth_host/ims/profile/v1?client_id=$oauth_client_id&bearer_token=$authtoken; + set_if_empty $oauth_host 'oauth-na1-stg1.adobelogin.com'; + proxy_pass https://$oauth_host/oauth/profile/v1?client_id=$oauth_client_id&bearer_token=$authtoken; proxy_method GET; proxy_pass_request_body off; proxy_pass_request_headers off; @@ -392,15 +392,22 @@ git submodule update --init --recursive ``` ## Running the tests +To run unit tests and integration tests, use `./run_tests.sh` -### With docker +### Unit tests + +In order to run the unit tests, the command is `./run_unit_tests.sh` + +### Integration tests + +#### With docker ``` make test-docker ``` This command spins up 2 containers ( Redis and API Gateway ) and executes the tests in `test/perl` -### With native binary +#### With native binary ``` make test ``` diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..16fa930 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,3 @@ +docker run -v $PWD:/mocka_space \ + -e "LUA_LIBRARIES=src/lua/" -e "PACKAGE=api-gateway-request-validation" -e "ENV=${1}" \ + -e "API_KEY=${API_KEY}" --privileged -i -t adobeapiplatform/mocka:latest /bin/sh /scripts/deploy.sh diff --git a/dist/luarocks/.version b/dist/luarocks/.version new file mode 100644 index 0000000..2d3ebe2 --- /dev/null +++ b/dist/luarocks/.version @@ -0,0 +1 @@ +1.3.12-1 \ No newline at end of file diff --git a/dist/luarocks/api-gateway-request-validation-1.3.10-1.rockspec b/dist/luarocks/api-gateway-request-validation-1.3.10-1.rockspec new file mode 100644 index 0000000..df7958f --- /dev/null +++ b/dist/luarocks/api-gateway-request-validation-1.3.10-1.rockspec @@ -0,0 +1,42 @@ +package="api-gateway-request-validation" +version="1.3.10-1" +local function make_plat(plat) + return { modules = { + ["api-gateway.dogstatsd.Dogstatsd"] = "src/lua/api-gateway/dogstatsd/Dogstatsd.lua", + ["api-gateway.redis.redisConnectionConfiguration"] = "src/lua/api-gateway/redis/redisConnectionConfiguration.lua", + ["api-gateway.redis.redisConnectionProvider"] = "src/lua/api-gateway/redis/redisConnectionProvider.lua", + ["api-gateway.redis.redisHealthCheck"] = "src/lua/api-gateway/redis/redisHealthCheck.lua", + ["api-gateway.util.logger"] = "src/lua/api-gateway/util/logger.lua", + ["api-gateway.util.OauthClient"] = "src/lua/api-gateway/util/OauthClient.lua", + ["api-gateway.validation.key.redisApiKeyValidator"] = "src/lua/api-gateway/validation/key/redisApiKeyValidator.lua", + ["api-gateway.validation.oauth2.oauthTokenValidator"] = "src/lua/api-gateway/validation/oauth2/oauthTokenValidator.lua", + ["api-gateway.validation.oauth2.userProfileValidator"] = "src/lua/api-gateway/validation/oauth2/userProfileValidator.lua", + ["api-gateway.validation.signing.hmacGenericSignatureValidator"] = "src/lua/api-gateway/validation/signing/hmacGenericSignatureValidator.lua", + ["api-gateway.validation.base"] = "src/lua/api-gateway/validation/base.lua", + ["api-gateway.validation.factory"] = "src/lua/api-gateway/validation/factory.lua", + ["api-gateway.validation.validator"] = "src/lua/api-gateway/validation/validator.lua", + ["api-gateway.validation.validatorsHandler"] = "src/lua/api-gateway/validation/validatorsHandler.lua", + ["api-gateway.validation.validatorsHandlerErrorDecorator"] = "src/lua/api-gateway/validation/validatorsHandlerErrorDecorator.lua" + } } +end +source = { + url = "https://github.com/adobe-apiplatform/api-gateway-request-validation.git", + tag = "api-gateway-request-validation-1.3.10" +} +description = { + summary = "Lua Module providing a request validation framework in the API Gateway.", + license = "MIT" +} +dependencies = { + "lua >= 5.1" +} +build = { + type = "builtin", + platforms = { + unix = make_plat("unix"), + macosx = make_plat("macosx"), + haiku = make_plat("haiku"), + win32 = make_plat("win32"), + mingw32 = make_plat("mingw32") + } +} diff --git a/dist/luarocks/api-gateway-request-validation-1.3.11-1.rockspec b/dist/luarocks/api-gateway-request-validation-1.3.11-1.rockspec new file mode 100644 index 0000000..1c846fd --- /dev/null +++ b/dist/luarocks/api-gateway-request-validation-1.3.11-1.rockspec @@ -0,0 +1,114 @@ +package = "api-gateway-request-validation" +version = "1.3.11-1" +source = { + url = "git://github.com/adobe-apiplatform/api-gateway-request-validation.git", + tag = "api-gateway-request-validation-1.3.11" +} +description = { + summary = "Lua Module providing a request validation framework in the API Gateway.", + license = "MIT" +} +dependencies = { + "lua >= 5.1", + "lua-api-gateway-hmac >= 1.0.0" +} +build = { + type = "builtin", + platforms = { + haiku = { + modules = { + ["api-gateway.dogstatsd.Dogstatsd"] = "src/lua/api-gateway/dogstatsd/Dogstatsd.lua", + ["api-gateway.redis.redisConnectionConfiguration"] = "src/lua/api-gateway/redis/redisConnectionConfiguration.lua", + ["api-gateway.redis.redisConnectionProvider"] = "src/lua/api-gateway/redis/redisConnectionProvider.lua", + ["api-gateway.redis.redisHealthCheck"] = "src/lua/api-gateway/redis/redisHealthCheck.lua", + ["api-gateway.util.OauthClient"] = "src/lua/api-gateway/util/OauthClient.lua", + ["api-gateway.util.logger"] = "src/lua/api-gateway/util/logger.lua", + ["api-gateway.validation.base"] = "src/lua/api-gateway/validation/base.lua", + ["api-gateway.validation.factory"] = "src/lua/api-gateway/validation/factory.lua", + ["api-gateway.validation.key.redisApiKeyValidator"] = "src/lua/api-gateway/validation/key/redisApiKeyValidator.lua", + ["api-gateway.validation.oauth2.oauthTokenValidator"] = "src/lua/api-gateway/validation/oauth2/oauthTokenValidator.lua", + ["api-gateway.validation.oauth2.userProfileValidator"] = "src/lua/api-gateway/validation/oauth2/userProfileValidator.lua", + ["api-gateway.validation.signing.hmacGenericSignatureValidator"] = "src/lua/api-gateway/validation/signing/hmacGenericSignatureValidator.lua", + ["api-gateway.validation.validator"] = "src/lua/api-gateway/validation/validator.lua", + ["api-gateway.validation.validatorsHandler"] = "src/lua/api-gateway/validation/validatorsHandler.lua", + ["api-gateway.validation.validatorsHandlerErrorDecorator"] = "src/lua/api-gateway/validation/validatorsHandlerErrorDecorator.lua" + } + }, + macosx = { + modules = { + ["api-gateway.dogstatsd.Dogstatsd"] = "src/lua/api-gateway/dogstatsd/Dogstatsd.lua", + ["api-gateway.redis.redisConnectionConfiguration"] = "src/lua/api-gateway/redis/redisConnectionConfiguration.lua", + ["api-gateway.redis.redisConnectionProvider"] = "src/lua/api-gateway/redis/redisConnectionProvider.lua", + ["api-gateway.redis.redisHealthCheck"] = "src/lua/api-gateway/redis/redisHealthCheck.lua", + ["api-gateway.util.OauthClient"] = "src/lua/api-gateway/util/OauthClient.lua", + ["api-gateway.util.logger"] = "src/lua/api-gateway/util/logger.lua", + ["api-gateway.validation.base"] = "src/lua/api-gateway/validation/base.lua", + ["api-gateway.validation.factory"] = "src/lua/api-gateway/validation/factory.lua", + ["api-gateway.validation.key.redisApiKeyValidator"] = "src/lua/api-gateway/validation/key/redisApiKeyValidator.lua", + ["api-gateway.validation.oauth2.oauthTokenValidator"] = "src/lua/api-gateway/validation/oauth2/oauthTokenValidator.lua", + ["api-gateway.validation.oauth2.userProfileValidator"] = "src/lua/api-gateway/validation/oauth2/userProfileValidator.lua", + ["api-gateway.validation.signing.hmacGenericSignatureValidator"] = "src/lua/api-gateway/validation/signing/hmacGenericSignatureValidator.lua", + ["api-gateway.validation.validator"] = "src/lua/api-gateway/validation/validator.lua", + ["api-gateway.validation.validatorsHandler"] = "src/lua/api-gateway/validation/validatorsHandler.lua", + ["api-gateway.validation.validatorsHandlerErrorDecorator"] = "src/lua/api-gateway/validation/validatorsHandlerErrorDecorator.lua" + } + }, + mingw32 = { + modules = { + ["api-gateway.dogstatsd.Dogstatsd"] = "src/lua/api-gateway/dogstatsd/Dogstatsd.lua", + ["api-gateway.redis.redisConnectionConfiguration"] = "src/lua/api-gateway/redis/redisConnectionConfiguration.lua", + ["api-gateway.redis.redisConnectionProvider"] = "src/lua/api-gateway/redis/redisConnectionProvider.lua", + ["api-gateway.redis.redisHealthCheck"] = "src/lua/api-gateway/redis/redisHealthCheck.lua", + ["api-gateway.util.OauthClient"] = "src/lua/api-gateway/util/OauthClient.lua", + ["api-gateway.util.logger"] = "src/lua/api-gateway/util/logger.lua", + ["api-gateway.validation.base"] = "src/lua/api-gateway/validation/base.lua", + ["api-gateway.validation.factory"] = "src/lua/api-gateway/validation/factory.lua", + ["api-gateway.validation.key.redisApiKeyValidator"] = "src/lua/api-gateway/validation/key/redisApiKeyValidator.lua", + ["api-gateway.validation.oauth2.oauthTokenValidator"] = "src/lua/api-gateway/validation/oauth2/oauthTokenValidator.lua", + ["api-gateway.validation.oauth2.userProfileValidator"] = "src/lua/api-gateway/validation/oauth2/userProfileValidator.lua", + ["api-gateway.validation.signing.hmacGenericSignatureValidator"] = "src/lua/api-gateway/validation/signing/hmacGenericSignatureValidator.lua", + ["api-gateway.validation.validator"] = "src/lua/api-gateway/validation/validator.lua", + ["api-gateway.validation.validatorsHandler"] = "src/lua/api-gateway/validation/validatorsHandler.lua", + ["api-gateway.validation.validatorsHandlerErrorDecorator"] = "src/lua/api-gateway/validation/validatorsHandlerErrorDecorator.lua" + } + }, + unix = { + modules = { + ["api-gateway.dogstatsd.Dogstatsd"] = "src/lua/api-gateway/dogstatsd/Dogstatsd.lua", + ["api-gateway.redis.redisConnectionConfiguration"] = "src/lua/api-gateway/redis/redisConnectionConfiguration.lua", + ["api-gateway.redis.redisConnectionProvider"] = "src/lua/api-gateway/redis/redisConnectionProvider.lua", + ["api-gateway.redis.redisHealthCheck"] = "src/lua/api-gateway/redis/redisHealthCheck.lua", + ["api-gateway.util.OauthClient"] = "src/lua/api-gateway/util/OauthClient.lua", + ["api-gateway.util.logger"] = "src/lua/api-gateway/util/logger.lua", + ["api-gateway.validation.base"] = "src/lua/api-gateway/validation/base.lua", + ["api-gateway.validation.factory"] = "src/lua/api-gateway/validation/factory.lua", + ["api-gateway.validation.key.redisApiKeyValidator"] = "src/lua/api-gateway/validation/key/redisApiKeyValidator.lua", + ["api-gateway.validation.oauth2.oauthTokenValidator"] = "src/lua/api-gateway/validation/oauth2/oauthTokenValidator.lua", + ["api-gateway.validation.oauth2.userProfileValidator"] = "src/lua/api-gateway/validation/oauth2/userProfileValidator.lua", + ["api-gateway.validation.signing.hmacGenericSignatureValidator"] = "src/lua/api-gateway/validation/signing/hmacGenericSignatureValidator.lua", + ["api-gateway.validation.validator"] = "src/lua/api-gateway/validation/validator.lua", + ["api-gateway.validation.validatorsHandler"] = "src/lua/api-gateway/validation/validatorsHandler.lua", + ["api-gateway.validation.validatorsHandlerErrorDecorator"] = "src/lua/api-gateway/validation/validatorsHandlerErrorDecorator.lua" + } + }, + win32 = { + modules = { + ["api-gateway.dogstatsd.Dogstatsd"] = "src/lua/api-gateway/dogstatsd/Dogstatsd.lua", + ["api-gateway.redis.redisConnectionConfiguration"] = "src/lua/api-gateway/redis/redisConnectionConfiguration.lua", + ["api-gateway.redis.redisConnectionProvider"] = "src/lua/api-gateway/redis/redisConnectionProvider.lua", + ["api-gateway.redis.redisHealthCheck"] = "src/lua/api-gateway/redis/redisHealthCheck.lua", + ["api-gateway.util.OauthClient"] = "src/lua/api-gateway/util/OauthClient.lua", + ["api-gateway.util.logger"] = "src/lua/api-gateway/util/logger.lua", + ["api-gateway.validation.base"] = "src/lua/api-gateway/validation/base.lua", + ["api-gateway.validation.factory"] = "src/lua/api-gateway/validation/factory.lua", + ["api-gateway.validation.key.redisApiKeyValidator"] = "src/lua/api-gateway/validation/key/redisApiKeyValidator.lua", + ["api-gateway.validation.oauth2.oauthTokenValidator"] = "src/lua/api-gateway/validation/oauth2/oauthTokenValidator.lua", + ["api-gateway.validation.oauth2.userProfileValidator"] = "src/lua/api-gateway/validation/oauth2/userProfileValidator.lua", + ["api-gateway.validation.signing.hmacGenericSignatureValidator"] = "src/lua/api-gateway/validation/signing/hmacGenericSignatureValidator.lua", + ["api-gateway.validation.validator"] = "src/lua/api-gateway/validation/validator.lua", + ["api-gateway.validation.validatorsHandler"] = "src/lua/api-gateway/validation/validatorsHandler.lua", + ["api-gateway.validation.validatorsHandlerErrorDecorator"] = "src/lua/api-gateway/validation/validatorsHandlerErrorDecorator.lua" + } + } + } +} diff --git a/dist/luarocks/api-gateway-request-validation-1.3.12-1.rockspec b/dist/luarocks/api-gateway-request-validation-1.3.12-1.rockspec new file mode 100644 index 0000000..8aef812 --- /dev/null +++ b/dist/luarocks/api-gateway-request-validation-1.3.12-1.rockspec @@ -0,0 +1,113 @@ +package = "api-gateway-request-validation" +version = "1.3.12-1" +source = { + url = "git://github.com/adobe-apiplatform/api-gateway-request-validation.git", + tag = "api-gateway-request-validation-1.3.12" +} +description = { + summary = "Lua Module providing a request validation framework in the API Gateway.", + license = "MIT" +} +dependencies = { + "lua >= 5.1", "lua-api-gateway-hmac >= 1.0.0" +} +build = { + type = "builtin", + platforms = { + haiku = { + modules = { + ["api-gateway.dogstatsd.Dogstatsd"] = "src/lua/api-gateway/dogstatsd/Dogstatsd.lua", + ["api-gateway.redis.redisConnectionConfiguration"] = "src/lua/api-gateway/redis/redisConnectionConfiguration.lua", + ["api-gateway.redis.redisConnectionProvider"] = "src/lua/api-gateway/redis/redisConnectionProvider.lua", + ["api-gateway.redis.redisHealthCheck"] = "src/lua/api-gateway/redis/redisHealthCheck.lua", + ["api-gateway.util.OauthClient"] = "src/lua/api-gateway/util/OauthClient.lua", + ["api-gateway.util.logger"] = "src/lua/api-gateway/util/logger.lua", + ["api-gateway.validation.base"] = "src/lua/api-gateway/validation/base.lua", + ["api-gateway.validation.factory"] = "src/lua/api-gateway/validation/factory.lua", + ["api-gateway.validation.key.redisApiKeyValidator"] = "src/lua/api-gateway/validation/key/redisApiKeyValidator.lua", + ["api-gateway.validation.oauth2.oauthTokenValidator"] = "src/lua/api-gateway/validation/oauth2/oauthTokenValidator.lua", + ["api-gateway.validation.oauth2.userProfileValidator"] = "src/lua/api-gateway/validation/oauth2/userProfileValidator.lua", + ["api-gateway.validation.signing.hmacGenericSignatureValidator"] = "src/lua/api-gateway/validation/signing/hmacGenericSignatureValidator.lua", + ["api-gateway.validation.validator"] = "src/lua/api-gateway/validation/validator.lua", + ["api-gateway.validation.validatorsHandler"] = "src/lua/api-gateway/validation/validatorsHandler.lua", + ["api-gateway.validation.validatorsHandlerErrorDecorator"] = "src/lua/api-gateway/validation/validatorsHandlerErrorDecorator.lua" + } + }, + macosx = { + modules = { + ["api-gateway.dogstatsd.Dogstatsd"] = "src/lua/api-gateway/dogstatsd/Dogstatsd.lua", + ["api-gateway.redis.redisConnectionConfiguration"] = "src/lua/api-gateway/redis/redisConnectionConfiguration.lua", + ["api-gateway.redis.redisConnectionProvider"] = "src/lua/api-gateway/redis/redisConnectionProvider.lua", + ["api-gateway.redis.redisHealthCheck"] = "src/lua/api-gateway/redis/redisHealthCheck.lua", + ["api-gateway.util.OauthClient"] = "src/lua/api-gateway/util/OauthClient.lua", + ["api-gateway.util.logger"] = "src/lua/api-gateway/util/logger.lua", + ["api-gateway.validation.base"] = "src/lua/api-gateway/validation/base.lua", + ["api-gateway.validation.factory"] = "src/lua/api-gateway/validation/factory.lua", + ["api-gateway.validation.key.redisApiKeyValidator"] = "src/lua/api-gateway/validation/key/redisApiKeyValidator.lua", + ["api-gateway.validation.oauth2.oauthTokenValidator"] = "src/lua/api-gateway/validation/oauth2/oauthTokenValidator.lua", + ["api-gateway.validation.oauth2.userProfileValidator"] = "src/lua/api-gateway/validation/oauth2/userProfileValidator.lua", + ["api-gateway.validation.signing.hmacGenericSignatureValidator"] = "src/lua/api-gateway/validation/signing/hmacGenericSignatureValidator.lua", + ["api-gateway.validation.validator"] = "src/lua/api-gateway/validation/validator.lua", + ["api-gateway.validation.validatorsHandler"] = "src/lua/api-gateway/validation/validatorsHandler.lua", + ["api-gateway.validation.validatorsHandlerErrorDecorator"] = "src/lua/api-gateway/validation/validatorsHandlerErrorDecorator.lua" + } + }, + mingw32 = { + modules = { + ["api-gateway.dogstatsd.Dogstatsd"] = "src/lua/api-gateway/dogstatsd/Dogstatsd.lua", + ["api-gateway.redis.redisConnectionConfiguration"] = "src/lua/api-gateway/redis/redisConnectionConfiguration.lua", + ["api-gateway.redis.redisConnectionProvider"] = "src/lua/api-gateway/redis/redisConnectionProvider.lua", + ["api-gateway.redis.redisHealthCheck"] = "src/lua/api-gateway/redis/redisHealthCheck.lua", + ["api-gateway.util.OauthClient"] = "src/lua/api-gateway/util/OauthClient.lua", + ["api-gateway.util.logger"] = "src/lua/api-gateway/util/logger.lua", + ["api-gateway.validation.base"] = "src/lua/api-gateway/validation/base.lua", + ["api-gateway.validation.factory"] = "src/lua/api-gateway/validation/factory.lua", + ["api-gateway.validation.key.redisApiKeyValidator"] = "src/lua/api-gateway/validation/key/redisApiKeyValidator.lua", + ["api-gateway.validation.oauth2.oauthTokenValidator"] = "src/lua/api-gateway/validation/oauth2/oauthTokenValidator.lua", + ["api-gateway.validation.oauth2.userProfileValidator"] = "src/lua/api-gateway/validation/oauth2/userProfileValidator.lua", + ["api-gateway.validation.signing.hmacGenericSignatureValidator"] = "src/lua/api-gateway/validation/signing/hmacGenericSignatureValidator.lua", + ["api-gateway.validation.validator"] = "src/lua/api-gateway/validation/validator.lua", + ["api-gateway.validation.validatorsHandler"] = "src/lua/api-gateway/validation/validatorsHandler.lua", + ["api-gateway.validation.validatorsHandlerErrorDecorator"] = "src/lua/api-gateway/validation/validatorsHandlerErrorDecorator.lua" + } + }, + unix = { + modules = { + ["api-gateway.dogstatsd.Dogstatsd"] = "src/lua/api-gateway/dogstatsd/Dogstatsd.lua", + ["api-gateway.redis.redisConnectionConfiguration"] = "src/lua/api-gateway/redis/redisConnectionConfiguration.lua", + ["api-gateway.redis.redisConnectionProvider"] = "src/lua/api-gateway/redis/redisConnectionProvider.lua", + ["api-gateway.redis.redisHealthCheck"] = "src/lua/api-gateway/redis/redisHealthCheck.lua", + ["api-gateway.util.OauthClient"] = "src/lua/api-gateway/util/OauthClient.lua", + ["api-gateway.util.logger"] = "src/lua/api-gateway/util/logger.lua", + ["api-gateway.validation.base"] = "src/lua/api-gateway/validation/base.lua", + ["api-gateway.validation.factory"] = "src/lua/api-gateway/validation/factory.lua", + ["api-gateway.validation.key.redisApiKeyValidator"] = "src/lua/api-gateway/validation/key/redisApiKeyValidator.lua", + ["api-gateway.validation.oauth2.oauthTokenValidator"] = "src/lua/api-gateway/validation/oauth2/oauthTokenValidator.lua", + ["api-gateway.validation.oauth2.userProfileValidator"] = "src/lua/api-gateway/validation/oauth2/userProfileValidator.lua", + ["api-gateway.validation.signing.hmacGenericSignatureValidator"] = "src/lua/api-gateway/validation/signing/hmacGenericSignatureValidator.lua", + ["api-gateway.validation.validator"] = "src/lua/api-gateway/validation/validator.lua", + ["api-gateway.validation.validatorsHandler"] = "src/lua/api-gateway/validation/validatorsHandler.lua", + ["api-gateway.validation.validatorsHandlerErrorDecorator"] = "src/lua/api-gateway/validation/validatorsHandlerErrorDecorator.lua" + } + }, + win32 = { + modules = { + ["api-gateway.dogstatsd.Dogstatsd"] = "src/lua/api-gateway/dogstatsd/Dogstatsd.lua", + ["api-gateway.redis.redisConnectionConfiguration"] = "src/lua/api-gateway/redis/redisConnectionConfiguration.lua", + ["api-gateway.redis.redisConnectionProvider"] = "src/lua/api-gateway/redis/redisConnectionProvider.lua", + ["api-gateway.redis.redisHealthCheck"] = "src/lua/api-gateway/redis/redisHealthCheck.lua", + ["api-gateway.util.OauthClient"] = "src/lua/api-gateway/util/OauthClient.lua", + ["api-gateway.util.logger"] = "src/lua/api-gateway/util/logger.lua", + ["api-gateway.validation.base"] = "src/lua/api-gateway/validation/base.lua", + ["api-gateway.validation.factory"] = "src/lua/api-gateway/validation/factory.lua", + ["api-gateway.validation.key.redisApiKeyValidator"] = "src/lua/api-gateway/validation/key/redisApiKeyValidator.lua", + ["api-gateway.validation.oauth2.oauthTokenValidator"] = "src/lua/api-gateway/validation/oauth2/oauthTokenValidator.lua", + ["api-gateway.validation.oauth2.userProfileValidator"] = "src/lua/api-gateway/validation/oauth2/userProfileValidator.lua", + ["api-gateway.validation.signing.hmacGenericSignatureValidator"] = "src/lua/api-gateway/validation/signing/hmacGenericSignatureValidator.lua", + ["api-gateway.validation.validator"] = "src/lua/api-gateway/validation/validator.lua", + ["api-gateway.validation.validatorsHandler"] = "src/lua/api-gateway/validation/validatorsHandler.lua", + ["api-gateway.validation.validatorsHandlerErrorDecorator"] = "src/lua/api-gateway/validation/validatorsHandlerErrorDecorator.lua" + } + } + } +} diff --git a/dist/maven/pom.xml b/dist/maven/pom.xml index fde822b..fed4f76 100644 --- a/dist/maven/pom.xml +++ b/dist/maven/pom.xml @@ -1,3 +1,4 @@ + - - + --> 4.0.0 com.adobe.api.gateway api-gateway-request-validation - 1.2.4 + 1.3.21-SNAPSHOT pom @@ -70,10 +69,9 @@ 7 false - true + false - diff --git a/docs/modules/api-gateway.redis.redisHealthCheck.html b/docs/modules/api-gateway.redis.redisHealthCheck.html new file mode 100644 index 0000000..3ffbda8 --- /dev/null +++ b/docs/modules/api-gateway.redis.redisHealthCheck.html @@ -0,0 +1,322 @@ + + + + + Codestin Search App + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Module api-gateway.redis.redisHealthCheck

+

Splits a string by a given separator

+

+ + +

Functions

+ + + + + +
RedisHealthCheck:getHealthyRedisNode (upstream, upstreamPassword)Checks for healthy Redis nodes and returns the first correct value
+

Local Functions

+ + + + + + + + + + + + + + + + + + + + + + + + + +
isPeerHealthy (peerName, upstreamPassword)Checks if a Redis peer is healthy by opening a TCP connection and sending a PING message.
generatePeersStatus (peers, upstreamPassword)Generates peers health status
getHealthCheckForUpstream (upstream, upstreamPassword)Checks for healthy upstreams using the ngx_upstream_lua module.
getHealthyRedisNodeFromCache (dictionaryName, upstreamName)Returns the upstream address from the shared cache
updateHealthyRedisNodeInCache (dictionaryName, upstreamName, healthyRedisHost)Sets the healthy upstream address in the shared cache
getHostAndPortInUpstream (upstreamRedis)Splits the host:port upstream format into corresponding fields
+ +
+
+ + +

Functions

+ +
+
+ + RedisHealthCheck:getHealthyRedisNode (upstream, upstreamPassword) +
+
+ Checks for healthy Redis nodes and returns the first correct value + + +

Parameters:

+
    +
  • upstream + The name of the upstream for which a healthy address is to be found +
  • +
  • upstreamPassword + The password for the upstream, should this need authentication prior to health checking +
  • +
+ +

Returns:

+
    +
  1. + Full upstream address as host:port
  2. +
  3. + Upstream host
  4. +
  5. + Upstream port
  6. +
+ + + + +
+
+

Local Functions

+ +
+
+ + isPeerHealthy (peerName, upstreamPassword) +
+
+ Checks if a Redis peer is healthy by opening a TCP connection and sending a PING message. If upstream_password + is present, authentication is performed prior to pinging the machine + + +

Parameters:

+
    +
  • peerName + Redis upstream in host:port format +
  • +
  • upstreamPassword + upstream password used for authentication before pinging +
  • +
+ +

Returns:

+
    + + true if the peer is considered healthy (PING response is PONG), false otherwise (password may be incorrect, + instance may be down) +
+ + + + +
+
+ + generatePeersStatus (peers, upstreamPassword) +
+
+ Generates peers health status + + +

Parameters:

+
    +
  • peers + Array of peers to be health checked +
  • +
  • upstreamPassword + Peer password used for authentication prior to health checking (one password for now) +
  • +
+ +

Returns:

+
    + + Upstream status array as peer -> 1 (healthy), 0(unhealthy) +
+ + + + +
+
+ + getHealthCheckForUpstream (upstream, upstreamPassword) +
+
+ Checks for healthy upstreams using the ngx_upstream_lua module. It checks for both primary and secondary peers + + +

Parameters:

+
    +
  • upstream + The name of the upstream, as defined in the conf files +
  • +
  • upstreamPassword + The password for the upstream, needed to perform authentication prior to the actual healthcheck +
  • +
+ +

Returns:

+
    + + Upstream status as a table containing host:port -> 1(healthy) or 0(unhealthy) +
+ + + + +
+
+ + getHealthyRedisNodeFromCache (dictionaryName, upstreamName) +
+
+ Returns the upstream address from the shared cache + + +

Parameters:

+
    +
  • dictionaryName + Shared dictionary containing the cached entries +
  • +
  • upstreamName + The name of the upstream, used in the caching key +
  • +
+ +

Returns:

+
    + + Upstream address as host:port +
+ + + + +
+
+ + updateHealthyRedisNodeInCache (dictionaryName, upstreamName, healthyRedisHost) +
+
+ Sets the healthy upstream address in the shared cache + + +

Parameters:

+
    +
  • dictionaryName + Shared dictionary containing the cached entries +
  • +
  • upstreamName + The name of the upstream, used in the caching key +
  • +
  • healthyRedisHost + Upstream address as host:port +
  • +
+ + + + + +
+
+ + getHostAndPortInUpstream (upstreamRedis) +
+
+ Splits the host:port upstream format into corresponding fields + + +

Parameters:

+
    +
  • upstreamRedis + Upstream address as host:port +
  • +
+ +

Returns:

+
    +
  1. + Upstream host
  2. +
  3. + Upstream port
  4. +
+ + + + +
+
+ + +
+
+
+generated by LDoc 1.4.6 +Last updated 2018-07-05 13:03:22 +
+
+ + diff --git a/run_integration_tests.sh b/run_integration_tests.sh new file mode 100755 index 0000000..0afe3fd --- /dev/null +++ b/run_integration_tests.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +echo "Running tests with simple redis" +make test-docker-jenkins +while docker ps | grep test_gateway_1 ; do + echo "Waiting for tests to finish" + sleep 5 +done +echo "Finished integration tests" +if ! docker logs test_gateway_1 --tail 1 | grep "PASS" ; then + echo "FAILED TESTS" + docker logs test_gateway_1 + cd ./test && docker-compose -f docker-compose-jenkins.yml stop && docker-compose -f docker-compose-jenkins.yml rm -f + exit 64 +fi +docker logs test_gateway_1 --tail 1 +cd ./test && docker-compose -f docker-compose-jenkins.yml stop && docker-compose -f docker-compose-jenkins.yml rm -f +rm -rf ~/tmp/apiplatform/api-gateway-request-validation +cd ../ + +echo "Running tests with redis with password" + +make test-docker-with-password-jenkins +while docker ps | grep test_gateway_1 ; do + echo "Waiting for tests to finish" + sleep 5 +done +echo "Finished integration tests" +if ! docker logs test_gateway_1 --tail 1 | grep "PASS" ; then + echo "FAILED TESTS" + docker logs test_gateway_1 + cd ./test && docker-compose -f docker-compose-with-password-jenkins.yml stop && docker-compose -f docker-compose-with-password-jenkins.yml rm -f + exit 64 +fi +docker logs test_gateway_1 --tail 1 +cd ./test && docker-compose -f docker-compose-with-password-jenkins.yml stop && docker-compose -f docker-compose-with-password-jenkins.yml rm -f +rm -rf ~/tmp/apiplatform/api-gateway-request-validation +cd ../ \ No newline at end of file diff --git a/run_tests.lua b/run_tests.lua new file mode 100644 index 0000000..25e0362 --- /dev/null +++ b/run_tests.lua @@ -0,0 +1,14 @@ +-- +-- Created by IntelliJ IDEA. +-- User: purcarea +-- Date: 30/03/18 +-- +local tests = { + "test.unit-tests.api-gateway.validation.redisApiKeyValidatorTest", + "test.unit-tests.api-gateway.util.hasherTest", + "test.unit-tests.api-gateway.validation.oauth2.oauthTokenValidatorTest", + "test.unit-tests.api-gateway.redis.redisHealthCheckTest", + "test.unit-tests.api-gateway.validation.oauth2.userProfileValidatorTest" +} + +require("mocka.suite")(tests) diff --git a/run_tests.sh b/run_tests.sh new file mode 100755 index 0000000..0c4c549 --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +./run_unit_tests.sh +./run_integration_tests.sh \ No newline at end of file diff --git a/run_unit_tests.sh b/run_unit_tests.sh new file mode 100755 index 0000000..ddb7c88 --- /dev/null +++ b/run_unit_tests.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +docker run -v $PWD:/mocka_space \ + -e "LUA_LIBRARIES=src/lua/" -e "COVERALLS_REPO_TOKEN=${COVERALLS_REPO_TOKEN}" --privileged -i adobeapiplatform/mocka:latest \ No newline at end of file diff --git a/src/lua/api-gateway/dogstatsd/Dogstatsd.lua b/src/lua/api-gateway/dogstatsd/Dogstatsd.lua new file mode 100644 index 0000000..792a544 --- /dev/null +++ b/src/lua/api-gateway/dogstatsd/Dogstatsd.lua @@ -0,0 +1,108 @@ + +--- Auxiliary module used to extract dogstats deamon initialization methods, incrementation of metrics +-- and calls to other useful functions +-- employed to measure calls to the Oauth provider: number, duration, etc. + +local Dogstatsd = {} + +function Dogstatsd:new(o) + o = o or {} + setmetatable(o, self) + self.__index = self + return o +end + +--- Loads a lua gracefully. If the module doesn't exist the exception is caught, logged and the execution continues +-- @param module path to the module to be loaded +-- @return The loaded module, or nil if the module cannot be loaded +-- +local function loadrequire(module) + ngx.log(ngx.DEBUG, "Loading module [", tostring(module), "]") + local function requiref(module) + return require(module) + end + + local status, result = pcall(requiref, module) + if not (status) then + ngx.log(ngx.WARN, "Could not load module [", module, "]. Error ", result) + return nil + end + + return result +end + +local dogstatsd + +--- Returns an instance of dogstatsd only if it does not already exist. Returns the instance if the feature is enabled +-- @param none +-- @return An instance of dogstatsd or nil if the class cannot be instantiated +-- +local function getDogstatsd() + + local isDogstatsEnabled = ngx.var.isDogstatsEnabled + if isDogstatsEnabled == nil or isDogstatsEnabled == "false" then + ngx.log(ngx.DEBUG, "dogstats module is disabled") + return nil + end + + local dogstatsHost = ngx.var.dogstatsHost + if dogstatsHost == nil or dogstatsHost == '' then + ngx.log(ngx.ERR, "dogstats host was not defined") + return nil + end + + if dogstatsd ~= nil then + return dogstatsd + end + + local restyDogstatsd = loadrequire('resty_dogstatsd') + + if restyDogstatsd == nil then + return nil + end + + dogstatsd = restyDogstatsd.new({ + statsd = { + host = dogstatsHost, + port = ngx.var.dogstatsPort or 8125, + namespace = "api_gateway", + }, + tags = { + "application:lua", + }, + }) + ngx.log(ngx.DEBUG, "Instantiated dogstatsd.") + return dogstatsd +end + +--- Increments the number of calls to the Oauth provider +-- @param metric - metric to be identified in the Dogstatsd dashboard +-- @param counter - the number of times we would like to have the metric incremented +-- @return - void method +-- +function Dogstatsd:increment(metric, counter) + dogstatsd = getDogstatsd() + + if dogstatsd ~= nil then + ngx.log(ngx.DEBUG, "[Dogstatsd] Incrementing metric ", metric) + dogstatsd:increment(metric, counter) + end +end + +--- Measures the number of milliseconds elapsed +-- @param metric - metric to be identified in the Dogstatsd dashboard +-- @param ms - the time it took a call to finish in milliseconds +-- @return - void method +-- +function Dogstatsd:time(metric, ms) + dogstatsd = getDogstatsd() + + if dogstatsd ~= nil then + ngx.log(ngx.DEBUG, "[Dogstatsd] Computing elapsed time for ", metric, ".Request duration ", ms) + dogstatsd:timer(metric, ms) + end +end + +return Dogstatsd + + diff --git a/src/lua/api-gateway/redis/redisConnectionConfiguration.lua b/src/lua/api-gateway/redis/redisConnectionConfiguration.lua new file mode 100644 index 0000000..bf61459 --- /dev/null +++ b/src/lua/api-gateway/redis/redisConnectionConfiguration.lua @@ -0,0 +1,22 @@ +-- +-- Created by IntelliJ IDEA. +-- User: vdatcu +-- Date: 04/08/2017 +-- Time: 11:54 +-- To change this template use File | Settings | File Templates. +-- + +local redisConf = { + ["oauth"] = { + env_password_variable = "REDIS_PASS_OAUTH", + ro_upstream_name = "oauth-redis-ro-upstream", + rw_upstream_name = "oauth-redis-rw-upstream" + }, + ["apiKey"] = { + env_password_variable = "REDIS_PASS_API_KEY", + ro_upstream_name = "api-gateway-redis-replica", + rw_upstream_name = "api-gateway-redis" + } +} + +return redisConf \ No newline at end of file diff --git a/src/lua/api-gateway/redis/redisConnectionProvider.lua b/src/lua/api-gateway/redis/redisConnectionProvider.lua new file mode 100644 index 0000000..e665405 --- /dev/null +++ b/src/lua/api-gateway/redis/redisConnectionProvider.lua @@ -0,0 +1,140 @@ +-- Copyright (c) 2017 Adobe Systems Incorporated. All rights reserved. +-- +-- Permission is hereby granted, free of charge, to any person obtaining a +-- copy of this software and associated documentation files (the "Software"), +-- to deal in the Software without restriction, including without limitation +-- the rights to use, copy, modify, merge, publish, distribute, sublicense, +-- and/or sell copies of the Software, and to permit persons to whom the +-- Software is furnished to do so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in +-- all copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +-- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +-- DEALINGS IN THE SOFTWARE. + +--- Redis connection provider module with retry and keepalive functionalities + +local restyRedis = require "resty.redis" +local RedisHealthCheck = require "api-gateway.redis.redisHealthCheck" +local apiGatewayRedisReadReplica = "api-gateway-redis-replica" + +local redisHealthCheck = RedisHealthCheck:new({ + shared_dict = "cachedkeys" +}) + +local max_idle_timeout = 30000 +local pool_size = 100 +local default_redis_timeout = 5000 + +local RedisConnectionProvider = {} + +function RedisConnectionProvider:new(o) + local o = o or {} + setmetatable(o, self) + self.__index = self + return o +end + +local function isNotEmpty(s) + return s ~= nil and s ~= '' +end + +--- Searches and returns a Redis upstream pair (host:port) based on the name provided +--- @param upstream_name The Redis upstream name, as defined in the Nginx conf file +--- @return Redis host +--- @return Redis port +function RedisConnectionProvider:getRedisUpstream(upstream_name, upstream_password) + local upstreamName = upstream_name or apiGatewayRedisReadReplica + local _, host, port = redisHealthCheck:getHealthyRedisNode(upstreamName, upstream_password) + ngx.log(ngx.DEBUG, "Obtained Redis Host:" .. tostring(host) .. ":" .. tostring(port), " from upstream:", upstreamName) + if (nil ~= host and nil ~= port) then + return host, port + end + + ngx.log(ngx.ERR, "Could not find a Redis upstream.") + return nil, nil +end + +--- Obtains a connection to a provided upstream using an optionally provided password and timeout +--- If no password is provided, it is searched in the REDIS_PASS and REDIS_PASSWORD env variables +--- If the first connection attempt fails, a second retry is automatically performed with the same connection options +--- @param connection_options If this is a table, it should have upstream, password and redis_timeout. +--- Otherwise, the connection_options is considered the upstream name +function RedisConnectionProvider:getConnection(connection_options) + local redisUpstream, + redisPassword, + redisTimeout; + + if (type(connection_options) == 'table') then + redisUpstream = connection_options["upstream"] + redisPassword = connection_options["password"] + redisTimeout = connection_options["redis_timeout"] + else + redisUpstream = connection_options + redisPassword = os.getenv('REDIS_PASS') or os.getenv('REDIS_PASSWORD') or '' + end + + local redisHost, redisPort = self:getRedisUpstream(redisUpstream, redisPassword) + ngx.log(ngx.DEBUG, "Trying with: " .. tostring(redisHost) .. " and " .. tostring(redisPort)) + local status, redisInstance = self:connectToRedis(redisHost, redisPort, redisPassword, redisTimeout) + if not status then + -- retry + ngx.log(ngx.WARN, "Connection to Redis failed. Retrieving new Redis host and retrying") + redisHost, redisPort = self:getRedisUpstream(redisUpstream, redisPassword) + ngx.log(ngx.DEBUG, "Got new upstream: " .. tostring(redisHost) .. " and " .. tostring(redisPort)) + status, redisInstance = self:connectToRedis(redisHost, redisPort, redisPassword, redisTimeout) + end + return status, redisInstance +end + +function RedisConnectionProvider:connectToRedis(host, port, password, redisTimeout) + local redis = restyRedis:new() + + -- sets general timeout - for all operations + local redis_timeout = redisTimeout or ngx.var.redis_timeout or default_redis_timeout + redis:set_timeout(redis_timeout) + + local ok, err = redis:connect(host, port) + if not ok then + ngx.log(ngx.ERR, "Failed to connect to Redis instance: " .. host .. ", port: " .. port .. ". Error: ", err) + return false, nil + end + + -- Check for existing connection + local times, error = redis:get_reused_times() + + if times and times ~= 0 then + ngx.log(ngx.DEBUG, "Reusing Redis connection. Reused times: " .. tostring(times)) + return true, redis + end + + if isNotEmpty(password) then + -- Authenticate + ok, err = redis:auth(password) + if not ok then + ngx.log(ngx.ERR, "Redis authentication failed for server: " .. host .. ":" .. port .. ". Error: ", err) + return false, nil + end + ngx.log(ngx.DEBUG, "Redis authentication successful") + return ok, redis + else + ngx.log(ngx.DEBUG, "No password authentication for Redis") + return true, redis + end +end + +function RedisConnectionProvider:closeConnection(redis_instance) + redis_instance:set_keepalive(max_idle_timeout, pool_size) +end + +function RedisConnectionProvider:closeConnectionWithTimeout(redis_instance, max_idle_timeout) + redis_instance:set_keepalive(max_idle_timeout, pool_size) +end + +return RedisConnectionProvider \ No newline at end of file diff --git a/src/lua/api-gateway/redis/redisHealthCheck.lua b/src/lua/api-gateway/redis/redisHealthCheck.lua index 7768d34..b08c93e 100644 --- a/src/lua/api-gateway/redis/redisHealthCheck.lua +++ b/src/lua/api-gateway/redis/redisHealthCheck.lua @@ -19,158 +19,252 @@ -- DEALINGS IN THE SOFTWARE. --- --- Created by IntelliJ IDEA. --- User: nramaswa --- Date: 4/17/14 --- Time: 7:38 PM --- To change this template use File | Settings | File Templates. --- - +--- Redis healthcheck module, which checks for both primary and backup upstreams health, +-- required by the RedisConnectionProvider module --- Base class for redis health check to get the healthy node - -local base = require "api-gateway.validation.base" - -local HealthCheck = {} +local RedisHealthCheck = {} local DEFAULT_SHARED_DICT = "cachedkeys" +local HEALTHY_REDIS_UPSTREAM_KEY_PREFIX = "healthy_redis_upstream:" -function HealthCheck:new(o) +function RedisHealthCheck:new(o) o = o or {} setmetatable(o, self) self.__index = self - if ( o ~= nil ) then - self.shared_dict = o.shared_dict or DEFAULT_SHARED_DICT + if (o ~= nil) then + self.sharedDictionary = o.sharedDictionary or DEFAULT_SHARED_DICT end return o end --- Reused from the "resty.upstream.healthcheck" module to get the --- status of the upstream nodes -local function gen_peers_status_info(peers, bits, idx) - local npeers = #peers - for i = 1, npeers do - local peer = peers[i] - bits[idx] = peer.name - if peer.down then - bits[idx + 1] = " DOWN\n" +--- Splits a string by a given separator +-- @param inputStr The string to be split +-- @param separator The separator used in splitting +-- @return Array containing the substrings after separation +local function split(inputStr, separator) + if separator == nil then + separator = "%s" + end + + local table = {} + local index = 1 + for str in string.gmatch(inputStr, "([^%s" .. separator .. "]+)") do + table[index] = str + index = index + 1 + end + return table +end + +--- Splits the host:port upstream format into corresponding fields +-- @param upstreamRedis Upstream address as host:port +-- @return Upstream host +-- @return Upstream port +local function getHostAndPortInUpstream(upstreamRedis) + local host, port + local upstreamNameSeparator = ":" + local splitUpstreamAddress = split(upstreamRedis, upstreamNameSeparator) + -- For the moment, validate the address is host:port + if #splitUpstreamAddress == 2 then + host = splitUpstreamAddress[1] + port = splitUpstreamAddress[2] + else + return nil, nil + end + return host, port +end + + +--- Checks if a Redis peer is healthy by opening a TCP connection and sending a PING message. If upstream_password +--- is present, authentication is performed prior to pinging the machine +-- @param peerName Redis upstream in host:port format +-- @param upstreamPassword upstream password used for authentication before pinging +-- @return true if the peer is considered healthy (PING response is PONG), false otherwise (password may be incorrect, +-- instance may be down) +local function isPeerHealthy(upstream, upstreamPassword) + + local enableRedisAdvancedHealthcheck = ngx.var.enable_redis_advanced_healthcheck + if enableRedisAdvancedHealthcheck ~= "true" then + ngx.log(ngx.DEBUG, "No advanced healthcheck, assuming peer is healthy: " .. tostring(upstream)) + return true + end + + local authMessage = "AUTH " + local successfulAuthResponse = "OK" + local pingMessage = "PING\r\n" + local pongResponse = "PONG" + + local peerHost, peerPort = getHostAndPortInUpstream(upstream) + + -- for now, we should only validate host:port + if peerHost == nil or peerPort == nil then + return false + end + + ngx.log(ngx.DEBUG, "Checking health for: " .. tostring(peerHost) .. ":" .. tostring(peerPort)) + local socket = ngx.socket.tcp + local tcpSocket = socket() + local ok, err = tcpSocket:connect(peerHost, peerPort) + if err or not ok then + ngx.log(ngx.ERR, "Could not open TCP connection to Redis: ", err) + tcpSocket:close() + return false + end + + tcpSocket:settimeout(2000) + + -- Remove spaces to eliminate false lengths + if upstreamPassword ~= nil then + upstreamPassword = string.gsub(upstreamPassword, "%s+", "") + end + + -- First auth using provided password + if upstreamPassword and upstreamPassword ~= nil and upstreamPassword ~= '' and type(upstreamPassword) == 'string' then + tcpSocket:send(authMessage .. upstreamPassword .. '\r\n') + local message, err, _ = tcpSocket:receive() + if err then + ngx.log(ngx.ERR, "Error while receiving message from Redis: ", err) + tcpSocket:close() + return false + end + if not message or not string.match(message, successfulAuthResponse) then + return false + end + end + + tcpSocket:send(pingMessage) + local message, err, _ = tcpSocket:receive() + tcpSocket:close() + if err then + ngx.log(ngx.ERR, "Error while health checking Redis: ", err) + return false + end + return message and string.match(message, pongResponse) +end + +--- Generates peers health status +-- @param peers Array of peers to be health checked +-- @param upstreamPassword Peer password used for authentication prior to health checking (one password for now) +-- @return Upstream status array as peer -> 1 (healthy), 0(unhealthy) +local function generatePeersStatus(peers, upstreamPassword) + local status = {} + for i = 1, #peers do + local currentPeerName = peers[i].name + if isPeerHealthy(currentPeerName, upstreamPassword) then + status[currentPeerName] = 1 else - bits[idx + 1] = " up\n" + status[currentPeerName] = 0 end - idx = idx + 2 end - return idx + return status end --- Pass the name of any upstream for which the health check is performed by the --- "resty.upstream.healthcheck" module. This is only to get the results of the healthcheck -local function getHealthCheckForUpstream(upstreamName) - local ok, upstream = pcall(require, "ngx.upstream") +--- Checks for healthy upstreams using the ngx_upstream_lua module. It checks for both primary and secondary peers +-- @param upstream The name of the upstream, as defined in the conf files +-- @param upstreamPassword The password for the upstream, needed to perform authentication prior to the actual healthcheck +-- @return Upstream status as a table containing host:port -> 1(healthy) or 0(unhealthy) +local function getHealthCheckForUpstream(upstream, upstreamPassword) + local ok, upstreamModule = pcall(require, "ngx.upstream") if not ok then error("ngx_upstream_lua module required") end - local get_primary_peers = upstream.get_primary_peers - local get_backup_peers = upstream.get_backup_peers + local get_primary_peers = upstreamModule.get_primary_peers + local get_backup_peers = upstreamModule.get_backup_peers - local ok, new_tab = pcall(require, "table.new") - if not ok or type(new_tab) ~= "function" then - new_tab = function (narr, nrec) return {} end - end + local peersStatus = {} - local n = 1 - local bits = new_tab(n * 20, 0) - local idx = 1 + local primaryPeers, err = get_primary_peers(upstream) + if err or not primaryPeers then + ngx.log(ngx.ERR, "Failed to get primary peers in upstream " .. tostring(upstream) .. ":" .. err) + else + local primaryPeersStatus = generatePeersStatus(primaryPeers, upstreamPassword) - local peers, err = get_primary_peers(upstreamName) - if not peers then - return "failed to get primary peers in upstream " .. upstreamName .. ": " - .. err + if primaryPeersStatus ~= nil then + for k, v in pairs(primaryPeersStatus) do + peersStatus[k] = v + end end + end - idx = gen_peers_status_info(peers, bits, idx) + local backupPeers, err = get_backup_peers(upstream) - peers, err = get_backup_peers(upstreamName) - if not peers then - return "failed to get backup peers in upstream " .. upstreamName .. ": " - .. err + if err or not backupPeers then + ngx.log(ngx.ERR, "Failed to get backup peers in upstream " .. tostring(upstream) .. ":" .. err) + else + local backupPeersStatus = generatePeersStatus(backupPeers, upstreamPassword) + if backupPeersStatus ~= nil then + for k, v in pairs(backupPeersStatus) do + peersStatus[k] = v + end end - idx = gen_peers_status_info(peers, bits, idx) + end - return bits + return peersStatus end -local function getHealthyRedisNodeFromCache(dict_name, upstream_name) - local dict = ngx.shared[dict_name]; +--- Returns the upstream address from the shared cache +-- @param dictionaryName Shared dictionary containing the cached entries +-- @param upstreamName The name of the upstream, used in the caching key +-- @return Upstream address as host:port +local function getHealthyRedisNodeFromCache(dictionaryName, upstreamName) + local dict = ngx.shared[dictionaryName]; local upstreamRedis - if ( nil ~= dict ) then - upstreamRedis = dict:get("healthy_redis_upstream:" .. tostring(upstream_name) ) + if (nil ~= dict) then + upstreamRedis = dict:get(HEALTHY_REDIS_UPSTREAM_KEY_PREFIX .. tostring(upstreamName)) end return upstreamRedis end -local function updateHealthyRedisNodeInCache(dict_name, upstream_name, healthy_redis_host) - local dict = ngx.shared[dict_name]; - if ( nil ~= dict ) then - ngx.log(ngx.DEBUG, "Saving a healthy redis host:", healthy_redis_host, " in cache:", dict_name, " for upstream:", upstream_name) - local exp_time_in_seconds = 5 - dict:set("healthy_redis_upstream:" .. tostring(upstream_name), healthy_redis_host, exp_time_in_seconds) +--- Sets the healthy upstream address in the shared cache +-- @param dictionaryName Shared dictionary containing the cached entries +-- @param upstreamName The name of the upstream, used in the caching key +-- @param healthyRedisHost Upstream address as host:port +local function updateHealthyRedisNodeInCache(dictionaryName, upstreamName, healthyRedisHost) + local dict = ngx.shared[dictionaryName]; + if (nil ~= dict) then + ngx.log(ngx.DEBUG, "Saving a healthy redis host:", healthyRedisHost, " in cache:", dictionaryName, " for upstream:", upstreamName) + local expiryTimeInSeconds = 5 + dict:set(HEALTHY_REDIS_UPSTREAM_KEY_PREFIX .. tostring(upstreamName), healthyRedisHost, expiryTimeInSeconds) return end - - ngx.log(ngx.WARN, "Dictionary ", dict_name, " doesn't seem to be set. Did you define one ? ") + ngx.log(ngx.WARN, "Dictionary ", dictionaryName, " doesn't seem to be set. Did you define one ? ") end -local function getHostAndPortInUpstream(upstreamRedis) - local p = {} - p.host = upstreamRedis - - local idx = string.find(upstreamRedis, ":", 1, true) - if idx then - p.host = string.sub(upstreamRedis, 1, idx - 1) - p.port = tonumber(string.sub(upstreamRedis, idx + 1)) - end - return p.host, p.port -end - --- Get the redis node to use for read. --- Returns 3 values: --- The difference between upstream and is that the upstream may be just a string containing host:port -function HealthCheck:getHealthyRedisNode(upstream_name) +--- Checks for healthy Redis nodes and returns the first correct value +-- @param upstream The name of the upstream for which a healthy address is to be found +-- @param upstreamPassword The password for the upstream, should this need authentication prior to health checking +-- @return Full upstream address as host:port +-- @return Upstream host +-- @return Upstream port +function RedisHealthCheck:getHealthyRedisNode(upstream, upstreamPassword) -- get the Redis host and port from the local cache first - local healthy_redis_host = getHealthyRedisNodeFromCache(self.shared_dict, upstream_name) - if ( nil ~= healthy_redis_host) then - local host, port = getHostAndPortInUpstream(healthy_redis_host) - return healthy_redis_host, host, port + local healthyRedisHost = getHealthyRedisNodeFromCache(self.sharedDictionary, upstream) + if (nil ~= healthyRedisHost) then + local host, port = getHostAndPortInUpstream(healthyRedisHost) + return healthyRedisHost, host, port end - ngx.log(ngx.DEBUG, "Looking up for a healthy redis node in upstream:", upstream_name) + ngx.log(ngx.DEBUG, "Looking up for a healthy redis node in upstream:", upstream) -- if the Redis host is not in the local cache get it from the upstream configuration - local redisUpstreamHealthResult = getHealthCheckForUpstream(upstream_name) + local redisUpstreamHealthResult = getHealthCheckForUpstream(upstream, upstreamPassword) - if(redisUpstreamHealthResult == nil) then - ngx.log(ngx.ERR, "\n No upstream results found for redis!!! ") - return nil + if (redisUpstreamHealthResult == nil) then + ngx.log(ngx.ERR, "No upstream results found for Redis!!!") + return nil, nil, nil end - - for key,value in ipairs(redisUpstreamHealthResult) do + for upstream, status in pairs(redisUpstreamHealthResult) do -- return the first node found to be up. - -- TODO: save all the nodes that are up and return them using round-robin alg - if(value == " up\n") then - healthy_redis_host = redisUpstreamHealthResult[key-1] - updateHealthyRedisNodeInCache(self.shared_dict, upstream_name, healthy_redis_host) - local host, port = getHostAndPortInUpstream(healthy_redis_host) - return healthy_redis_host, host, port - end - if(value == " DOWN\n" and redisUpstreamHealthResult[key-1] ~= nil ) then - ngx.log(ngx.WARN, "\n Redis node " .. tostring(redisUpstreamHealthResult[key-1]) .. " is down! Checking for backup nodes. ") + if (status == 1) then + healthyRedisHost = upstream + updateHealthyRedisNodeInCache(self.sharedDictionary, upstream, healthyRedisHost) + local host, port = getHostAndPortInUpstream(healthyRedisHost) + return healthyRedisHost, host, port end end - ngx.log(ngx.ERR, "\n All Redis nodes are down!!! ") - return nil -- No redis nodes are up + ngx.log(ngx.ERR, "All Redis nodes are down!!!") + return nil, nil, nil end -return HealthCheck \ No newline at end of file +return RedisHealthCheck \ No newline at end of file diff --git a/src/lua/api-gateway/util/OauthClient.lua b/src/lua/api-gateway/util/OauthClient.lua new file mode 100644 index 0000000..4299332 --- /dev/null +++ b/src/lua/api-gateway/util/OauthClient.lua @@ -0,0 +1,132 @@ +-- Copyright (c) 2018 Adobe Systems Incorporated. All rights reserved. +-- +-- Permission is hereby granted, free of charge, to any person obtaining a +-- copy of this software and associated documentation files (the "Software"), +-- to deal in the Software without restriction, including without limitation +-- the rights to use, copy, modify, merge, publish, distribute, sublicense, +-- and/or sell copies of the Software, and to permit persons to whom the +-- Software is furnished to do so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in +-- all copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +-- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +-- DEALINGS IN THE SOFTWARE. + +--- +--- Created by trifan. +--- DateTime: 10/01/2018 12:18 +--- +--- +--- Created by trifan. +--- DateTime: 10/01/2018 11:36 +--- + +local OauthClient = {} + +function OauthClient:new(o) + local o = o or {} + setmetatable(o, self) + self.__index = self + return o +end + +local dogstats = require "api-gateway.dogstatsd.Dogstatsd" +local dogstatsInstance = dogstats:new() + +--- Namespace used for computing metric names for Dogstatsd +OauthClient.oauthHttpCallsNamespace = 'oauth.http_calls' + +--- Increments the number of calls to the Oauth provider +-- @param metric - metric to be identified in the Dogstatsd dashboard +-- @return - void method +-- +function OauthClient:increment(metric) + dogstatsInstance:increment(metric, 1) +end + +--- Measures the number of milliseconds elapsed +-- @param metric - metric to be identified in the Dogstatsd dashboard +-- @param ms - the time it took a call to finish in milliseconds +-- @return - void method +-- +function OauthClient:time(metric, ms) + dogstatsInstance:time(metric, ms) +end + +--- Pushes metrics about the total number of https calls to the oauth provider, +--- the time it took for a http call to finish and the response status code. +--- +-- @param oauthHttpCallsNamespace - Namespace used for computing metric names for Dogstatsd +-- @param metricsIdentifier - metric identifier +-- @param startTime - The time the call was initiated +-- @param endTime - The time the call returned +-- @param statusCode - The status code returned by the call +-- @return - void method +-- +function OauthClient:pushMetrics(oauthHttpCallsNamespace, metricsIdentifier, startTime, endTime, statusCode) + local noOfOauthHttpCallsMetric = oauthHttpCallsNamespace + local elapsedTimeMetric = oauthHttpCallsNamespace .. '.' .. metricsIdentifier .. '.duration' + local oauthStatusMetric = oauthHttpCallsNamespace .. '.' .. metricsIdentifier .. '.status.' .. statusCode + + local elapsedTime = string.format("%.3f", endTime - startTime) + + self:increment(noOfOauthHttpCallsMetric) + self:time(elapsedTimeMetric, elapsedTime) + self:increment(oauthStatusMetric) +end + +function OauthClient:makeValidateTokenCall(internalPath, oauth_host, oauth_token) + oauth_host = oauth_host or ngx.var.oauth_host + oauth_token = oauth_token or ngx.var.authtoken + + ngx.log(ngx.INFO, "validateToken request to host=", oauth_host) + + local startTime = os.clock() + local res = ngx.location.capture(internalPath, { + share_all_vars = true, + args = { authtoken = oauth_token } + }) + local endTime = os.clock() + + self:pushMetrics(self.oauthHttpCallsNamespace, 'makeValidateTokenCall', startTime, endTime, res.status) + + local logLevel = ngx.INFO + if res.status ~= 200 then + logLevel = ngx.WARN + end + + ngx.log(logLevel, "validateToken Host=", oauth_host, " responded with status=", res.status, " and x-debug-id=", + tostring(res.header["X-DEBUG-ID"]), " body=", res.body) + + return res +end + +function OauthClient:makeProfileCall(internalPath, oauth_host) + + oauth_host = oauth_host or ngx.var.oauth_host + ngx.log(ngx.INFO, "profileCall request to host=", oauth_host) + local startTime = os.clock() + local res = ngx.location.capture(internalPath, { share_all_vars = true }) + local endTime = os.clock() + + local metricIdentifier = 'makeProfileCall.' .. internalPath + self:pushMetrics(self.oauthHttpCallsNamespace, metricIdentifier, startTime, endTime, res.status) + + local logLevel = ngx.INFO + if res.status ~= 200 then + logLevel = ngx.WARN + end + + ngx.log(logLevel, "profileCall Host=", oauth_host, " responded with status=", res.status, " and x-debug-id=", + tostring(res.header["X-DEBUG-ID"]), " body=", res.body) + + return res +end + +return OauthClient \ No newline at end of file diff --git a/src/lua/api-gateway/util/hasher.lua b/src/lua/api-gateway/util/hasher.lua new file mode 100644 index 0000000..17ba393 --- /dev/null +++ b/src/lua/api-gateway/util/hasher.lua @@ -0,0 +1,67 @@ +-- Copyright (c) 2018 Adobe Systems Incorporated. All rights reserved. +-- +-- Permission is hereby granted, free of charge, to any person obtaining a +-- copy of this software and associated documentation files (the "Software"), +-- to deal in the Software without restriction, including without limitation +-- the rights to use, copy, modify, merge, publish, distribute, sublicense, +-- and/or sell copies of the Software, and to permit persons to whom the +-- Software is furnished to do so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in +-- all copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +-- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +-- DEALINGS IN THE SOFTWARE. + +local str = require "resty.string" + +--- +-- Verifies whether a table contains a given value +-- @param table the table to verify +-- @param value the to search for +-- @return - true if the value is in the table or false otherwise +-- +local function contains(table, val) + for i=1,#table do + if table[i] == val then + return true + end + end + return false +end + +--- +-- Encrypts the plain_text using an algoithm specified via a Chef env variable - SHA256 is the default. +-- Possible values: sha256, sha224, sha512, sha384 +-- @param plain_text The Text to encode +-- @return - the encrypted text +-- +local function _hash(plain_text) + local algorithm = ngx.var.hashing_algorithm + if (algorithm == nil or algorithm == '') then + ngx.log(ngx.INFO, "No hashing algorithm has been passed. Defaulting to SHA256") + algorithm = "sha256" + end + + local hashing_algorithm = {"sha256", "sha224", "sha512", "sha384"} + + if not contains(hashing_algorithm, algorithm) then + ngx.log(ngx.INFO, "The hashing algorithm passed is invalid. Defaulting to SHA256") + algorithm = "sha256" + end + + local restySha = require ("resty." .. algorithm) + local sha = restySha:new() + sha:update(plain_text) + local digest = sha:final() + return str.to_hex(digest) +end + +return { + hash = _hash +} \ No newline at end of file diff --git a/src/lua/api-gateway/util/logger.lua b/src/lua/api-gateway/util/logger.lua new file mode 100644 index 0000000..75af907 --- /dev/null +++ b/src/lua/api-gateway/util/logger.lua @@ -0,0 +1,87 @@ +-- Copyright (c) 2018 Adobe Systems Incorporated. All rights reserved. +-- +-- Permission is hereby granted, free of charge, to any person obtaining a +-- copy of this software and associated documentation files (the "Software"), +-- to deal in the Software without restriction, including without limitation +-- the rights to use, copy, modify, merge, publish, distribute, sublicense, +-- and/or sell copies of the Software, and to permit persons to whom the +-- Software is furnished to do so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in +-- all copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +-- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +-- DEALINGS IN THE SOFTWARE. + +local set = false + +--- +-- Checks and returns the ngx.var.requestId if possible +-- ngx.var is not accessible in some nginx phases like init phase and so we also check this +-- +local function is_in_init_phase() + return ngx.var.requestId +end + +--- +-- Get an error log format level, [file:currentline:function_name() req_id=], message. This is passed to the +-- original ngx.log function +-- @param level - the log level like ngx.DEBUG, ngx.INFO, etc. +-- @param debugInfo - the debug.getinfo() table needed for the stacktrace +-- @param ... - other variables normally passed to ngx.log(), in general string concatenation +-- +local function getLogFormat(level, debugInfo, ...) + local status, request_id = pcall(is_in_init_phase) + --- testing for init phase + if not status then + request_id = "N/A" + end + + return level, "[", debugInfo.short_src, + ":", debugInfo.currentline, + ":", debugInfo.name, + "() req_id=", tostring(request_id), + "] ", ... +end + +--- +-- Replaces the ngx.log function with the original ngx.log but redecorate the message +-- +local function _decorateLogger() + if not set then + local oldNgx = ngx.log + ngx.log = function(level, ...) + -- gets the level 2 because level 1 is this function and I need my caller + -- nSl means line, name, source + local debugInfo = debug.getinfo(2, "nSl") + pcall(function(...) + oldNgx(getLogFormat(level, debugInfo, ...)) + end, ...) + end + set = true + end +end + +local function error(...) + ngx.log(ngx.ERR, ...) +end + +local function debug(...) + ngx.log(ngx.DEBUG, ...) +end + +local function info(...) + ngx.log(ngx.INFO, ...) +end + +return { + decorateLogger = _decorateLogger, + error = error, + debug = debug, + info = info +} \ No newline at end of file diff --git a/src/lua/api-gateway/validation/factory.lua b/src/lua/api-gateway/validation/factory.lua index 06d9458..3b8b0c0 100644 --- a/src/lua/api-gateway/validation/factory.lua +++ b/src/lua/api-gateway/validation/factory.lua @@ -39,9 +39,9 @@ local ApiKeyValidatorCls = require "api-gateway.validation.key.redisApiKeyValida local HmacSignatureValidator = require "api-gateway.validation.signing.hmacGenericSignatureValidator" local OAuthTokenValidator = require "api-gateway.validation.oauth2.oauthTokenValidator" local UserProfileValidator = require "api-gateway.validation.oauth2.userProfileValidator" +--- needed to be run in isolation and for fallback purposes +local logger = require "api-gateway.util.logger" - -local debug_mode = ngx.config.debug local function debug(...) if debug_mode then ngx.log(ngx.DEBUG, "validator: ", ...) @@ -52,10 +52,11 @@ end -- Function designed to be called from access_by_lua -- It calls an internal /validate-request path which can provide any custom implementation for request validation local function _validateRequest() + logger.decorateLogger() + if (ngx.var.request_method == 'OPTIONS') then return ngx.OK; end - local res = ngx.location.capture("/validate-request", { share_all_vars = true }); debug("Final validation result:" .. ngx.var.validate_request_response_body .. ", [" .. res.status .. "]") @@ -64,6 +65,12 @@ local function _validateRequest() end if res.status == ngx.HTTP_OK then + if (ngx.var.is_access_phase_tracking_enabled == "true") then + if (ngx.apiGateway.tracking ~= nil) then + ngx.log(ngx.DEBUG, "Request tracking done on access phase."); + ngx.apiGateway.tracking.track() + end + end return ngx.OK; end @@ -111,7 +118,6 @@ local function _validateUserProfile() return userProfileValidator:validateRequest() end - return { validateApiKey = _validateApiKey, validateHmacSignature = _validateHmacSignature, @@ -122,4 +128,3 @@ return { defaultValidateRequestImpl = _defaultValidateRequestImpl, } ---return _M diff --git a/src/lua/api-gateway/validation/key/redisApiKeyValidator.lua b/src/lua/api-gateway/validation/key/redisApiKeyValidator.lua index 8d6cb6f..46aa8b4 100644 --- a/src/lua/api-gateway/validation/key/redisApiKeyValidator.lua +++ b/src/lua/api-gateway/validation/key/redisApiKeyValidator.lua @@ -36,11 +36,16 @@ -- local BaseValidator = require "api-gateway.validation.validator" local cjson = require "cjson" -local redis = require "resty.redis" -local RedisHealthCheck = require "api-gateway.redis.redisHealthCheck" + +local RedisConnectionProvider = require "api-gateway.redis.redisConnectionProvider" +local RedisConnectionConfiguration = require "api-gateway.redis.redisConnectionConfiguration" local ApiKeyValidator = BaseValidator:new() +ApiKeyValidator["log_identifier"] = "api_key_validator_execution_time"; + +local redisConnectionProvider = RedisConnectionProvider:new() + local super = { instance = BaseValidator, getKeyFromRedis = BaseValidator.getKeyFromRedis, @@ -48,9 +53,9 @@ local super = { } local RESPONSES = { - MISSING_KEY = { error_code = "403000", message = "Api KEY is missing" }, - INVALID_KEY = { error_code = "403003", message = "Api KEY is invalid" }, - UNKNOWN_ERROR = { error_code = "503000", message = "Could not validate API KEY"} + MISSING_KEY = { error_code = "403000", message = "Api KEY is missing" }, + INVALID_KEY = { error_code = "403003", message = "Api KEY is invalid" }, + UNKNOWN_ERROR = { error_code = "503000", message = "Could not validate API KEY" } } --- @Deprecated @@ -59,29 +64,31 @@ local RESPONSES = { -- function ApiKeyValidator:getLegacyKeyFromRedis(redis_key) ngx.log(ngx.DEBUG, "Looking for a legacy api-key in Redis") - local red = redis:new(); + local connection_options = { + upstream = RedisConnectionConfiguration["apiKey"]["ro_upstream_name"], + password = os.getenv(RedisConnectionConfiguration["apiKey"]["env_password_variable"]) + } - local redis_host, redis_port = self:getRedisUpstream() - local ok, err = red:connect(redis_host, redis_port) - if ok then + local ok, redis = redisConnectionProvider:getConnection(connection_options); + if ok then --local selectresult, selecterror = red:hgetall(redis_key); -- these are the fields to be saved in the request variables. -- NOTE: all the fields have to be defined before in nginx configuration file like : set $realm 'default_value'; - local fields = {"key", "realm", "service_id", "service_name", "consumer_org_name", "app_name", "plan_name", "key_secret" } - local selectresult, selecterror = red:hmget(redis_key, "key", "realm", "service-id", "service-name", "consumer-org-name", "app-name", "plan-name", "key_secret") - red:set_keepalive(30000, 100); - if selectresult then + local fields = { "key", "realm", "service_id", "service_name", "consumer_org_name", "app_name", "plan_name", "key_secret" } + local selectresult, selecterror = redis:hmget(redis_key, "key", "realm", "service-id", "service-name", "consumer-org-name", "app-name", "plan-name", "key_secret") + redisConnectionProvider:closeConnection(redis) + if not selecterror then local api_key_obj = {} if selectresult and type(selectresult) == "table" then local found = 0 - for i,v in ipairs(selectresult) do + for i, v in ipairs(selectresult) do if type(v) == "string" then found = 1 api_key_obj[fields[i]] = v end end - if ( found == 0 ) then + if (found == 0) then return ngx.HTTP_NOT_FOUND; end --ngx.log(ngx.WARN, "JSON:" .. cjson.encode(json_output) ) @@ -89,9 +96,11 @@ function ApiKeyValidator:getLegacyKeyFromRedis(redis_key) return ngx.HTTP_NOT_FOUND end return api_key_obj; + else + ngx.log(ngx.ERR, "Failed to get key ", tostring(redis_key), " error: ", selecterror) + return ngx.HTTP_SERVICE_UNAVAILABLE; end else - ngx.log(ngx.WARN, "Could not connect to redis at[" .. redis_host .. ":" .. redis_port .. "]:", err); return ngx.HTTP_SERVICE_UNAVAILABLE; end end @@ -102,8 +111,8 @@ function ApiKeyValidator:getKeyFromRedis(hashed_key) local redis_metadata = super.getKeyFromRedis(ApiKeyValidator, redis_key, "metadata") if redis_metadata ~= nil then ngx.log(ngx.DEBUG, "Found API KEY Metadata in Redis:", tostring(redis_metadata)) - local metadata = assert( cjson.decode(redis_metadata), "Invalid metadata found in Redis:" .. tostring(redis_metadata) ) - if metadata ~= nil then + local metadata = assert(cjson.decode(redis_metadata), "Invalid metadata found in Redis:" .. tostring(redis_metadata)) + if metadata ~= nil then return metadata end end @@ -139,11 +148,11 @@ function ApiKeyValidator:validate_api_key() end local redis_key = self:getKeyFromRedis(hashedkey); - if (redis_key == ngx.HTTP_NOT_FOUND ) then + if (redis_key == ngx.HTTP_NOT_FOUND) then --return self:exitFn(ngx.HTTP_FORBIDDEN) return self:exitFn(RESPONSES.INVALID_KEY.error_code, cjson.encode(RESPONSES.INVALID_KEY)) end - if ( redis_key == ngx.HTTP_SERVICE_UNAVAILABLE ) then + if (redis_key == ngx.HTTP_SERVICE_UNAVAILABLE) then --return self:exitFn(ngx.HTTP_SERVICE_UNAVAILABLE) return self:exitFn(RESPONSES.UNKNOWN_ERROR.error_code, cjson.encode(RESPONSES.UNKNOWN_ERROR)) end @@ -158,6 +167,4 @@ function ApiKeyValidator:validateRequest(obj) return self:validate_api_key(); end -return ApiKeyValidator - - +return ApiKeyValidator \ No newline at end of file diff --git a/src/lua/api-gateway/validation/oauth2/oauthTokenValidator.lua b/src/lua/api-gateway/validation/oauth2/oauthTokenValidator.lua index 36f19cf..86cc180 100644 --- a/src/lua/api-gateway/validation/oauth2/oauthTokenValidator.lua +++ b/src/lua/api-gateway/validation/oauth2/oauthTokenValidator.lua @@ -1,4 +1,4 @@ --- Copyright (c) 2015 Adobe Systems Incorporated. All rights reserved. +-- Copyright (c) 2018 Adobe Systems Incorporated. All rights reserved. -- -- Permission is hereby granted, free of charge, to any person obtaining a -- copy of this software and associated documentation files (the "Software"), @@ -39,7 +39,11 @@ -- 4. oauth_token_expires_at local BaseValidator = require "api-gateway.validation.validator" +local redisConfigurationProvider = require "api-gateway.redis.redisConnectionConfiguration" +local OauthClient = require "api-gateway.util.OauthClient":new() local cjson = require "cjson" +local hasher = require "api-gateway.util.hasher" +local safeCjson = require "cjson.safe" local _M = BaseValidator:new({ RESPONSES = { @@ -52,9 +56,17 @@ local _M = BaseValidator:new({ } }) +_M["redis_RO_upstream"] = redisConfigurationProvider["oauth"]["ro_upstream_name"] +_M["redis_RW_upstream"] = redisConfigurationProvider["oauth"]["rw_upstream_name"] +_M["redis_pass_env"] = redisConfigurationProvider["oauth"]["env_password_variable"] +_M["log_identifier"] = "oauth_validator_execution_time"; + --- -- Maximum time in seconds specifying how long to cache a valid token in GW's memory local LOCAL_CACHE_TTL = 60 +--- +-- Maximum time in milliseconds specifying how long to cache a valid token in Redis +local REDIS_CACHE_TTL = 6 * 60 * 60 -- Hook to override the logic verifying if a token is valid function _M:isTokenValid(json) @@ -67,6 +79,9 @@ end -- @param json Token info object -- function _M:isCachedTokenValid(json) + if (json == nil) then + return -1 + end local expires_in_s = self:getExpiresIn(json.oauth_token_expires_at) return expires_in_s end @@ -85,7 +100,7 @@ end -- @param expire_at UTC expiration time in seconds -- function _M:getExpiresIn(expire_at) - if ( expire_at == nil ) then + if (expire_at == nil) then return LOCAL_CACHE_TTL end @@ -101,21 +116,26 @@ end function _M:storeTokenInCache(cacheLookupKey, cachingObj, expire_at_ms_utc) local expires_in_s = self:getExpiresIn(expire_at_ms_utc) - if ( expires_in_s <= 0 ) then + if (expires_in_s <= 0) then ngx.log(ngx.DEBUG, "OAuth Token was not persisted in the cache as it has expired at:" .. tostring(expire_at_ms_utc) .. ", while now is:" .. tostring(ngx.time() * 1000) .. " ms.") return nil end - local local_expire_in = math.min( expires_in_s, LOCAL_CACHE_TTL ) + local local_expire_in = math.min(expires_in_s, LOCAL_CACHE_TTL) ngx.log(ngx.DEBUG, "Storing a new token expiring in " .. tostring(local_expire_in) .. " s locally, out of a total validity of " .. tostring(expires_in_s) .. " s.") local cachingObjString = cjson.encode(cachingObj) + local default_ttl_expire = REDIS_CACHE_TTL + if ngx.var.max_oauth_redis_cache_ttl ~= nil and ngx.var.max_oauth_redis_cache_ttl ~= '' then + default_ttl_expire = ngx.var.max_oauth_redis_cache_ttl + end + self:setKeyInLocalCache(cacheLookupKey, cachingObjString, local_expire_in, "cachedOauthTokens") - self:setKeyInRedis(cacheLookupKey, "token_json", expire_at_ms_utc, cachingObjString) + self:setKeyInRedis(cacheLookupKey, "token_json", math.min(expire_at_ms_utc, (ngx.time() + default_ttl_expire) * 1000), cachingObjString) end --- -- Returns an object with a set of variables to be saved in the request's context and later in the request's vars --- IMPORTANT: This method is only called when validating a new token, otherwise the information from the cache --- is read and automatically added to the context based on the object returned by this method +-- IMPORTANT: This method is only called when validating a new token, otherwise the information from the cache +-- is read and automatically added to the context based on the object returned by this method -- @param tokenInfo An object with the decoded response from the OAuth 2.0 service -- function _M:extractContextVars(tokenInfo) @@ -123,14 +143,14 @@ function _M:extractContextVars(tokenInfo) cachingObj.oauth_token_scope = tokenInfo.token.scope cachingObj.oauth_token_client_id = tokenInfo.token.client_id cachingObj.oauth_token_user_id = tokenInfo.token.user_id - cachingObj.oauth_token_expires_at = tokenInfo.expires_at -- NOTE: Assumption: value in ms + cachingObj.oauth_token_expires_at = self:getMaxLocalCacheTTL(tokenInfo.expires_at) -- NOTE: Assumption: value in ms return cachingObj end -- TODO: cache invalid tokens too for a short while -- Check in the response if the token is valid -- function _M:checkResponseFromAuth(res, cacheLookupKey) - local json = cjson.decode(res.body) + local json = safeCjson.decode(res.body) if json ~= nil then local tokenValidity, error = self:isTokenValid(json) @@ -141,7 +161,7 @@ function _M:checkResponseFromAuth(res, cacheLookupKey) local cachingObj = self:extractContextVars(json) self:setContextProperties(cachingObj) - self:storeTokenInCache(cacheLookupKey, cachingObj, json.expires_at) + self:storeTokenInCache(cacheLookupKey, cachingObj, cachingObj.oauth_token_expires_at) return true end end @@ -153,14 +173,14 @@ function _M:getTokenFromCache(cacheLookupKey) local localCacheValue = self:getKeyFromLocalCache(cacheLookupKey, "cachedOauthTokens") if (localCacheValue ~= nil) then - ngx.log(ngx.DEBUG, "Found IMS token in local cache") + ngx.log(ngx.DEBUG, "Found oauth token in local cache") return localCacheValue end local redisCacheValue = self:getKeyFromRedis(cacheLookupKey, "token_json") if (redisCacheValue ~= nil) then - ngx.log(ngx.DEBUG, "Found IMS token in redis cache") --- self:setKeyInLocalCache(cacheLookupKey, redisCacheValue, 60, "cachedOauthTokens") + ngx.log(ngx.DEBUG, "Found oauth token in redis cache") + -- self:setKeyInLocalCache(cacheLookupKey, redisCacheValue, 60, "cachedOauthTokens") return redisCacheValue end return nil; @@ -176,35 +196,37 @@ function _M:validateOAuthToken() end --1. try to get token info from the cache first ( local or redis cache ) - local oauth_token_hash = ngx.md5(oauth_token) + local oauth_token_hash = hasher.hash(oauth_token) local cacheLookupKey = self:getOauthTokenForCaching(oauth_token_hash, oauth_host) local cachedToken = self:getTokenFromCache(cacheLookupKey) if (cachedToken ~= nil) then - -- ngx.log(ngx.WARN, "Cached token=" .. cachedToken) - local obj = cjson.decode(cachedToken) + -- ngx.log(ngx.INFO, "Cached token=" .. cachedToken) + local obj = safeCjson.decode(cachedToken) local tokenValidity, error = self:isCachedTokenValid(obj) - if tokenValidity > 0 then - local local_expire_in = math.min( tokenValidity, LOCAL_CACHE_TTL ) - ngx.log(ngx.DEBUG, "Caching locally a new token for " .. tostring(local_expire_in) .. " s, out of a total validity of " .. tostring(tokenValidity ) .. " s.") - self:setKeyInLocalCache(cacheLookupKey, cachedToken, local_expire_in , "cachedOauthTokens") + if (tokenValidity > 0) then + local local_expire_in = math.min(tokenValidity, LOCAL_CACHE_TTL) + ngx.log(ngx.DEBUG, "Caching locally a new token for " .. tostring(local_expire_in) .. " s, out of a total validity of " .. tostring(tokenValidity) .. " s.") + self:setKeyInLocalCache(cacheLookupKey, cachedToken, local_expire_in, "cachedOauthTokens") self:setContextProperties(obj) return ngx.HTTP_OK + elseif (tokenValidity == 0) then + ngx.log(ngx.DEBUG, "Token is still in the cache and it will expire in less than 1s") + else + -- at this point the cached token is not valid + ngx.log(ngx.INFO, "Invalid OAuth Token found in cache. OAuth host=" .. tostring(oauth_host)) + if (error == nil) then + error = self.RESPONSES.INVALID_TOKEN + end + error.error_code = error.error_code or self.RESPONSES.INVALID_TOKEN.error_code + return error.error_code, cjson.encode(error) end - -- at this point the cached token is not valid - ngx.log(ngx.WARN, "Invalid OAuth Token found in cache. OAuth host=" .. tostring(oauth_host)) - if (error == nil) then - error = self.RESPONSES.INVALID_TOKEN - end - error.error_code = error.error_code or self.RESPONSES.INVALID_TOKEN.error_code - return error.error_code, cjson.encode(error) end + ngx.log(ngx.INFO, "Failed to get oauth token from cache falling back to oauth provider") + -- 2. validate the token with the OAuth endpoint - local res = ngx.location.capture("/validate-token", { - share_all_vars = true, - args = { authtoken = oauth_token} - }) + local res = OauthClient:makeValidateTokenCall("/validate-token", oauth_host, oauth_token) if res.status == ngx.HTTP_OK then local tokenValidity, error = self:checkResponseFromAuth(res, cacheLookupKey) if (tokenValidity == true) then @@ -217,7 +239,10 @@ function _M:validateOAuthToken() end error.error_code = error.error_code or self.RESPONSES.INVALID_TOKEN.error_code return error.error_code, cjson.encode(error) + else + ngx.log(ngx.WARN, "Oauth provider call failed with status code=", res.status, " body=", res.body) end + return res.status, cjson.encode(self.RESPONSES.UNKNOWN_ERROR); end @@ -225,6 +250,12 @@ function _M:validateRequest() return self:exitFn(self:validateOAuthToken()) end +function _M:getMaxLocalCacheTTL(expires_at) + if ngx.var.max_oauth_local_cache_ttl ~= nil and ngx.var.max_oauth_local_cache_ttl ~= '' then + expires_at = math.min(expires_at, (ngx.var.max_oauth_local_cache_ttl + ngx.time()) * 1000) + end + return expires_at +end return _M diff --git a/src/lua/api-gateway/validation/oauth2/userProfileValidator.lua b/src/lua/api-gateway/validation/oauth2/userProfileValidator.lua index 22454f1..299fab4 100644 --- a/src/lua/api-gateway/validation/oauth2/userProfileValidator.lua +++ b/src/lua/api-gateway/validation/oauth2/userProfileValidator.lua @@ -43,22 +43,37 @@ -- Added the logic to check for the user country and pass it as header. -- local BaseValidator = require "api-gateway.validation.validator" +local redisConfigurationProvider = require "api-gateway.redis.redisConnectionConfiguration" +local OauthClient = require "api-gateway.util.OauthClient":new() local cjson = require "cjson" +local hasher = require "api-gateway.util.hasher" local _M = BaseValidator:new() +_M["redis_RO_upstream"] = redisConfigurationProvider["oauth"]["ro_upstream_name"] +_M["redis_RW_upstream"] = redisConfigurationProvider["oauth"]["rw_upstream_name"] +_M["redis_pass_env"] = redisConfigurationProvider["oauth"]["env_password_variable"] +_M.PROFILE_VALIDATION_LOCATION = "/validate-user" + +--- Nginx shared dictionary for storing user profiles +_M.USER_PROFILE_DICTIONARY = "cachedUserProfiles" + local RESPONSES = { - P_MISSING_TOKEN = { error_code = "403020", message = "Oauth token is missing" }, - INVALID_PROFILE = { error_code = "403023", message = "Profile is not valid" }, - NOT_ALLOWED = { error_code = "403024", message = "Not allowed to read the profile"}, - P_UNKNOWN_ERROR = { error_code = "503020", message = "Could not read the profile" } + P_MISSING_TOKEN = { error_code = "403020", message = "Oauth token is missing" }, + INVALID_PROFILE = { error_code = "403023", message = "Profile is not valid" }, + NOT_ALLOWED = { error_code = "403024", message = "Not allowed to read the profile"}, + P_UNKNOWN_ERROR = { error_code = "503020", message = "Could not read the profile" } } --- -- Maximum time in seconds specifying how long to cache a valid token in GW's memory local LOCAL_CACHE_TTL = 60 --- returns the key that should be used when looking up in the cache -- +--- +-- Maximum time in milliseconds specifying how long to cache a valid token in Redis +local REDIS_CACHE_TTL = 6 * 60 * 60 + +--- returns the key that should be used when looking up in the cache -- function _M:getCacheToken(token) local t = token; local oauth_host = ngx.var.oauth_host @@ -69,6 +84,29 @@ function _M:getCacheToken(token) end end +function _M:getCacheTokenLookupKey() + local oauth_token = ngx.var.authtoken + local oauth_token_hash = hasher.hash(oauth_token) + return self:getCacheToken(oauth_token_hash) +end + +function _M:getRedisCacheLookupProfileKey() + if self.PROFILE_VALIDATOR_CODE ~= nil and self.PROFILE_VALIDATOR_CODE ~= "" then + return "user_json:" .. self.PROFILE_VALIDATOR_CODE; + else + return "user_json"; + end +end + +function _M:getLocalCacheLookupProfileKey() + local cacheLookupKey = self:getCacheTokenLookupKey() + if self.PROFILE_VALIDATOR_CODE ~= nil and self.PROFILE_VALIDATOR_CODE ~= "" then + return cacheLookupKey .. ":" .. self.PROFILE_VALIDATOR_CODE; + else + return cacheLookupKey; + end +end + --- Converts the expire_at into expire_in in seconds -- @param expire_at UTC expiration time in seconds -- @@ -100,44 +138,75 @@ function _M:getContextPropertiesObject(obj) return props end -function _M:getProfileFromCache(cacheLookupKey) - local localCacheValue = self:getKeyFromLocalCache(cacheLookupKey, "cachedUserProfiles") +function _M:getProfileFromCache(cacheTokenLookupKey) + local redisCacheLookupProfileKey = self:getRedisCacheLookupProfileKey() + local localCacheLookupProfileKey = self:getLocalCacheLookupProfileKey() + + local localCacheValue = self:getKeyFromLocalCache(localCacheLookupProfileKey, self.USER_PROFILE_DICTIONARY) if ( localCacheValue ~= nil ) then - -- ngx.log(ngx.WARN, "Found profile in local cache") + -- ngx.log(ngx.INFO, "Found profile in local cache") return localCacheValue end - local redisCacheValue = self:getKeyFromRedis(cacheLookupKey, "user_json") + local redisCacheValue = self:getKeyFromRedis(cacheTokenLookupKey, redisCacheLookupProfileKey) if ( redisCacheValue ~= nil ) then ngx.log(ngx.DEBUG, "Found User Profile in Redis cache") local oauthTokenExpiration = ngx.ctx.oauth_token_expires_at local expiresIn = self:getExpiresIn(oauthTokenExpiration) local localExpiresIn = math.min( expiresIn, LOCAL_CACHE_TTL ) ngx.log(ngx.DEBUG, "Storing cached User Profile in the local cache for " .. tostring(localExpiresIn) .. " s out of a total validity of " .. tostring(expiresIn) .. " s.") - self:setKeyInLocalCache(cacheLookupKey, redisCacheValue, localExpiresIn, "cachedUserProfiles") + self:setKeyInLocalCache(localCacheLookupProfileKey, redisCacheValue, localExpiresIn, self.USER_PROFILE_DICTIONARY) return redisCacheValue end return nil; end -function _M:storeProfileInCache(cacheLookupKey, cachingObj) +function _M:storeProfileInCache(cacheTokenLookupKey, cachingObj) local cachingObjString = cjson.encode(cachingObj) - local oauthTokenExpiration = ngx.ctx.oauth_token_expires_at + local oauthTokenExpiration = (ngx.ctx.oauth_token_expires_at or ((ngx.time() + LOCAL_CACHE_TTL) * 1000)) local expiresIn = self:getExpiresIn(oauthTokenExpiration) + + if ( expiresIn <= 0 ) then + ngx.log(ngx.ERR, "OAuth Token was not persisted in the cache as it has expired at:" .. tostring(expiresIn) .. ", while now is:" .. tostring(ngx.time() * 1000) .. " ms.") + return nil + end + local localExpiresIn = math.min( expiresIn, LOCAL_CACHE_TTL ) ngx.log(ngx.DEBUG, "Storing new cached User Profile in the local cache for " .. tostring(localExpiresIn) .. " s out of a total validity of " .. tostring(expiresIn) .. " s.") - self:setKeyInLocalCache(cacheLookupKey, cachingObjString, localExpiresIn , "cachedUserProfiles") + local default_ttl_expire = REDIS_CACHE_TTL + if ngx.var.max_oauth_redis_cache_ttl ~= nil and ngx.var.max_oauth_redis_cache_ttl ~= '' then + default_ttl_expire = ngx.var.max_oauth_redis_cache_ttl + end + + local redisCacheLookupProfileKey = self:getRedisCacheLookupProfileKey() + local localCacheLookupProfileKey = self:getLocalCacheLookupProfileKey() + + self:setKeyInLocalCache(localCacheLookupProfileKey, cachingObjString, localExpiresIn , self.USER_PROFILE_DICTIONARY) + -- cache the use profile for 5 minutes - self:setKeyInRedis(cacheLookupKey, "user_json", oauthTokenExpiration or ((ngx.time() + LOCAL_CACHE_TTL) * 1000 ), cachingObjString) + self:setKeyInRedis(cacheTokenLookupKey, redisCacheLookupProfileKey, math.min(oauthTokenExpiration, (ngx.time() + default_ttl_expire) * 1000), cachingObjString) +end + +--- +-- Deletes user profile from redis and local cache. +-- +function _M:deleteProfileFromCache() + local localCacheLookupProfileKey = self:getLocalCacheLookupProfileKey() + self:deleteKeyInLocalCache(localCacheLookupProfileKey, self.USER_PROFILE_DICTIONARY) + + local cacheTokenLookupKey = self:getCacheTokenLookupKey() + self:deleteKeyFromRedis(cacheTokenLookupKey) end ---- Returns true if the profile is valid for the request context --- This method is to be overritten when this class is extended +--- Returns true if the profile is valid for the request context. If profile is not valid then it returns the failure +-- status code and message. +-- This method is to be overritten when this class is extended. -- @param cachedProfile The information about the user profile that gets cached +-- function _M:isProfileValid(cachedProfile) - return true + return true, nil, nil end --- @@ -157,51 +226,54 @@ function _M:extractContextVars(profile) end function _M:validateUserProfile() - -- ngx.var.authtoken needs to be set before calling this method - local oauth_token = ngx.var.authtoken - if oauth_token == nil or oauth_token == "" then - return RESPONSES.P_MISSING_TOKEN.error_code, cjson.encode(RESPONSES.P_MISSING_TOKEN) - end - --1. try to get user's profile from the cache first ( local or redis cache ) - local oauth_token_hash = ngx.md5(oauth_token) - local cacheLookupKey = self:getCacheToken(oauth_token_hash) - local cachedUserProfile = self:getProfileFromCache(cacheLookupKey) + local cacheTokenLookupKey = self:getCacheTokenLookupKey() + local cachedUserProfile = self:getProfileFromCache(cacheTokenLookupKey) if ( cachedUserProfile ~= nil ) then if (type(cachedUserProfile) == 'string') then cachedUserProfile = cjson.decode(cachedUserProfile) end self:setContextProperties(self:getContextPropertiesObject(cachedUserProfile)) - if ( self:isProfileValid(cachedUserProfile) == true ) then + + local isValid, failureErrorCode, failureMessage = self:isProfileValid(cachedUserProfile) + if isValid == true then return ngx.HTTP_OK + elseif failureErrorCode ~= nil and failureMessage ~= nil then + return failureErrorCode, failureMessage else return RESPONSES.INVALID_PROFILE.error_code, cjson.encode(RESPONSES.INVALID_PROFILE) end + end - -- 2. get the user profile from the IMS profile - local res = ngx.location.capture("/validate-user", { share_all_vars = true }) + ngx.log(ngx.INFO, "Failed to get profile from cache falling back to oauth provider") + -- 2. get the user profile from the oauth profile + local res = OauthClient:makeProfileCall(self.PROFILE_VALIDATION_LOCATION) + if res.status == ngx.HTTP_OK then - local json = cjson.decode(res.body) - if json ~= nil then + local json = cjson.decode(res.body) + if json ~= nil then local cachingObj = self:extractContextVars(json) self:setContextProperties(self:getContextPropertiesObject(cachingObj)) - self:storeProfileInCache(cacheLookupKey, cachingObj) - if ( self:isProfileValid(cachingObj) == true ) then + local isValid, failureErrorCode, failureMessage = self:isProfileValid(cachingObj) + if isValid == true then + self:storeProfileInCache(cacheTokenLookupKey, cachingObj) return ngx.HTTP_OK + elseif failureErrorCode ~= nil and failureMessage ~= nil then + return failureErrorCode, failureMessage else return RESPONSES.INVALID_PROFILE.error_code, cjson.encode(RESPONSES.INVALID_PROFILE) end else - ngx.log(ngx.WARN, "Could not decode /validate-user response:" .. tostring(res.body) ) + ngx.log(ngx.WARN, "Could not decode " .. self.PROFILE_VALIDATION_LOCATION .. " response:" .. tostring(res.body) ) end else - -- ngx.log(ngx.WARN, "Could not read /ims-profile. status=" .. res.status .. ".body=" .. res.body .. ". token=" .. ngx.var.authtoken) - ngx.log(ngx.WARN, "Could not read /validate-user. status=" .. res.status .. ".body=" .. res.body ) + -- ngx.log(ngx.WARN, "Could not read /oauth-profile. status=" .. res.status .. ".body=" .. res.body .. ". token=" .. ngx.var.authtoken) + ngx.log(ngx.WARN, "Could not read " .. self.PROFILE_VALIDATION_LOCATION .. ". status=" .. res.status .. ".body=" .. res.body ) if ( res.status == ngx.HTTP_UNAUTHORIZED or res.status == ngx.HTTP_BAD_REQUEST ) then return RESPONSES.NOT_ALLOWED.error_code, cjson.encode(RESPONSES.NOT_ALLOWED) end @@ -211,6 +283,13 @@ function _M:validateUserProfile() end function _M:validateRequest() + + local oauth_token = ngx.var.authtoken + if oauth_token == nil or oauth_token == "" then + ngx.log(ngx.DEBUG, "Token is either null or empty") + return self:exitFn(RESPONSES.P_MISSING_TOKEN.error_code, cjson.encode(RESPONSES.P_MISSING_TOKEN)) + end + return self:exitFn(self:validateUserProfile()) end diff --git a/src/lua/api-gateway/validation/validator.lua b/src/lua/api-gateway/validation/validator.lua index 39c4a62..e0950e2 100644 --- a/src/lua/api-gateway/validation/validator.lua +++ b/src/lua/api-gateway/validation/validator.lua @@ -30,15 +30,12 @@ -- 1. api-gateway-redis upstream needs to be set -- 2. api-gateway-redis-replica needs to be set -- -local base = require "api-gateway.validation.base" -local redis = require "resty.redis" -local RedisHealthCheck = require "api-gateway.redis.redisHealthCheck" -local cjson = require "cjson" -local debug_mode = ngx.config.debug +local base = require "api-gateway.validation.base" +local RedisHealthCheck = require "api-gateway.redis.redisHealthCheck" +local cjson = require "cjson" +local debug_mode = ngx.config.debug --- redis endpoints are assumed to be global per GW node and therefore are read here -local redis_RO_upstream = "api-gateway-redis-replica" -local redis_RW_upstream = "api-gateway-redis" +local RedisConnectionProvider = require "api-gateway.redis.redisConnectionProvider" -- class to be used as a base class for all api-gateway validators -- local BaseValidator = {} @@ -46,8 +43,14 @@ local redisHealthCheck = RedisHealthCheck:new({ shared_dict = "cachedkeys" }) +local redisConnectionProvider = RedisConnectionProvider:new() + function BaseValidator:new(o) local o = o or {} + self.redis_RO_upstream = self.redis_RO_upstream or "api-gateway-redis-replica" + self.redis_RW_upstream = self.redis_RW_upstream or "api-gateway-redis" + self.redis_pass_env = self.redis_pass_env or "REDIS_PASS_API_KEY" + self.log_identifier = self.log_identifier or nil setmetatable(o, self) self.__index = self return o @@ -68,6 +71,8 @@ function BaseValidator:getKeyFromLocalCache(key, dict_name) local localCachedKeys = ngx.shared[dict_name]; if (nil ~= localCachedKeys) then return localCachedKeys:get(key); + else + ngx.log(ngx.ERR, "Dictionary " .. dict_name .. " does not exist") end end @@ -75,19 +80,20 @@ function BaseValidator:setKeyInLocalCache(key, string_value, exptime, dict_name) local localCachedKeys = ngx.shared[dict_name]; if (nil ~= localCachedKeys) then return localCachedKeys:safe_set(key, string_value, exptime); + else + ngx.log(ngx.ERR, "Dictionary " .. dict_name .. " does not exist") end end -function BaseValidator:getRedisUpstream(upstream_name) - local n = upstream_name or redis_RO_upstream - local upstream, host, port = redisHealthCheck:getHealthyRedisNode(n) - ngx.log(ngx.DEBUG, "Obtained Redis Host:" .. tostring(host) .. ":" .. tostring(port), " from upstream:", n) - if (nil ~= host and nil ~= port) then - return host, port - end +function BaseValidator:deleteKeyInLocalCache(key, dict_name) + local localCachedKeys = ngx.shared[dict_name] - ngx.log(ngx.ERR, "Could not find a Redis upstream.") - return nil,nil + if (nil ~= localCachedKeys) then + ngx.log(ngx.DEBUG, "Deleting entry with key " .. key .. " from local cache [" .. dict_name .. "]") + return localCachedKeys:delete(key) + else + ngx.log(ngx.ERR, "Dictionary " .. dict_name .. " does not exist") + end end -- retrieves a saved information from the Redis cache -- @@ -100,22 +106,27 @@ function BaseValidator:getKeyFromRedis(key, hash_name) return self:getHashValueFromRedis(key, hash_name) end - local redisread = redis:new() - local redis_host, redis_port = self:getRedisUpstream(redis_RO_upstream) - local ok, err = redisread:connect(redis_host, redis_port) + local connection_options = { + upstream = self.redis_RO_upstream, + password = os.getenv(self.redis_pass_env) + } + + local ok, redisread = redisConnectionProvider:getConnection(connection_options); if ok then local result, err = redisread:get(key) - redisread:set_keepalive(30000, 100) - if ( not result and err ~= nil ) then - ngx.log(ngx.WARN, "Failed to read key " .. tostring(key) .. " from Redis cache:[", redis_host, ":", redis_port, "]. Error:", err) + redisConnectionProvider:closeConnection(redisread) + if (not result and err ~= nil) then + ngx.log(ngx.WARN, "Failed to read key " .. tostring(key) .. ". Error:", err) return nil else if (type(result) == 'string') then return result + elseif (result == ngx.null) then + ngx.log(ngx.WARN, "The value for the key " .. tostring(key) .. " is empty") + else + ngx.log(ngx.WARN, "type of result is not correct " .. tostring(type(result))) end end - else - ngx.log(ngx.WARN, "Failed to read key " .. tostring(key) .. " from Redis cache:[", redis_host, ":", redis_port, "]. Error:", err) end return nil; end @@ -124,29 +135,56 @@ end -- the method uses HGET redis command -- -- it returns the value of the key, when found in the cache, nil otherwise -- function BaseValidator:getHashValueFromRedis(key, hash_field) - local redisread = redis:new() - local redis_host, redis_port = self:getRedisUpstream(redis_RO_upstream) - local ok, err = redisread:connect(redis_host, redis_port) + local connection_options = { + upstream = self.redis_RO_upstream, + password = os.getenv(self.redis_pass_env) + } + + local ok, redisread = redisConnectionProvider:getConnection(connection_options); if ok then local redis_key, selecterror = redisread:hget(key, hash_field) - redisread:set_keepalive(30000, 100) + redisConnectionProvider:closeConnection(redisread) if (type(redis_key) == 'string') then return redis_key + elseif selecterror then + ngx.log(ngx.ERR, "failed to get key from redis ", tostring(key), " error: ", selecterror) end - else - ngx.log(ngx.WARN, "Failed to read key " .. tostring(key) .. " from Redis cache:[", redis_host, ":", redis_port, "]. Error:", err) end return nil; end + +-- is wrapper over redis exists but returns boolean instead +function BaseValidator:exists(key) + local connection_options = { + upstream = self.redis_RO_upstream, + password = os.getenv(self.redis_pass_env) + } + + local ok, redisread = redisConnectionProvider:getConnection(connection_options); + if ok then + local redis_key, selecterror = redisread:exists(key) + redisConnectionProvider:closeConnection(redisread) + if selecterror or redis_key ~= 1 then + ngx.log(ngx.WARN, "Failed to read key " .. key .. " from Redis cache ", selecterror) + return false + end + return true; + end + return false +end + -- saves a value into the redis cache. -- -- the method uses HSET redis command -- -- it retuns true if the information is saved in the cache, false otherwise -- function BaseValidator:setKeyInRedis(key, hash_name, keyexpires, value) - ngx.log(ngx.DEBUG, "Storing in Redis the key [", tostring(key), "], expireat=", tostring(keyexpires), ", value=", tostring(value) ) - local rediss = redis:new() - local redis_host, redis_port = self:getRedisUpstream(redis_RW_upstream) - local ok, err = rediss:connect(redis_host, redis_port) + ngx.log(ngx.DEBUG, "Storing in Redis the key [", tostring(key), "], expireat=", tostring(keyexpires), ", value=", tostring(value)) + local connection_options = { + upstream = self.redis_RW_upstream, + password = os.getenv(self.redis_pass_env) + } + + local ok, rediss = redisConnectionProvider:getConnection(connection_options); if ok then --ngx.log(ngx.DEBUG, "WRITING IN REDIS JSON OBJ key=" .. key .. "=" .. value .. ",expiring in:" .. (keyexpires - (os.time() * 1000)) ) rediss:init_pipeline() @@ -154,20 +192,36 @@ function BaseValidator:setKeyInRedis(key, hash_name, keyexpires, value) if keyexpires ~= nil then rediss:pexpireat(key, keyexpires) end - local commit_res, commit_err = rediss:commit_pipeline() - rediss:set_keepalive(30000, 100) + local _, commit_err = rediss:commit_pipeline() + redisConnectionProvider:closeConnection(rediss) --ngx.log(ngx.WARN, "SAVE RESULT:" .. cjson.encode(commit_res) ) if (commit_err == nil) then return true else ngx.log(ngx.WARN, "Failed to write the key [", key, "] in Redis. Error:", commit_err) end - else - ngx.log(ngx.WARN, "Failed to save key:" .. tostring(key) .. " into cache: [", tostring(redis_host) .. ":" .. tostring(redis_port), "]. Error:", err) end return false; end +function BaseValidator:deleteKeyFromRedis(key) + ngx.log(ngx.DEBUG, "Deleting key from Redis: " .. key) + local connection_options = { + upstream = self.redis_RW_upstream, + password = os.getenv(self.redis_pass_env) + } + + local ok, redis = redisConnectionProvider:getConnection(connection_options); + if ok then + local redisResponse, err = redis:del(key) + if err then + ngx.log(ngx.ERR, "Error while deleting key from redis: ", err) + return nil + end + return redisResponse + end +end + -- it accepts a table or a string and saves the properties into the current request context -- function BaseValidator:setContextProperties(cached_token) local jsonCacheObj = cached_token @@ -181,11 +235,60 @@ function BaseValidator:setContextProperties(cached_token) end end +-- TTL using LuaResty Redis +function BaseValidator:executeTtl(key) + local connection_options = { + upstream = self.redis_RO_upstream, + password = os.getenv(self.redis_pass_env) + } + + local ok, redis = redisConnectionProvider:getConnection(connection_options); + if ok then + ngx.log(ngx.DEBUG, "Executing TTL for key:" .. key) + local ttl, err = redis:ttl(key) + if not ttl then + ngx.log(ngx.ERR, "Could not execute TTL for key: " .. key .. ". Error: " .. err) + else + ngx.log(ngx.DEBUG, "TTL response: " .. ttl) + return ttl + end + end +end + +-- converts a response status to a valid HTTP status code +function BaseValidator:convertToValidHttpStatusCode(response_status) + response_status = tonumber(response_status) + if response_status == nil then + return 500 + end + if (response_status >= 100 and response_status <= 599) then + return response_status + end + + local http_code_str = string.sub(tostring(response_status), 1, 3) + local http_code_number = tonumber(http_code_str) + if http_code_number ~= nil and http_code_number >= 100 and http_code_number <= 599 then + return http_code_number + end + + ngx.log(ngx.DEBUG, "Status code: ", tostring(response_status), " is not in a valid HTTP Status Code format") + return 500 +end + -- generic exit function for a validator -- function BaseValidator:exitFn(status, resp_body) - ngx.header["Response-Time"] = ngx.now() - ngx.req.start_time() + local responseTime = ngx.now() - ngx.req.start_time() + ngx.header["Response-Time"] = responseTime + + if(self.log_identifier) then + if(ngx.var[self.log_identifier]) then + ngx.var[self.log_identifier] = string.format("%.3f", responseTime) + else + ngx.log(ngx.WARN, "ngx variable ", self.log_identifier , " is not declared in ngx conf") + end + end - ngx.status = status + ngx.status = self:convertToValidHttpStatusCode(status) if (ngx.null ~= resp_body) then ngx.say(resp_body) @@ -194,4 +297,34 @@ function BaseValidator:exitFn(status, resp_body) return ngx.OK end +function BaseValidator:overrideErrorResponses(custom_error_responses) + + --- handle the case when custom_error_responses is passed as string + if type(custom_error_responses) == "string" then + custom_error_responses = cjson.decode(custom_error_responses) + end + + if custom_error_responses ~= nil and type(custom_error_responses) == "table" then + + local existing_custom_error_responses = ngx.var.validator_custom_error_responses + if existing_custom_error_responses ~= nil and existing_custom_error_responses ~= "" then + ngx.log(ngx.DEBUG, "ngx.var.validator_custom_error_responses already exist. Going to merge...") + + existing_custom_error_responses = cjson.decode(existing_custom_error_responses) + for k, v in pairs(custom_error_responses) do + if (existing_custom_error_responses[k] == nil) then + existing_custom_error_responses[k] = v + end + end + + ngx.var.validator_custom_error_responses = cjson.encode(existing_custom_error_responses) + else + + ngx.var.validator_custom_error_responses = cjson.encode(custom_error_responses) + end + else + ngx.log(ngx.DEBUG, "No custom error responses defined for validator") + end +end + return BaseValidator \ No newline at end of file diff --git a/src/lua/api-gateway/validation/validatorsHandler.lua b/src/lua/api-gateway/validation/validatorsHandler.lua index 7d9d6fa..1fdbbf0 100644 --- a/src/lua/api-gateway/validation/validatorsHandler.lua +++ b/src/lua/api-gateway/validation/validatorsHandler.lua @@ -27,8 +27,8 @@ -- -- location /my-location { -- request_validator "on; path=/validate_api_key; args=api_key,service_id; order=1"; --- request_validator "on; path=/validate_ims_oauth; args=authtoken; order=1"; --- request_validator "on; path=/validate_ims_profile"; args=authtoken; order=1"; +-- request_validator "on; path=/validate_oauth_oauth; args=authtoken; order=1"; +-- request_validator "on; path=/validate_oauth_profile"; args=authtoken; order=1"; -- request_validator "on; path=/validate_user_plan; args=oauth_user_id; order=2"; -- } -- @@ -36,8 +36,8 @@ -- Subrequests share all variables, and write properties into the request context -- location /my-location { -- set $validate_api_key "on; path=/validate_api_key; order=1; "; --- set $validate_ims_oauth "on; path=/validate_ims_oauth; order=1; "; --- set $validate_ims_profile "on; path=/validate_ims_profile"; order=1; "; +-- set $validate_oauth_oauth "on; path=/validate_oauth_oauth; order=1; "; +-- set $validate_oauth_profile "on; path=/validate_oauth_profile"; order=1; "; -- set $validate_user_plan "on; path=/validate_user_plan; order=2; "; -- set $request_validator_1 "on; path=/validate_a_custom_case; order=2; "; -- @@ -47,10 +47,6 @@ -- Time: 2:55 PM -- - -local base = require "api-gateway.validation.base" -local cjson = require "cjson" - -- class to be used as a base class for all api-gateway validators -- local ValidatorsHandler = {} @@ -174,7 +170,6 @@ function ValidatorsHandler:getValidatorsFromConfiguration( localContext ) ) end end - return reqs; end @@ -183,11 +178,6 @@ function ValidatorsHandler:validateSubrequests(order, subrequests, localContext, local subrequests_count = table.getn(subrequests) ngx.log(ngx.DEBUG, "Validating " .. subrequests_count .. " subrequests. Order=" .. order ) - if (subrequests_count == 0) then - ngx.log(ngx.WARN, "Nothing to validate on subrequest") - ngx.exit(ngx.HTTP_OK) - end - -- issue all the requests at once and wait until they all return local resps = { ngx.location.capture_multi(subrequests) } local validation_response_status = ngx.HTTP_OK @@ -241,9 +231,11 @@ end function ValidatorsHandler:saveContextInRequestVars(localContext) for k,v in pairs(localContext) do -- for i,k in pairs(varsToSet) do - if ngx.var[k] ~= nil and type(localContext[k]) == "string" then + if ngx.var[k] ~= nil and (type(localContext[k]) == "string" or type(localContext[k]) == "number") then -- ngx.log(ngx.DEBUG, "Setting " .. k .. ",from: " .. ngx.var[k] .. ",to:" .. v) - ngx.var[k] = localContext[k] + if v ~= nil then + ngx.var[k] = v + end end end end @@ -254,19 +246,22 @@ function ValidatorsHandler:validateRequest() local subrequestResultStatus = ngx.HTTP_OK local subrequestResultBody local responseTimesHeaders = {} - local reqs_count = table.getn(reqs) + local reqs_count = table.maxn(reqs) ngx.log(ngx.DEBUG, "Executing " .. reqs_count .. " ordered subrequests") for i = 1,reqs_count do - --ngx.log(ngx.DEBUG, "Executing validators with order=" .. i) + ngx.log(ngx.DEBUG, "Executing validators with order=" .. i) --ngx.log(ngx.DEBUG, "Printing ctx object before executing validators of order:" .. i) local requests = reqs[i] - if ( requests ~= nil ) then +-- ngx.log(ngx.DEBUG, "Table requests " .. table.getn(requests)) + if ( requests ~= nil and table.maxn(requests) > 0) then subrequestResultStatus, subrequestResultBody = self:validateSubrequests(i, requests, localContext, responseTimesHeaders) if ( subrequestResultStatus ~= ngx.HTTP_OK ) then self:saveContextInRequestVars(localContext) return ngx.exit(subrequestResultStatus) end + else + ngx.log(ngx.DEBUG, "Skipped this validator.") end end self:saveContextInRequestVars(localContext) diff --git a/src/lua/api-gateway/validation/validatorsHandlerErrorDecorator.lua b/src/lua/api-gateway/validation/validatorsHandlerErrorDecorator.lua index 7acbbfd..dbb7c55 100644 --- a/src/lua/api-gateway/validation/validatorsHandlerErrorDecorator.lua +++ b/src/lua/api-gateway/validation/validatorsHandlerErrorDecorator.lua @@ -40,37 +40,41 @@ local debug_mode = ngx.config.debug -- When a validator fail with the given "error_code", the HTTP response code is the "http_status" associated to the "error_code" -- The "message" associated to the "error_code" is returned as well. local DEFAULT_RESPONSES = { + -- ip filtering + BLACKLIST_IP = { http_status = 403, error_code = 403012, message = '{"error_code":"403012","message":"Your IP is blacklisted"}', headers = { ["X-Request-Id"] = "ngx.var.requestId" } }, + WHITELIST_IP = { http_status = 403, error_code = 403013, message = '{"error_code":"403013","message":"Your IP is not whitelisted"}', headers = { ["X-Request-Id"] = "ngx.var.requestId" } }, -- redisApiKeyValidator error - MISSING_KEY = { http_status = 403, error_code = 403000, message = '{"error_code":"403000","message":"Api Key is required"}', headers = { ["X-Request-Id"] = "ngx.var.requestId" }}, - INVALID_KEY = { http_status = 403, error_code = 403003, message = '{"error_code":"403003","message":"Api Key is invalid"}', headers = { ["X-Request-Id"] = "ngx.var.requestId" }}, - K_UNKNOWN_ERROR = { http_status = 503, error_code = 503000, message = '{"error_code":"503000","message":"Could not validate Api Key"}', headers = { ["X-Request-Id"] = "ngx.var.requestId" }}, + MISSING_KEY = { http_status = 403, error_code = 403000, message = '{"error_code":"403000","message":"Api Key is required"}', headers = { ["X-Request-Id"] = "ngx.var.requestId" } }, + INVALID_KEY = { http_status = 403, error_code = 403003, message = '{"error_code":"403003","message":"Api Key is invalid"}', headers = { ["X-Request-Id"] = "ngx.var.requestId" } }, + K_UNKNOWN_ERROR = { http_status = 503, error_code = 503000, message = '{"error_code":"503000","message":"Could not validate Api Key"}', headers = { ["X-Request-Id"] = "ngx.var.requestId" } }, --oauth errors - MISSING_TOKEN = { http_status = 403, error_code = 403010, message = '{"error_code":"403010","message":"Oauth token is missing."}' , headers = { ["X-Request-Id"] = "ngx.var.requestId" }}, - INVALID_TOKEN = { http_status = 401, error_code = 401013, message = '{"error_code":"401013","message":"Oauth token is not valid"}' , headers = { ["X-Request-Id"] = "ngx.var.requestId" }}, - T_UNKNOWN_ERROR = { http_status = 503, error_code = 503010, message = '{"error_code":"503010","message":"Could not validate the oauth token"}', headers = { ["X-Request-Id"] = "ngx.var.requestId" }}, - SCOPE_MISMATCH = { http_status = 403, error_code = 403011, message = '{"error_code":"403011","message":"Scope mismatch"}', headers = { ["X-Request-Id"] = "ngx.var.requestId" }}, + MISSING_TOKEN = { http_status = 403, error_code = 403010, message = '{"error_code":"403010","message":"Oauth token is missing."}', headers = { ["X-Request-Id"] = "ngx.var.requestId" } }, + INVALID_TOKEN = { http_status = 401, error_code = 401013, message = '{"error_code":"401013","message":"Oauth token is not valid"}', headers = { ["X-Request-Id"] = "ngx.var.requestId" } }, + T_UNKNOWN_ERROR = { http_status = 503, error_code = 503010, message = '{"error_code":"503010","message":"Could not validate the oauth token"}', headers = { ["X-Request-Id"] = "ngx.var.requestId" } }, + SCOPE_MISMATCH = { http_status = 403, error_code = 403011, message = '{"error_code":"403011","message":"Scope mismatch"}', headers = { ["X-Request-Id"] = "ngx.var.requestId" } }, -- oauth profile error - P_MISSING_TOKEN = { http_status = 403, error_code = 403020, message = '{"error_code":"403020","message":"Oauth token missing or invalid"}' , headers = { ["X-Request-Id"] = "ngx.var.requestId" }}, - INVALID_PROFILE = { http_status = 403, error_code = 403023, message = '{"error_code":"403023","message":"Profile is not valid"}' , headers = { ["X-Request-Id"] = "ngx.var.requestId" }}, - NOT_ALLOWED = { http_status = 403, error_code = 403024, message = '{"error_code":"403024","message":"Not allowed to read the profile"}', headers = { ["X-Request-Id"] = "ngx.var.requestId" }}, - P_UNKNOWN_ERROR = { http_status = 403, error_code = 503020, message = '{"error_code":"503020","message":"Could not read the profile"}' , headers = { ["X-Request-Id"] = "ngx.var.requestId" }}, + P_MISSING_TOKEN = { http_status = 403, error_code = 403020, message = '{"error_code":"403020","message":"Oauth token missing or invalid"}', headers = { ["X-Request-Id"] = "ngx.var.requestId" } }, + INVALID_PROFILE = { http_status = 403, error_code = 403023, message = '{"error_code":"403023","message":"Profile is not valid"}', headers = { ["X-Request-Id"] = "ngx.var.requestId" } }, + NOT_ALLOWED = { http_status = 403, error_code = 403024, message = '{"error_code":"403024","message":"Not allowed to read the profile"}', headers = { ["X-Request-Id"] = "ngx.var.requestId" } }, + P_UNKNOWN_ERROR = { http_status = 403, error_code = 503020, message = '{"error_code":"503020","message":"Could not read the profile"}', headers = { ["X-Request-Id"] = "ngx.var.requestId" } }, -- hmacSha1SignatureValidator errors - MISSING_SIGNATURE = { http_status = 403, error_code = 403030, message = '{"error_code":"403030","message":"Signature is missing"}' , headers = { ["X-Request-Id"] = "ngx.var.requestId" }}, - INVALID_SIGNATURE = { http_status = 403, error_code = 403033, message = '{"error_code":"403033","message":"Signature is invalid"}' , headers = { ["X-Request-Id"] = "ngx.var.requestId" }}, - UNKNOWN_ERROR = { http_status = 503, error_code = 503030, message = '{"error_code":"503030","message":"Could not validate Signature"}' , headers = { ["X-Request-Id"] = "ngx.var.requestId" }}, + MISSING_SIGNATURE = { http_status = 403, error_code = 403030, message = '{"error_code":"403030","message":"Signature is missing"}', headers = { ["X-Request-Id"] = "ngx.var.requestId" } }, + INVALID_SIGNATURE = { http_status = 403, error_code = 403033, message = '{"error_code":"403033","message":"Signature is invalid"}', headers = { ["X-Request-Id"] = "ngx.var.requestId" } }, + UNKNOWN_ERROR = { http_status = 503, error_code = 503030, message = '{"error_code":"503030","message":"Could not validate Signature"}', headers = { ["X-Request-Id"] = "ngx.var.requestId" } }, -- Service limit errrors - LIMIT_EXCEEDED = { http_status = 429, error_code = 429001, message = '{"error_code":"429001","message":"Service usage limit reached"}' , headers = { ["X-Request-Id"] = "ngx.var.requestId" }}, - DEV_KEY_LIMIT_EXCEEDED = { http_status = 429, error_code = 429002, message = '{"error_code":"429002","message":"Developer key usage limit reached"}' , headers = { ["X-Request-Id"] = "ngx.var.requestId" }}, - BLOCK_REQUEST = { http_status = 429, error_code = 429050, message = '{"error_code":"429050","message":"Too many requests"}' , headers = { ["X-Request-Id"] = "ngx.var.requestId" }}, + LIMIT_EXCEEDED = { http_status = 429, error_code = 429001, message = '{"error_code":"429001","message":"Service usage limit reached"}', headers = { ["X-Request-Id"] = "ngx.var.requestId" } }, + DEV_KEY_LIMIT_EXCEEDED = { http_status = 429, error_code = 429002, message = '{"error_code":"429002","message":"Developer key usage limit reached"}', headers = { ["X-Request-Id"] = "ngx.var.requestId" } }, + BLOCK_REQUEST = { http_status = 429, error_code = 429050, message = '{"error_code":"429050","message":"Too many requests"}', headers = { ["X-Request-Id"] = "ngx.var.requestId", ["Retry-After"] = "ngx.var.retry_after" } }, -- App valdations - DELAY_CLIENT_ON_REQUEST = { http_status = 503, error_code = 503071, messsage = '', headers = { ["Retry_After"] = "300s" } , headers = { ["X-Request-Id"] = "ngx.var.requestId" }}, + DELAY_CLIENT_ON_REQUEST = { http_status = 503, error_code = 503071, messsage = '', headers = { ["Retry_After"] = "300s" }, headers = { ["X-Request-Id"] = "ngx.var.requestId" } }, -- CC Link validation - INVALID_LINK = { http_status = 403, error_code = 403040, message = '{"error_code":"403040","message":"Invalid link"}' , headers = { ["X-Request-Id"] = "ngx.var.requestId" }}, - LINK_NOT_FOUND = { http_status = 404, error_code = 404040, message = '{"error_code":"404040","message":"Link not found"}' , headers = { ["X-Request-Id"] = "ngx.var.requestId" }}, + INVALID_LINK = { http_status = 403, error_code = 403040, message = '{"error_code":"403040","message":"Invalid link"}', headers = { ["X-Request-Id"] = "ngx.var.requestId" } }, + LINK_NOT_FOUND = { http_status = 404, error_code = 404040, message = '{"error_code":"404040","message":"Link not found"}', headers = { ["X-Request-Id"] = "ngx.var.requestId" } }, -- Generate Hmac validators - MISSING_SOURCE = { http_status = 400, error_code = 400001, message = '{"error_code":"400001","message"="Missing digest source"}', headers = { ["X-Request-Id"] = "ngx.var.requestId" }}, - MISSING_SECRET = { http_status = 400, error_code = 400002, message = '{"error_code":"400002","message"="Missing digest secret"}', headers = { ["X-Request-Id"] = "ngx.var.requestId" }} - } + MISSING_SOURCE = { http_status = 400, error_code = 400001, message = '{"error_code":"400001","message":"Missing digest source"}', headers = { ["X-Request-Id"] = "ngx.var.requestId" } }, + MISSING_SECRET = { http_status = 400, error_code = 400002, message = '{"error_code":"400002","message":"Missing digest secret"}', headers = { ["X-Request-Id"] = "ngx.var.requestId" } }, + MISSING_HEADER = { http_status = 400, error_code = 400003, message = '{"error_code":"400003","message":"Missing header"}', headers = { ["X-Request-Id"] = "ngx.var.requestId" } } +} local default_responses_array local user_defined_responses @@ -79,12 +83,12 @@ local function getResponsesTemplate() return user_defined_responses or default_responses_array end -local function convertResponsesToArray( responses ) +local function convertResponsesToArray(responses) local a = {} - for k,v in pairs(responses) do - if ( v.error_code ~= nil and v.http_status ~= nil ) then + for k, v in pairs(responses) do + if (v.error_code ~= nil and v.http_status ~= nil) then --table.insert(a, v.error_code, { http_status = v.http_status, message = v.message } ) - table.insert(a, v.error_code, v ) + table.insert(a, v.error_code, v) end end return a @@ -101,30 +105,43 @@ function ValidatorHandlerErrorDecorator:new(o) end -- decorates the response by the given response_status and response_body -function ValidatorHandlerErrorDecorator:decorateResponse( response_status, response_body ) +function ValidatorHandlerErrorDecorator:decorateResponse(response_status, response_body) response_status = tonumber(response_status) local o = getResponsesTemplate()[response_status] - if ( o ~= nil ) then + -- If no match by status code (e.g. status was converted from an extended error_code like 401013 to 401), + -- try to extract the error_code from the response body and look up by that instead. + if (o == nil and response_body ~= nil and #response_body > 0 and response_body ~= "nil\n") then + local ok, json_body = pcall(cjson.decode, response_body) + if ok and json_body and json_body.error_code then + o = getResponsesTemplate()[tonumber(json_body.error_code)] + end + end + + if (o ~= nil) then ngx.status = self:convertToValidHttpStatusCode(o.http_status) -- NOTE: assumption: for the moment if it's custom, then it's application/json ngx.header["Content-Type"] = "application/json" -- add custom headers too - if ( o.headers ~= nil ) then + if (o.headers ~= nil) then local val, i, j - for k,v in pairs(o.headers) do + for k, v in pairs(o.headers) do val = tostring(v) -- see if the header is a variable and replace it with ngx.var. - i,j = string.find(val,"ngx.var.") - if ( i ~= nil and j ~= nil ) then - val = string.sub(val,j+1) - if ( #val > 0 ) then + i, j = string.find(val, "ngx.var.") + if (i ~= nil and j ~= nil) then + val = string.sub(val, j + 1) + if (#val > 0) then val = ngx.var[val] end end ngx.header[k] = val end end + -- initialize an nginx variable with the error_code in order to print it in the logging file + if (o.error_code ~= nil) then + ngx.var.request_validator_error_code = o.error_code; + end -- ngx.say(o.message) -- add custom message local msg = self:parseResponseMessage(o.message) @@ -133,13 +150,13 @@ function ValidatorHandlerErrorDecorator:decorateResponse( response_status, respo end -- if no custom status code was used, assume the default one is right by trusting the validators - if ( response_body ~= nil and #response_body > 0 and response_body ~= "nil\n" ) then + if (response_body ~= nil and #response_body > 0 and response_body ~= "nil\n") then ngx.status = self:convertToValidHttpStatusCode(response_status) - ngx.say( response_body ) + ngx.say(response_body) return end -- if there is no custom response form the validator just exit with the status - ngx.exit( self:convertToValidHttpStatusCode(response_status) ) + ngx.exit(self:convertToValidHttpStatusCode(response_status)) end --- Convert the codes sent by validators to real HTTP response codes @@ -156,7 +173,7 @@ function ValidatorHandlerErrorDecorator:convertToValidHttpStatusCode(response_st return http_code_number end - ngx.log(ngx.DEBUG, "Status code: " , tostring(response_status) , " has not a valid HTTP Status Code format") + ngx.log(ngx.DEBUG, "Status code: ", tostring(response_status), " has not a valid HTTP Status Code format") return 500 end @@ -169,7 +186,7 @@ function ValidatorHandlerErrorDecorator:parseResponseMessage(message) local cnt = 0 while cnt < 3 do from, to = ngx.re.find(m, "ngx.var.[a-zA-Z_0-9]+", "jo") - if(from) then + if (from) then var = string.sub(m, from, to) varName = string.sub(m, from + 8, to) -- "+ 8" jump over "ngx.var." value = ngx.var[varName] @@ -184,18 +201,19 @@ function ValidatorHandlerErrorDecorator:parseResponseMessage(message) end -- hook to overwrite the DEFAULT_RESPONSES by specifying a jsonString -function ValidatorHandlerErrorDecorator:setUserDefinedResponsesFromJson( jsonString ) - if ( jsonString == nil or #jsonString < 2) then +function ValidatorHandlerErrorDecorator:setUserDefinedResponsesFromJson(jsonString) + if (jsonString == nil or #jsonString < 2) then + user_defined_responses = nil return end - local r = assert( cjson.decode(jsonString), "Invalid user defined jsonString:" .. tostring(jsonString)) + local r = assert(cjson.decode(jsonString), "Invalid user defined jsonString:" .. tostring(jsonString)) if r ~= nil then user_defined_responses = r local user_responses = convertResponsesToArray(r) -- merge tables - for k,v in pairs(default_responses_array) do + for k, v in pairs(default_responses_array) do -- merge only if user didn't overwrite the default response - if ( user_responses[k] == nil ) then + if (user_responses[k] == nil) then user_responses[k] = v end end diff --git a/test/docker-compose-jenkins.yml b/test/docker-compose-jenkins.yml new file mode 100644 index 0000000..ede17b9 --- /dev/null +++ b/test/docker-compose-jenkins.yml @@ -0,0 +1,17 @@ +gateway: + image: adobeapiplatform/apigateway + links: + - redis:redis.docker + volumes: + - ~/tmp/apiplatform/api-gateway-request-validation/src/lua/api-gateway/util:/usr/local/api-gateway/lualib/api-gateway/util + - ~/tmp/apiplatform/api-gateway-request-validation/src/lua/api-gateway/validation:/usr/local/api-gateway/lualib/api-gateway/validation + - ~/tmp/apiplatform/api-gateway-request-validation/src/lua/api-gateway/redis:/usr/local/api-gateway/lualib/api-gateway/redis + - ~/tmp/apiplatform/api-gateway-request-validation/src/lua/api-gateway/dogstatsd:/usr/local/api-gateway/lualib/api-gateway/dogstatsd + - ~/tmp/apiplatform/api-gateway-request-validation/test/perl:/tmp/perl + - ../target/:/t + entrypoint: ["prove", "-I/usr/local/test-nginx-0.24/lib", "-I/usr/local/test-nginx-0.24/inc", "-r", "/tmp/perl/"] +redis: + image: redis:2.8 + volumes: + - ../test/scripts:/tmp/scripts + entrypoint: /tmp/scripts/start-redis.sh \ No newline at end of file diff --git a/test/docker-compose-with-password-jenkins.yml b/test/docker-compose-with-password-jenkins.yml new file mode 100644 index 0000000..aad921f --- /dev/null +++ b/test/docker-compose-with-password-jenkins.yml @@ -0,0 +1,23 @@ +gateway: + environment: + - REDIS_PASS_API_KEY=redisPasswordForTests + - REDIS_PASS_OAUTH=redisPasswordForTests + image: adobeapiplatform/apigateway + links: + - redis:redis.docker + volumes: + - ~/tmp/apiplatform/api-gateway-request-validation/src/lua/api-gateway/util:/usr/local/api-gateway/lualib/api-gateway/util + - ~/tmp/apiplatform/api-gateway-request-validation/src/lua/api-gateway/validation:/usr/local/api-gateway/lualib/api-gateway/validation + - ~/tmp/apiplatform/api-gateway-request-validation/src/lua/api-gateway/redis:/usr/local/api-gateway/lualib/api-gateway/redis + - ~/tmp/apiplatform/api-gateway-request-validation/src/lua/api-gateway/dogstatsd:/usr/local/api-gateway/lualib/api-gateway/dogstatsd + - ~/tmp/apiplatform/api-gateway-request-validation/test/perl:/tmp/perl + - ~/tmp/apiplatform/api-gateway-request-validation/target/:/t + entrypoint: ["prove", "-I/usr/local/test-nginx-0.24/lib", "-I/usr/local/test-nginx-0.24/inc", "-r", "/tmp/perl/"] +redis: + image: redis:2.8 + environment: + - REDIS_PASS_API_KEY=redisPasswordForTests + - REDIS_PASS_OAUTH=redisPasswordForTests + volumes: + - ../test/scripts:/tmp/scripts + entrypoint: /tmp/scripts/start-redis.sh redisPasswordForTests \ No newline at end of file diff --git a/test/docker-compose-with-password.yml b/test/docker-compose-with-password.yml new file mode 100644 index 0000000..62b206b --- /dev/null +++ b/test/docker-compose-with-password.yml @@ -0,0 +1,27 @@ +gateway: + environment: + - REDIS_PASSWORD=asd123 + - REDIS_PASS=asd123 + - REDIS_PASS_API_KEY=redisPasswordForTests + - REDIS_PASS_OAUTH=redisPasswordForTests + image: adobeapiplatform/apigateway + links: + - redis:redis.docker + volumes: + - ~/tmp/apiplatform/api-gateway-request-validation/src/lua/api-gateway/util:/usr/local/api-gateway/lualib/api-gateway/util + - ~/tmp/apiplatform/api-gateway-request-validation/src/lua/api-gateway/validation:/usr/local/api-gateway/lualib/api-gateway/validation + - ~/tmp/apiplatform/api-gateway-request-validation/src/lua/api-gateway/redis:/usr/local/api-gateway/lualib/api-gateway/redis + - ~/tmp/apiplatform/api-gateway-request-validation/src/lua/api-gateway/dogstatsd:/usr/local/api-gateway/lualib/api-gateway/dogstatsd + - ~/tmp/apiplatform/api-gateway-request-validation/test/perl:/tmp/perl + - ~/tmp/apiplatform/api-gateway-request-validation/target/:/t + entrypoint: ["prove", "-I/usr/local/test-nginx-0.24/lib", "-I/usr/local/test-nginx-0.24/inc", "-r", "/tmp/perl/"] +redis: + image: redis:2.8 + environment: + - REDIS_PASS_API_KEY=redisPasswordForTests + - REDIS_PASS_OAUTH=redisPasswordForTests + volumes: + - ../test/scripts:/tmp/scripts + entrypoint: /tmp/scripts/start-redis.sh redisPasswordForTests + ports: + - "6379:6379" \ No newline at end of file diff --git a/test/docker-compose.yml b/test/docker-compose.yml index cc0fc82..3aa97c9 100644 --- a/test/docker-compose.yml +++ b/test/docker-compose.yml @@ -3,11 +3,17 @@ gateway: links: - redis:redis.docker volumes: + - ~/tmp/apiplatform/api-gateway-request-validation/src/lua/api-gateway/util:/usr/local/api-gateway/lualib/api-gateway/util - ~/tmp/apiplatform/api-gateway-request-validation/src/lua/api-gateway/validation:/usr/local/api-gateway/lualib/api-gateway/validation + - ~/tmp/apiplatform/api-gateway-request-validation/src/lua/api-gateway/redis:/usr/local/api-gateway/lualib/api-gateway/redis + - ~/tmp/apiplatform/api-gateway-request-validation/src/lua/api-gateway/dogstatsd:/usr/local/api-gateway/lualib/api-gateway/dogstatsd - ~/tmp/apiplatform/api-gateway-request-validation/test/perl:/tmp/perl - - ~/tmp/apiplatform/api-gateway-request-validation/target/:/t + - ../target/:/t entrypoint: ["prove", "-I/usr/local/test-nginx-0.24/lib", "-I/usr/local/test-nginx-0.24/inc", "-r", "/tmp/perl/"] redis: image: redis:2.8 + volumes: + - ../test/scripts:/tmp/scripts + entrypoint: /tmp/scripts/start-redis.sh ports: - - "6379:6379" + - "6379:6379" \ No newline at end of file diff --git a/test/perl/api-gateway/validation/key/apiKeyValidator.t b/test/perl/api-gateway/validation/key/apiKeyValidator.t index 2bcd102..999bfe2 100644 --- a/test/perl/api-gateway/validation/key/apiKeyValidator.t +++ b/test/perl/api-gateway/validation/key/apiKeyValidator.t @@ -58,6 +58,11 @@ run_tests(); __DATA__ === TEST 1: test api_key is saved in redis + +--- main_config +env REDIS_PASS_API_KEY; +env REDIS_PASS_OAUTH; + --- http_config eval: $::HttpConfig --- config include ../../api-gateway/api_key_service.conf; @@ -68,22 +73,29 @@ X-Test: test --- request POST /cache/api_key?key=key-123&service_id=s-123 --- response_body eval -['{ - "key":"key-123", - "key_secret":"-", - "realm":"sandbox", - "service_id":"s-123", - "service_name":"_undefined_", - "consumer_org_name":"_undefined_", - "app_name":"_undefined_", - "plan_name":"_undefined_" - } -'] +[ +'{ + "key":"key-123", + "key_secret":"-", + "realm":"sandbox", + "service_id":"s-123", + "service_name":"_undefined_", + "consumer_org_name":"_undefined_", + "app_name":"_undefined_", + "plan_name":"_undefined_" + } +' +] --- error_code: 200 --- no_error_log [error] === TEST 2: check request without api_key parameter is rejected + +--- main_config +env REDIS_PASS_API_KEY; +env REDIS_PASS_OAUTH; + --- http_config eval: $::HttpConfig --- config include ../../api-gateway/api_key_service.conf; @@ -91,6 +103,7 @@ POST /cache/api_key?key=key-123&service_id=s-123 error_log ../test-logs/apiKeyValidator_test2_error.log debug; location /test-api-key { + set $service_id s-123; set $api_key $arg_api_key; @@ -109,6 +122,11 @@ GET /test-api-key [error] === TEST 3: check request with invalid api_key is rejected + +--- main_config +env REDIS_PASS_API_KEY; +env REDIS_PASS_OAUTH; + --- http_config eval: $::HttpConfig --- config include ../../api-gateway/api_key_service.conf; @@ -134,6 +152,11 @@ GET /test-api-key?api_key=ab123 [error] === TEST 4: test request with valid api_key + +--- main_config +env REDIS_PASS_API_KEY; +env REDIS_PASS_OAUTH; + --- http_config eval: $::HttpConfig --- config include ../../api-gateway/api_key_service.conf; @@ -157,15 +180,15 @@ GET /test-api-key?api_key=ab123 "GET /test-api-key?api_key=test-apikey-1234"] --- response_body eval ['{ - "key":"test-apikey-1234", - "key_secret":"-", - "realm":"sandbox", - "service_id":"s-123", - "service_name":"_undefined_", - "consumer_org_name":"_undefined_", - "app_name":"_undefined_", - "plan_name":"_undefined_" - } + "key":"test-apikey-1234", + "key_secret":"-", + "realm":"sandbox", + "service_id":"s-123", + "service_name":"_undefined_", + "consumer_org_name":"_undefined_", + "app_name":"_undefined_", + "plan_name":"_undefined_" + } ', "api-key is valid.\n", "api-key is valid.\n" @@ -174,6 +197,11 @@ GET /test-api-key?api_key=ab123 === TEST 5: test that api_key fields are saved in the request variables + +--- main_config +env REDIS_PASS_API_KEY; +env REDIS_PASS_OAUTH; + --- http_config eval: $::HttpConfig --- config include ../../api-gateway/api_key_service.conf; @@ -199,15 +227,15 @@ GET /test-api-key?api_key=ab123 "GET /test-api-key-5?api_key=test-apikey-12345"] --- response_body eval ['{ - "key":"test-apikey-12345", - "key_secret":"my-secret", - "realm":"sandbox", - "service_id":"s-123", - "service_name":"test-service-name", - "consumer_org_name":"test-consumer-name", - "app_name":"test-app-name", - "plan_name":"_undefined_" - } + "key":"test-apikey-12345", + "key_secret":"my-secret", + "realm":"sandbox", + "service_id":"s-123", + "service_name":"test-service-name", + "consumer_org_name":"test-consumer-name", + "app_name":"test-app-name", + "plan_name":"_undefined_" + } ', '{"valid":true}' . "\n", "service_name=test-service-name,consumer_org_name=test-consumer-name,app_name=test-app-name,secret=my-secret\n"] @@ -215,6 +243,11 @@ GET /test-api-key?api_key=ab123 === TEST 6: test debug headers + +--- main_config +env REDIS_PASS_API_KEY; +env REDIS_PASS_OAUTH; + --- http_config eval: $::HttpConfig --- config include ../../api-gateway/api_key_service.conf; @@ -237,15 +270,15 @@ GET /test-api-key?api_key=ab123 "GET /test-api-key?api_key=test-key-123&debug=true"] --- response_body eval ['{ - "key":"test-key-123", - "key_secret":"-", - "realm":"sandbox", - "service_id":"s-123", - "service_name":"_undefined_", - "consumer_org_name":"_undefined_", - "app_name":"_undefined_", - "plan_name":"_undefined_" - } + "key":"test-key-123", + "key_secret":"-", + "realm":"sandbox", + "service_id":"s-123", + "service_name":"_undefined_", + "consumer_org_name":"_undefined_", + "app_name":"_undefined_", + "plan_name":"_undefined_" + } ', "api-key is valid.\n"] --- response_headers_like eval @@ -258,6 +291,11 @@ GET /test-api-key?api_key=ab123 === TEST 7: test api-key related field starting with capital H + +--- main_config +env REDIS_PASS_API_KEY; +env REDIS_PASS_OAUTH; + --- http_config eval: $::HttpConfig --- config include ../../api-gateway/api_key_service.conf; @@ -282,15 +320,15 @@ GET /test-api-key?api_key=ab123 --- response_body eval [ '{ - "key":"H-test-apikey-1234", - "key_secret":"-", - "realm":"sandbox", - "service_id":"HH-123", - "service_name":"_undefined_", - "consumer_org_name":"_undefined_", - "app_name":"HHHH", - "plan_name":"_undefined_" - } + "key":"H-test-apikey-1234", + "key_secret":"-", + "realm":"sandbox", + "service_id":"HH-123", + "service_name":"_undefined_", + "consumer_org_name":"_undefined_", + "app_name":"HHHH", + "plan_name":"_undefined_" + } ', "api-key is valid.\n"] --- response_headers_like eval @@ -304,6 +342,11 @@ GET /test-api-key?api_key=ab123 === TEST 8: test with more api-key fields + +--- main_config +env REDIS_PASS_API_KEY; +env REDIS_PASS_OAUTH; + --- http_config eval: $::HttpConfig --- config include ../../api-gateway/api_key_service.conf; diff --git a/test/perl/api-gateway/validation/key/api_key_deprecated.t b/test/perl/api-gateway/validation/key/api_key_deprecated.t deleted file mode 100644 index e653bb3..0000000 --- a/test/perl/api-gateway/validation/key/api_key_deprecated.t +++ /dev/null @@ -1,253 +0,0 @@ -#/* -# * Copyright (c) 2015 Adobe Systems Incorporated. All rights reserved. -# * -# * Permission is hereby granted, free of charge, to any person obtaining a -# * copy of this software and associated documentation files (the "Software"), -# * to deal in the Software without restriction, including without limitation -# * the rights to use, copy, modify, merge, publish, distribute, sublicense, -# * and/or sell copies of the Software, and to permit persons to whom the -# * Software is furnished to do so, subject to the following conditions: -# * -# * The above copyright notice and this permission notice shall be included in -# * all copies or substantial portions of the Software. -# * -# * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# * DEALINGS IN THE SOFTWARE. -# * -# */ -# vim:set ft= ts=4 sw=4 et fdm=marker: -use lib 'lib'; -use Test::Nginx::Socket::Lua; -use Cwd qw(cwd); - -#worker_connections(1014); -#master_process_enabled(1); -#log_level('warn'); - -repeat_each(2); - -plan tests => repeat_each() * (blocks() * 4) + 14; - -my $pwd = cwd(); - -our $HttpConfig = <<_EOC_; - # lua_package_path "$pwd/scripts/?.lua;;"; - lua_package_path "src/lua/?.lua;/usr/local/lib/lua/?.lua;;"; - init_by_lua ' - local v = require "jit.v" - v.on("$Test::Nginx::Util::ErrLogFile") - require "resty.core" - '; - init_worker_by_lua ' - ngx.apiGateway = ngx.apiGateway or {} - ngx.apiGateway.validation = require "api-gateway.validation.factory" - '; - lua_shared_dict cachedkeys 50m; # caches api-keys - include ../../api-gateway/redis-upstream.conf; -_EOC_ - -#no_diff(); -no_long_string(); -run_tests(); - -__DATA__ - -=== TEST 1: test api_key is saved in redis ---- http_config eval: $::HttpConfig ---- config - include ../../api-gateway/api_key_service_deprecated.conf; - error_log ../test-logs/api_key_test1_error.log debug; - ---- more_headers -X-Test: test ---- request -POST /cache/api_key?key=k-123&service_id=s-123 ---- response_body eval -["+OK\r\n"] ---- error_code: 200 ---- no_error_log -[error] - -=== TEST 2: check request without api_key parameter is rejected ---- http_config eval: $::HttpConfig ---- config - include ../../api-gateway/api_key_service_deprecated.conf; - include ../../api-gateway/default_validators.conf; - error_log ../test-logs/api_key_test2_error.log debug; - - location /test-api-key { - set $service_id s-123; - - set $api_key $arg_api_key; - set_if_empty $api_key $http_x_api_key; - - set $validate_api_key on; - - access_by_lua "ngx.apiGateway.validation.validateRequest()"; - content_by_lua "ngx.say('api-key is valid.')"; - } ---- request -GET /test-api-key ---- response_body_like: {"error_code":"403000","message":"Api Key is required"} ---- error_code: 403 ---- no_error_log -[error] - -=== TEST 3: check request with invalid api_key is rejected ---- http_config eval: $::HttpConfig ---- config - include ../../api-gateway/api_key_service_deprecated.conf; - include ../../api-gateway/default_validators.conf; - error_log ../test-logs/api_key_test3_error.log debug; - - location /test-api-key { - set $service_id s-123; - - set $api_key $arg_api_key; - set_if_empty $api_key $http_x_api_key; - - set $validate_api_key on; - - access_by_lua "ngx.apiGateway.validation.validateRequest()"; - content_by_lua "ngx.say('api-key is valid.')"; - } ---- request -GET /test-api-key?api_key=ab123 ---- response_body_like: {"error_code":"403003","message":"Api Key is invalid"} ---- error_code: 403 ---- no_error_log -[error] - -=== TEST 4: test request with valid api_key ---- http_config eval: $::HttpConfig ---- config - include ../../api-gateway/api_key_service_deprecated.conf; - include ../../api-gateway/default_validators.conf; - error_log ../test-logs/api_key_test4_error.log debug; - - location /test-api-key { - set $service_id s-123; - - set $api_key $arg_api_key; - set_if_empty $api_key $http_x_api_key; - - set $validate_api_key on; - - access_by_lua "ngx.apiGateway.validation.validateRequest()"; - content_by_lua "ngx.say('api-key is valid.')"; - } ---- pipelined_requests eval -["POST /cache/api_key?key=test-key-1234&service_id=s-123", -"GET /test-api-key?api_key=test-key-1234", -"GET /test-api-key?api_key=test-key-1234"] ---- response_body eval -["+OK\r\n", -"api-key is valid.\n", -"api-key is valid.\n" -] ---- no_error_log - - -=== TEST 5: test that api_key fields are saved in the request variables ---- http_config eval: $::HttpConfig ---- config - include ../../api-gateway/api_key_service_deprecated.conf; - include ../../api-gateway/default_validators.conf; - error_log ../test-logs/api_key_test5_error.log debug; - - location /test-api-key-5 { - set $service_id s-123; - - set $api_key $arg_api_key; - set_if_empty $api_key $http_x_api_key; - - set $validate_api_key on; - - access_by_lua "ngx.apiGateway.validation.validateRequest()"; - content_by_lua " - ngx.say('service_name=' .. ngx.var.service_name .. ',consumer_org_name=' .. ngx.var.consumer_org_name .. ',app_name=' .. ngx.var.app_name .. ',secret=' .. tostring(ngx.var.key_secret) ) - "; - } ---- pipelined_requests eval -["POST /cache/api_key?key=test-key-12345&service_id=s-123&service_name=test-service-name&consumer_org_name=test-consumer-name&app_name=test-app-name&secret=my-secret", -"GET /cache/api_key/get?key=test-key-12345&service_id=s-123", -"GET /test-api-key-5?api_key=test-key-12345"] ---- response_body eval -["+OK\r\n", -'{"valid":true}' . "\n", -"service_name=test-service-name,consumer_org_name=test-consumer-name,app_name=test-app-name,secret=my-secret\n"] ---- no_error_log - - -=== TEST 6: test debug headers ---- http_config eval: $::HttpConfig ---- config - include ../../api-gateway/api_key_service_deprecated.conf; - include ../../api-gateway/default_validators.conf; - error_log ../test-logs/api_key_test6_error.log debug; - - location /test-api-key { - set $service_id s-123; - - set $api_key $arg_api_key; - set_if_empty $api_key $http_x_api_key; - - set $validate_api_key on; - - access_by_lua "ngx.apiGateway.validation.validateRequest()"; - content_by_lua "ngx.say('api-key is valid.')"; - } ---- pipelined_requests eval -["POST /cache/api_key?key=test-key-123&service_id=s-123", -"GET /test-api-key?api_key=test-key-123&debug=true"] ---- response_body eval -["+OK\r\n", -"api-key is valid.\n"] ---- response_headers_like eval -[ -"", -"X-Debug-Validation-Response-Times: /validate_api_key, \\d+ ms, status:200, request_validator \\[order:1\\], \\d+ ms, status:200" -] ---- no_error_log -[error] - - -=== TEST 7: test api-key related field starting with capital H ---- http_config eval: $::HttpConfig ---- config - include ../../api-gateway/api_key_service_deprecated.conf; - include ../../api-gateway/default_validators.conf; - error_log ../test-logs/api_key_test7_error.log debug; - - location /test-api-key { - set $service_id hH-123; - - set $api_key $arg_api_key; - set_if_empty $api_key $http_x_api_key; - - set $validate_api_key on; - - access_by_lua "ngx.apiGateway.validation.validateRequest()"; - content_by_lua "ngx.say('api-key is valid.')"; - } ---- pipelined_requests eval -[ -"POST /cache/api_key?key=test-key-1234_HHH&service_id=hH-123&app_name=hHHH", -"GET /test-api-key?api_key=test-key-1234_HHH&debug=true"] ---- response_body eval -[ -"+OK\r\n", -"api-key is valid.\n"] ---- response_headers_like eval -[ -"", -"X-Debug-Validation-Response-Times: /validate_api_key, \\d+ ms, status:200, request_validator \\[order:1\\], \\d+ ms, status:200" -] ---- no_error_log -[error] - diff --git a/test/perl/api-gateway/validation/oauth2/oauthTokenValidator.t b/test/perl/api-gateway/validation/oauth2/oauthTokenValidator.t index ae63803..454a0c4 100644 --- a/test/perl/api-gateway/validation/oauth2/oauthTokenValidator.t +++ b/test/perl/api-gateway/validation/oauth2/oauthTokenValidator.t @@ -22,6 +22,8 @@ # */ # 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); @@ -60,7 +62,14 @@ run_tests(); __DATA__ -=== TEST 1: test ims_token is validated correctly +=== TEST 1: test oauth_token is validated correctly + +--- main_config +env REDIS_PASS_API_KEY; +env REDIS_PASS_OAUTH; +env REDIS_PASSWORD; +env REDIS_PASS; + --- http_config eval: $::HttpConfig --- config include ../../api-gateway/api-gateway-cache.conf; @@ -78,7 +87,7 @@ __DATA__ set $validate_oauth_token on; access_by_lua "ngx.apiGateway.validation.validateRequest()"; - content_by_lua "ngx.say('ims token is valid.')"; + content_by_lua "ngx.say('oauth token is valid.')"; } location /validate-token { internal; @@ -90,12 +99,19 @@ Authorization: Bearer SOME_OAUTH_TOKEN_1 --- request GET /test-oauth-validation --- response_body eval -["ims token is valid.\n"] +["oauth token is valid.\n"] --- error_code: 200 --- no_error_log [error] -=== TEST 2: test ims_token is saved in the cache +=== TEST 2: test oauth_token is saved in the cache + +--- main_config +env REDIS_PASS_API_KEY; +env REDIS_PASS_OAUTH; +env REDIS_PASSWORD; +env REDIS_PASS; + --- http_config eval: $::HttpConfig --- config include ../../api-gateway/api-gateway-cache.conf; @@ -114,25 +130,32 @@ GET /test-oauth-validation set $validate_oauth_token on; access_by_lua "ngx.apiGateway.validation.validateRequest()"; - content_by_lua "ngx.say('ims token is valid.')"; + content_by_lua "ngx.say('oauth token is valid.')"; } location /get-from-cache { - set $authtoken $http_authorization; set_if_empty $authtoken $arg_user_token; set_by_lua $authtoken 'return ngx.re.gsub(ngx.arg[1], "bearer ", "","ijo") ' $authtoken; - set_md5 $authtoken_hash $authtoken; - set $key 'cachedoauth:$authtoken_hash'; content_by_lua ' + local hasher = require "api-gateway.util.hasher" + local oauthTokenHash = ngx.var.authtoken_hash + local key = ngx.var.key + + oauthTokenHash = hasher.hash(ngx.var.authtoken) + key = "cachedoauth:" .. oauthTokenHash + local BaseValidator = require "api-gateway.validation.validator" local TestValidator = BaseValidator:new() + TestValidator["redis_RO_upstream"] = "oauth-redis-ro-upstream" + TestValidator["redis_RW_upstream"] = "oauth-redis-rw-upstream" + TestValidator["redis_pass_env"] = "REDIS_PASS_OAUTH" local validator = TestValidator:new() - local res = validator:getKeyFromRedis(ngx.var.key, "token_json") + local res = validator:getKeyFromRedis(key, "token_json") if ( res ~= nil) then validator:exitFn(200, res) else - validator:exitFn(200, "OAuth " .. ngx.var.key .. " not found in local cache") + validator:exitFn(200, "OAuth " .. key .. " not found in local cache") end '; } @@ -141,9 +164,25 @@ GET /test-oauth-validation set $authtoken $http_authorization; set_if_empty $authtoken $arg_user_token; set_by_lua $authtoken 'return ngx.re.gsub(ngx.arg[1], "bearer ", "","ijo") ' $authtoken; - set_md5 $authtoken_hash $authtoken; - set $redis_ttl_cmd 'TTL cachedoauth:$authtoken_hash'; - rewrite /test-oauth-token-expiry(.*)$ /cache/redis_query?$redis_ttl_cmd last; + + content_by_lua ' + local hasher = require "api-gateway.util.hasher" + local oauthTokenHash = ngx.var.authtoken_hash + local key = ngx.var.key + + oauthTokenHash = hasher.hash(ngx.var.authtoken) + key = "cachedoauth:" .. oauthTokenHash + + local BaseValidator = require "api-gateway.validation.validator" + local TestValidator = BaseValidator:new() + local validator = TestValidator:new() + local res = validator:executeTtl(key) + if ( res ~= nil) then + validator:exitFn(200, res) + end + '; + + # rewrite /test-oauth-token-expiry(.*)$ /cache/redis_query?$redis_ttl_cmd last; # echo $redis_ttl_cmd; } location /validate-token { @@ -169,19 +208,27 @@ Authorization: Bearer SOME_OAUTH_TOKEN_TEST_2_X_0 "GET /test-oauth-token-expiry", ] --- response_body_like eval -[ "ims token is valid.\n" , -'.*{"oauth_token_client_id":"client_id_test_2","oauth_token_expires_at":\\d{13},"oauth_token_scope":"openid email profile","oauth_token_user_id":"21961FF44F97F8A10A490D36"}.*', +[ "oauth token is valid.\n" , +'.*{"oauth_token_scope":"openid email profile","oauth_token_client_id":"client_id_test_2","oauth_token_user_id":"21961FF44F97F8A10A490D36","oauth_token_expires_at":\\d{13}}.*', '.*"expires_at":\d+,.*', -'^:[1-4]\r\n$', # the cached token expiry time is in seconds, and it can only be between 1s to 4s, but not less. -1 response indicated the key is not cached or it has expired +'[1-4]', # the cached token expiry time is in seconds, and it can only be between 1s to 4s, but not less. -1 response indicated the key is not cached or it has expired 'OK\n', -'OAuth cachedoauth\:bd9fdbb91a974d1c94e65dc6a0ce31a4 not found in local cache', -'-2\r\n' # redis should have expired the oauth token by now +'OAuth cachedoauth\:5a6e9de38155078dd80f66330a013c9a3383a87b4879c5ec7ac7b42689330b21 not found in local cache', +'-2' # redis should have expired the oauth token by now ] --- timeout: 10s --- no_error_log [error] === TEST 3: test oauth vars are saved in request variables + +--- main_config +env REDIS_PASS_API_KEY; +env REDIS_PASS_OAUTH; +env REDIS_PASSWORD; +env REDIS_PASS; + + --- http_config eval: $::HttpConfig --- config include ../../api-gateway/api-gateway-cache.conf; @@ -240,7 +287,14 @@ Authorization: Bearer SOME_OAUTH_TOKEN_TEST3 --- no_error_log [error] -=== TEST 4: test IMS token is saved in redis and in the local cache +=== TEST 4: test oauth token is saved in redis and in the local cache + +--- main_config +env REDIS_PASS_API_KEY; +env REDIS_PASS_OAUTH; +env REDIS_PASSWORD; +env REDIS_PASS; + --- http_config eval: $::HttpConfig --- config include ../../api-gateway/api-gateway-cache.conf; @@ -263,7 +317,7 @@ Authorization: Bearer SOME_OAUTH_TOKEN_TEST3 set $validate_oauth_token on; access_by_lua "ngx.apiGateway.validation.validateRequest()"; - content_by_lua "ngx.say('ims token is valid.')"; + content_by_lua "ngx.say('oauth token is valid.')"; } location /test-oauth-validation-again { #set $oauth_token_scope 'unset'; @@ -279,7 +333,7 @@ Authorization: Bearer SOME_OAUTH_TOKEN_TEST3 set $validate_oauth_token on; access_by_lua "ngx.apiGateway.validation.validateRequest()"; - content_by_lua "ngx.say('ims token is also valid.')"; + content_by_lua "ngx.say('oauth token is also valid.')"; } location /validate-token { internal; @@ -288,33 +342,57 @@ Authorization: Bearer SOME_OAUTH_TOKEN_TEST3 } location /l2_cache/api_key { set $local_key $arg_key; - content_by_lua " + content_by_lua_block { local localCachedKeys = ngx.shared.cachedOauthTokens; if ( nil ~= localCachedKeys ) then local k = localCachedKeys:get(ngx.var.local_key); ngx.say('Local cache:' .. tostring(k) ); - -- ngx.say('Local cache:' .. ngx.var.local_key); end - "; + } + } + + location /query-for-key { + set $service_id s-123; + set $authtoken $http_authorization; + set_if_empty $authtoken $arg_user_token; + set_by_lua $authtoken 'return ngx.re.gsub(ngx.arg[1], "bearer ", "","ijo") ' $authtoken; + set_unescape_uri $query $query_string; + content_by_lua ' + ngx.log(ngx.DEBUG,"The query value is : "..ngx.var.query) + local BaseValidator = require "api-gateway.validation.validator" + local TestValidator = BaseValidator:new() + local validator = TestValidator:new() + local res = validator:getKeyFromRedis(ngx.var.query, "token_json") + if ( res ~= nil) then + ngx.say(tostring(res)) + end + '; } --- more_headers Authorization: Bearer SOME_OAUTH_TOKEN_TEST4 --- pipelined_requests eval ["GET /test-oauth-validation", -"GET /cache/redis_query?HGET%20cachedoauth:1eb30b79089ce83d1b18a89501b41998%20token_json", +"GET /query-for-key?cachedoauth:46223d289c67faf405c4d20f1c93d518e112d052752eedc58575a04e1e455922", "GET /test-oauth-validation-again", -"GET /l2_cache/api_key?key=cachedoauth:1eb30b79089ce83d1b18a89501b41998" +"GET /l2_cache/api_key?key=cachedoauth:46223d289c67faf405c4d20f1c93d518e112d052752eedc58575a04e1e455922" ] --- response_body_like eval -["ims token is valid.\n", -'.*{"oauth_token_client_id":"test_Client_ID","oauth_token_expires_at":\\d{13},"oauth_token_scope":"openid,AdobeID","oauth_token_user_id":"21961FF44F97F8A10A490D36"}.*', -"ims token is also valid.\n", -'Local cache:{"oauth_token_client_id":"test_Client_ID","oauth_token_expires_at":\\d{13},"oauth_token_scope":"openid,AdobeID","oauth_token_user_id":"21961FF44F97F8A10A490D36"}\n' +["oauth token is valid.\n", +'.*{"oauth_token_scope":"openid,AdobeID","oauth_token_client_id":"test_Client_ID","oauth_token_user_id":"21961FF44F97F8A10A490D36","oauth_token_expires_at":\\d{13}}.*', +"oauth token is also valid.\n", +'Local cache:{"oauth_token_scope":"openid,AdobeID","oauth_token_client_id":"test_Client_ID","oauth_token_user_id":"21961FF44F97F8A10A490D36","oauth_token_expires_at":\\d{13}}\n' ] --- no_error_log [error] === TEST 5: test invalid token returns 401 + +--- main_config +env REDIS_PASS_API_KEY; +env REDIS_PASS_OAUTH; +env REDIS_PASSWORD; +env REDIS_PASS; + --- http_config eval: $::HttpConfig --- config include ../../api-gateway/api-gateway-cache.conf; @@ -332,7 +410,7 @@ Authorization: Bearer SOME_OAUTH_TOKEN_TEST4 set $validate_oauth_token on; access_by_lua "ngx.apiGateway.validation.validateRequest()"; - content_by_lua "ngx.say('ims token is valid.')"; + content_by_lua "ngx.say('oauth token is valid.')"; } location /validate-token { @@ -351,6 +429,13 @@ GET /test-oauth-validation [error] === TEST 6: test that validation behaviour can be customized + +--- main_config +env REDIS_PASS_API_KEY; +env REDIS_PASS_OAUTH; +env REDIS_PASSWORD; +env REDIS_PASS; + --- http_config eval: $::HttpConfig --- config include ../../api-gateway/default_validators.conf; @@ -379,7 +464,7 @@ GET /test-oauth-validation set $validate_oauth_token "on; path=/validate_custom_oauth_token; order=1;"; set $custom_token_var $arg_custom_token; access_by_lua "ngx.apiGateway.validation.validateRequest()"; - content_by_lua "ngx.say('ims token is valid.')"; + content_by_lua "ngx.say('oauth token is valid.')"; } location /validate-token { diff --git a/test/perl/api-gateway/validation/oauth2/userProfileValidator.t b/test/perl/api-gateway/validation/oauth2/userProfileValidator.t index 08cb6ff..f349ac5 100644 --- a/test/perl/api-gateway/validation/oauth2/userProfileValidator.t +++ b/test/perl/api-gateway/validation/oauth2/userProfileValidator.t @@ -22,6 +22,8 @@ # */ # 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); @@ -31,7 +33,7 @@ use Cwd qw(cwd); repeat_each(2); -plan tests => repeat_each() * (blocks() * 8 ) - 12; +plan tests => repeat_each() * (blocks() * 8) - 10; my $pwd = cwd(); @@ -62,7 +64,12 @@ run_tests(); __DATA__ -=== TEST 1: test ims_profile is saved correctly in cache and in request variables +=== TEST 1: test oauth_profile is saved correctly in cache and in request variables + +--- main_config +env REDIS_PASS_API_KEY; +env REDIS_PASS_OAUTH; + --- http_config eval: $::HttpConfig --- config include ../../api-gateway/api-gateway-cache.conf; @@ -86,13 +93,22 @@ __DATA__ set $authtoken $http_authorization; set_if_empty $authtoken $arg_user_token; set_by_lua $authtoken 'return ngx.re.gsub(ngx.arg[1], "bearer ", "","ijo") ' $authtoken; - set_md5 $authtoken_hash $authtoken; - set $key 'cachedoauth:$authtoken_hash'; + content_by_lua ' + local hasher = require "api-gateway.util.hasher" + local oauthTokenHash = ngx.var.authtoken_hash + local key = ngx.var.key + + oauthTokenHash = hasher.hash(ngx.var.authtoken) + key = "cachedoauth:" .. oauthTokenHash + local BaseValidator = require "api-gateway.validation.validator" local v = BaseValidator:new() - local k = v:getKeyFromLocalCache(ngx.var.key,"cachedUserProfiles") - v:exitFn(200,"Local: " .. tostring(k)) + v["redis_RO_upstream"] = "oauth-redis-ro-upstream" + v["redis_RW_upstream"] = "oauth-redis-rw-upstream" + v["redis_pass_env"] = "REDIS_PASS_OAUTH" + local k = v:getKeyFromLocalCache(key,"cachedUserProfiles") + v:exitFn(200, "Local: " .. tostring(k)) '; } @@ -100,12 +116,21 @@ __DATA__ set $authtoken $http_authorization; set_if_empty $authtoken $arg_user_token; set_by_lua $authtoken 'return ngx.re.gsub(ngx.arg[1], "bearer ", "","ijo") ' $authtoken; - set_md5 $authtoken_hash $authtoken; - set $key 'cachedoauth:$authtoken_hash'; + content_by_lua ' + local hasher = require "api-gateway.util.hasher" + local oauthTokenHash = ngx.var.authtoken_hash + local key = ngx.var.key + + oauthTokenHash = hasher.hash(ngx.var.authtoken) + key = "cachedoauth:" .. oauthTokenHash + local BaseValidator = require "api-gateway.validation.validator" local v = BaseValidator:new() - local k = v:getKeyFromRedis(ngx.var.key,"user_json") + v["redis_RO_upstream"] = "oauth-redis-ro-upstream" + v["redis_RW_upstream"] = "oauth-redis-rw-upstream" + v["redis_pass_env"] = "REDIS_PASS_OAUTH" + local k = v:getKeyFromRedis(key,"user_json") v:exitFn(200,"Redis: " .. tostring(k)) '; } @@ -129,7 +154,12 @@ Authorization: Bearer SOME_OAUTH_PROFILE_TEST_1 --- no_error_log [error] -=== TEST 2: test ims_profile is saved correctly in cache and in request variables +=== TEST 2: test oauth_profile is saved correctly in cache and in request variables + +--- main_config +env REDIS_PASS_API_KEY; +env REDIS_PASS_OAUTH; + --- http_config eval: $::HttpConfig --- config include ../../api-gateway/api-gateway-cache.conf; @@ -151,23 +181,58 @@ Authorization: Bearer SOME_OAUTH_PROFILE_TEST_1 } location /local-cache { + # get OAuth token either from header or from the user_token query string + set $authtoken $http_authorization; + set_if_empty $authtoken $arg_user_token; + set_by_lua $authtoken 'return ngx.re.gsub(ngx.arg[1], "bearer ", "","ijo") ' $authtoken; + content_by_lua ' + local hasher = require "api-gateway.util.hasher" + local oauthTokenHash = ngx.var.authtoken_hash + local key = ngx.var.key + + oauthTokenHash = hasher.hash(ngx.var.authtoken) + key = "cachedoauth:" .. oauthTokenHash + local BaseValidator = require "api-gateway.validation.validator" local v = BaseValidator:new() - local k = v:getKeyFromLocalCache("cachedoauth:8cd12eadb5032aa2153c8f830d01e0be","cachedUserProfiles") - v:exitFn(200,k) + v["redis_RO_upstream"] = "oauth-redis-ro-upstream" + v["redis_RW_upstream"] = "oauth-redis-rw-upstream" + v["redis_pass_env"] = "REDIS_PASS_OAUTH" + local k = v:getKeyFromLocalCache(key,"cachedUserProfiles") + v:exitFn(200,tostring(k)) '; } location /redis-cache { + # get OAuth token either from header or from the user_token query string + set $authtoken $http_authorization; + set_if_empty $authtoken $arg_user_token; + set_by_lua $authtoken 'return ngx.re.gsub(ngx.arg[1], "bearer ", "","ijo") ' $authtoken; + content_by_lua ' + local hasher = require "api-gateway.util.hasher" + local oauthTokenHash = ngx.var.authtoken_hash + local key = ngx.var.key + + oauthTokenHash = hasher.hash(ngx.var.authtoken) + key = "cachedoauth:" .. oauthTokenHash + local BaseValidator = require "api-gateway.validation.validator" local v = BaseValidator:new() - local k = v:getKeyFromRedis("cachedoauth:8cd12eadb5032aa2153c8f830d01e0be","user_json") - v:exitFn(200,k) + v["redis_RO_upstream"] = "oauth-redis-ro-upstream" + v["redis_RW_upstream"] = "oauth-redis-rw-upstream" + v["redis_pass_env"] = "REDIS_PASS_OAUTH" + local k = v:getKeyFromRedis(key,"user_json") + v:exitFn(200,tostring(k)) '; } + location /validate-token { + internal; + set_by_lua $generated_expires_at 'return ((os.time() + 4) * 1000 )'; + return 200 '{"valid":false,"expires_at":$generated_expires_at,"token":{"id":"1234","scope":"openid email profile","user_id":"21961FF44F97F8A10A490D36","expires_in":"86400000","client_id":"test_Client_ID","type":"access_token"}}'; + } location /validate-user { internal; return 200 '{"countryCode":"CA","emailVerified":"true","email":"noreply@domain.com","userId":"1234","name":"full name","displayName":"display_name"}'; @@ -187,7 +252,12 @@ Authorization: Bearer SOME_OAUTH_TOKEN_TEST_TWO --- no_error_log [error] -=== TEST 3: test ims_profile can add corresponding headers to request +=== TEST 3: test oauth_profile can add corresponding headers to request + +--- main_config +env REDIS_PASS_API_KEY; +env REDIS_PASS_OAUTH; + --- http_config eval: $::HttpConfig --- config include ../../api-gateway/api-gateway-cache.conf; @@ -195,6 +265,11 @@ Authorization: Bearer SOME_OAUTH_TOKEN_TEST_TWO error_log ../test-logs/userProfileValidator_test3_error.log debug; + location /validate-token { + internal; + set_by_lua $generated_expires_at 'return ((os.time() + 4) * 1000 )'; + return 200 '{"valid":false,"expires_at":$generated_expires_at,"token":{"id":"1234","scope":"openid email profile","user_id":"21961FF44F97F8A10A490D36","expires_in":"86400000","client_id":"test_Client_ID","type":"access_token"}}'; + } location /test-validate-user { set $service_id s-123; # get OAuth token either from header or from the user_token query string @@ -230,7 +305,12 @@ X-User-Name: display_name-%E5%B7%A5%EF%BC%8D%E5%A5%B3%EF%BC%8D%E9%95%BF --- no_error_log [error] -=== TEST 4: test ims_profile with a null field +=== TEST 4: test oauth_profile with a null field + +--- main_config +env REDIS_PASS_API_KEY; +env REDIS_PASS_OAUTH; + --- http_config eval: $::HttpConfig --- config include ../../api-gateway/api-gateway-cache.conf; @@ -272,7 +352,12 @@ X-User-Name: display_name-%E5%B7%A5%EF%BC%8D%E5%A5%B3%EF%BC%8D%E9%95%BF --- no_error_log [error] -=== TEST 5: test ims_profile with a null name field +=== TEST 5: test oauth_profile with a null name field + +--- main_config +env REDIS_PASS_API_KEY; +env REDIS_PASS_OAUTH; + --- http_config eval: $::HttpConfig --- config include ../../api-gateway/api-gateway-cache.conf; @@ -313,3 +398,95 @@ X-User-Name: display_name-%E5%B7%A5%EF%BC%8D%E5%A5%B3%EF%BC%8D%E9%95%BF --- error_code: 200 --- no_error_log [error] + +=== TEST 6: test oauth_profile is saved correctly in cache and in request variables - hashed with sha 512 + +--- main_config +env REDIS_PASS_API_KEY; +env REDIS_PASS_OAUTH; + +--- http_config eval: $::HttpConfig +--- config + include ../../api-gateway/api-gateway-cache.conf; + include ../../api-gateway/default_validators.conf; + + error_log ../test-logs/userProfileValidator_test6_error.log debug; + + set $hashing_algorithm "sha512"; + + location /test-validate-user { + set $service_id s-123; + # get OAuth token either from header or from the user_token query string + set $authtoken $http_authorization; + set_if_empty $authtoken $arg_user_token; + set_by_lua $authtoken 'return ngx.re.gsub(ngx.arg[1], "bearer ", "","ijo") ' $authtoken; + + set $validate_user_profile on; + + access_by_lua "ngx.apiGateway.validation.validateRequest()"; + content_by_lua 'ngx.say("user_email=" .. ngx.var.user_email .. ",user_country_code=" .. ngx.var.user_country_code .. ",user_name=" .. ngx.var.user_name)'; + } + location /local-cache { + set $authtoken $http_authorization; + set_if_empty $authtoken $arg_user_token; + set_by_lua $authtoken 'return ngx.re.gsub(ngx.arg[1], "bearer ", "","ijo") ' $authtoken; + + content_by_lua ' + local hasher = require "api-gateway.util.hasher" + local oauthTokenHash = ngx.var.authtoken_hash + local key = ngx.var.key + + oauthTokenHash = hasher.hash(ngx.var.authtoken) + key = "cachedoauth:" .. oauthTokenHash + + local BaseValidator = require "api-gateway.validation.validator" + local v = BaseValidator:new() + v["redis_RO_upstream"] = "oauth-redis-ro-upstream" + v["redis_RW_upstream"] = "oauth-redis-rw-upstream" + v["redis_pass_env"] = "REDIS_PASS_OAUTH" + local k = v:getKeyFromLocalCache(key,"cachedUserProfiles") + v:exitFn(200, "Local: " .. tostring(k)) + '; + } + + location /redis-cache { + set $authtoken $http_authorization; + set_if_empty $authtoken $arg_user_token; + set_by_lua $authtoken 'return ngx.re.gsub(ngx.arg[1], "bearer ", "","ijo") ' $authtoken; + + content_by_lua ' + local hasher = require "api-gateway.util.hasher" + local oauthTokenHash = ngx.var.authtoken_hash + local key = ngx.var.key + + oauthTokenHash = hasher.hash(ngx.var.authtoken) + key = "cachedoauth:" .. oauthTokenHash + + local BaseValidator = require "api-gateway.validation.validator" + local v = BaseValidator:new() + v["redis_RO_upstream"] = "oauth-redis-ro-upstream" + v["redis_RW_upstream"] = "oauth-redis-rw-upstream" + v["redis_pass_env"] = "REDIS_PASS_OAUTH" + local k = v:getKeyFromRedis(key,"user_json") + v:exitFn(200,"Redis: " .. tostring(k)) + '; + } + + location /validate-user { + internal; + return 200 '{"countryCode":"AT","emailVerified":"true","email":"johndoe_ĂÂă@domain.com","userId":"1234","name":"full name","displayName":"display_name—大-女"}'; + } +--- more_headers +Authorization: Bearer SOME_OAUTH_PROFILE_TEST_1 +--- pipelined_requests eval +[ +"GET /test-validate-user", +"GET /local-cache", +"GET /redis-cache" +] +--- response_body_like eval +['^user_email=johndoe_ĂÂă\@domain.com,user_country_code=AT,user_name=display_name%E2%80%94%E5%A4%A7%EF%BC%8D%E5%A5%B3.*', +'Local: {"user_name":"display_name—大-女","user_email":"johndoe_ĂÂă@domain.com","user_country_code":"AT"}', +'Redis: {"user_name":"display_name—大-女","user_email":"johndoe_ĂÂă@domain.com","user_country_code":"AT"}'] +--- no_error_log +[error] diff --git a/test/perl/api-gateway/validation/signing/hmacGenericSignatureValidator.t b/test/perl/api-gateway/validation/signing/hmacGenericSignatureValidator.t index bc26f63..f3e1576 100644 --- a/test/perl/api-gateway/validation/signing/hmacGenericSignatureValidator.t +++ b/test/perl/api-gateway/validation/signing/hmacGenericSignatureValidator.t @@ -59,8 +59,14 @@ __DATA__ === TEST 1: test basic HMAC SHA1 signature validation + +--- main_config +env REDIS_PASS_API_KEY; +env REDIS_PASS_OAUTH; + --- http_config eval: $::HttpConfig --- config + error_log ../test-logs/hmacGenericSignatureValidator_test1_error.log debug; include ../../api-gateway/default_validators.conf; location /validate-hmac-sha1 { @@ -81,8 +87,14 @@ __DATA__ === TEST 2: test HMAC SHA1 validator with request validation + +--- main_config +env REDIS_PASS_API_KEY; +env REDIS_PASS_OAUTH; + --- http_config eval: $::HttpConfig --- config + error_log ../test-logs/hmacGenericSignatureValidator_test2_error.log debug; include ../../api-gateway/api_key_service.conf; include ../../api-gateway/default_validators.conf; @@ -122,15 +134,15 @@ __DATA__ --- response_body eval [ '{ - "key":"test-key-1234", - "key_secret":"-", - "realm":"sandbox", - "service_id":"s-123", - "service_name":"_undefined_", - "consumer_org_name":"_undefined_", - "app_name":"_undefined_", - "plan_name":"_undefined_" - } + "key":"test-key-1234", + "key_secret":"-", + "realm":"sandbox", + "service_id":"s-123", + "service_name":"_undefined_", + "consumer_org_name":"_undefined_", + "app_name":"_undefined_", + "plan_name":"_undefined_" + } ', "signature is valid\n" ] @@ -140,8 +152,14 @@ __DATA__ [error] === TEST 3: test HMAC SHA1 validator with API KEY validation + +--- main_config +env REDIS_PASS_API_KEY; +env REDIS_PASS_OAUTH; + --- http_config eval: $::HttpConfig --- config + error_log ../test-logs/hmacGenericSignatureValidator_test3_error.log debug; include ../../api-gateway/api_key_service.conf; include ../../api-gateway/default_validators.conf; @@ -173,15 +191,15 @@ __DATA__ --- response_body eval [ '{ - "key":"sZ28nvYnStSUS2dSzedgnwkJtUdLkNdR", - "key_secret":"mO2AIfdUQeQFiGQq", - "realm":"sandbox", - "service_id":"s-123", - "service_name":"_undefined_", - "consumer_org_name":"_undefined_", - "app_name":"_undefined_", - "plan_name":"_undefined_" - } + "key":"sZ28nvYnStSUS2dSzedgnwkJtUdLkNdR", + "key_secret":"mO2AIfdUQeQFiGQq", + "realm":"sandbox", + "service_id":"s-123", + "service_name":"_undefined_", + "consumer_org_name":"_undefined_", + "app_name":"_undefined_", + "plan_name":"_undefined_" + } ', "signature is valid\n" ] @@ -191,9 +209,15 @@ __DATA__ [error] === TEST 4: test HMAC SHA1 validator with API KEY validation with deprecated API-KEY API + +--- main_config +env REDIS_PASS_API_KEY; +env REDIS_PASS_OAUTH; + --- http_config eval: $::HttpConfig --- config - include ../../api-gateway/api_key_service_deprecated.conf; + error_log ../test-logs/hmacGenericSignatureValidator_test4_error.log debug; + include ../../api-gateway/api_key_service.conf; include ../../api-gateway/default_validators.conf; location /v1.0/accounts/ { @@ -223,7 +247,17 @@ __DATA__ ] --- response_body eval [ -"+OK\r\n", +'{ + "key":"sZ28nvYnStSUS2dSzedgnwkJtUdLkNdR", + "key_secret":"mO2AIfdUQeQFiGQq", + "realm":"sandbox", + "service_id":"s-123", + "service_name":"_undefined_", + "consumer_org_name":"_undefined_", + "app_name":"_undefined_", + "plan_name":"_undefined_" + } +', "signature is valid\n" ] --- error_code_like eval @@ -232,8 +266,14 @@ __DATA__ [error] === TEST 5: test HMAC SHA1 validator with API KEY validation and custom ERROR MESSAGES + +--- main_config +env REDIS_PASS_API_KEY; +env REDIS_PASS_OAUTH; + --- http_config eval: $::HttpConfig --- config + error_log ../test-logs/hmacGenericSignatureValidator_test5_error.log debug; include ../../api-gateway/api_key_service.conf; include ../../api-gateway/default_validators.conf; # customize error response @@ -259,7 +299,7 @@ __DATA__ set $validate_api_key on; set $validate_hmac_signature on; - # set $validate_ims_oauth on; + # set $validate_oauth_oauth on; access_by_lua "ngx.apiGateway.validation.validateRequest()"; content_by_lua 'ngx.say("signature is valid")'; @@ -281,15 +321,15 @@ __DATA__ --- response_body eval [ '{ - "key":"sZ28nvYnStSUS2dSzedgnwkJtUdLkNdR", - "key_secret":"mO2AIfdUQeQFiGQq", - "realm":"sandbox", - "service_id":"s-123", - "service_name":"_undefined_", - "consumer_org_name":"_undefined_", - "app_name":"_undefined_", - "plan_name":"_undefined_" - } + "key":"sZ28nvYnStSUS2dSzedgnwkJtUdLkNdR", + "key_secret":"mO2AIfdUQeQFiGQq", + "realm":"sandbox", + "service_id":"s-123", + "service_name":"_undefined_", + "consumer_org_name":"_undefined_", + "app_name":"_undefined_", + "plan_name":"_undefined_" + } ', "signature is valid\n", 'while (1) {}{"code":1033,"description":"Developer key missing or invalid"}' . "\n", @@ -303,6 +343,11 @@ __DATA__ [error] === TEST 6: test HMAC signature validation and generation + +--- main_config +env REDIS_PASS_API_KEY; +env REDIS_PASS_OAUTH; + --- http_config eval: $::HttpConfig --- config error_log ../test-logs/hmacGenericSignatureValidator_test6_error.log debug; @@ -321,9 +366,9 @@ __DATA__ set $api_key $arg_api_key; set_if_empty $api_key $http_x_api_key; - + set_by_lua $hmac_source_string 'return string.lower(ngx.var.request_method .. ngx.var.uri .. ngx.var.api_key)'; - + set $hmac_target_string $arg_api_signature; set $hmac_method sha1; @@ -356,15 +401,15 @@ __DATA__ --- response_body eval [ '{ - "key":"sZ28nvYnStSUS2dSzedgnwkJtUdLkNdR", - "key_secret":"mO2AIfdUQeQFiGQq", - "realm":"sandbox", - "service_id":"123456", - "service_name":"_undefined_", - "consumer_org_name":"_undefined_", - "app_name":"_undefined_", - "plan_name":"_undefined_" - } + "key":"sZ28nvYnStSUS2dSzedgnwkJtUdLkNdR", + "key_secret":"mO2AIfdUQeQFiGQq", + "realm":"sandbox", + "service_id":"123456", + "service_name":"_undefined_", + "consumer_org_name":"_undefined_", + "app_name":"_undefined_", + "plan_name":"_undefined_" + } ', "5XPFapKr91/nLn3F+tzfkvSuE4A=\n", 'while (1) {}{"code":1033,"description":"Developer key missing or invalid"}' . "\n", @@ -378,6 +423,11 @@ __DATA__ [error] === TEST 7: test HMAC digest in isolation + +--- main_config +env REDIS_PASS_API_KEY; +env REDIS_PASS_OAUTH; + --- http_config eval: $::HttpConfig --- config error_log ../test-logs/hmacGenericSignatureValidator_test7_error.log debug; @@ -406,11 +456,11 @@ __DATA__ --- response_body eval [ "DYUCC7E/MCyn+aNcCb5EhM7OPDE=\n", -'{"error_code":"400002","message"="Missing digest secret"} +'{"error_code":"400002","message":"Missing digest secret"} ', -'{"error_code":"400001","message"="Missing digest source"} +'{"error_code":"400001","message":"Missing digest source"} ', -'{"error_code":"400001","message"="Missing digest source"} +'{"error_code":"400001","message":"Missing digest source"} ' ] --- error_code_like eval diff --git a/test/perl/api-gateway/validation/validator.t b/test/perl/api-gateway/validation/validator.t index ef7a9e7..379ce00 100644 --- a/test/perl/api-gateway/validation/validator.t +++ b/test/perl/api-gateway/validation/validator.t @@ -56,6 +56,13 @@ run_tests(); __DATA__ === TEST 1: test core validator initialization + +--- main_config +env REDIS_PASS_API_KEY; +env REDIS_PASS_OAUTH; +env REDIS_PASS; +env REDIS_PASSWORD; + --- http_config eval: $::HttpConfig --- config location /test-base-validator { @@ -81,6 +88,13 @@ GET /test-base-validator [error] === TEST 2: test core validator local caching + +--- main_config +env REDIS_PASS_API_KEY; +env REDIS_PASS; +env REDIS_PASS_OAUTH; +env REDIS_PASSWORD; + --- http_config eval: $::HttpConfig --- config location /post-local-cache { @@ -133,6 +147,13 @@ GET /test-base-validator [error] === TEST 3: test core validator with Redis caching + +--- main_config +env REDIS_PASS_API_KEY; +env REDIS_PASS; +env REDIS_PASS_OAUTH; +env REDIS_PASSWORD; + --- http_config eval: $::HttpConfig --- config include ../../api-gateway/api-gateway-cache.conf; @@ -186,6 +207,13 @@ GET /test-base-validator [error] === TEST 4: test setContextProperties with object + +--- main_config +env REDIS_PASS_API_KEY; +env REDIS_PASS; +env REDIS_PASS_OAUTH; +env REDIS_PASSWORD; + --- http_config eval: $::HttpConfig --- config set $prop1 'unset'; @@ -214,6 +242,13 @@ GET /test-base-validator [error] === TEST 5: test setContextProperties with string + +--- main_config +env REDIS_PASS_API_KEY; +env REDIS_PASS; +env REDIS_PASS_OAUTH; +env REDIS_PASSWORD; + --- http_config eval: $::HttpConfig --- config set $prop1 'unset'; @@ -235,4 +270,4 @@ GET /test-base-validator [".*prop1=val1,prop2=val2.*"] --- error_code: 201 --- no_error_log -[error] \ No newline at end of file +[error] diff --git a/test/perl/api-gateway/validation/validatorHandler.t b/test/perl/api-gateway/validation/validatorHandler.t index bc09685..e5cf91b 100644 --- a/test/perl/api-gateway/validation/validatorHandler.t +++ b/test/perl/api-gateway/validation/validatorHandler.t @@ -22,6 +22,8 @@ # */ # 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); @@ -31,7 +33,7 @@ use Cwd qw(cwd); repeat_each(2); -plan tests => repeat_each() * (blocks() * 9) + 2; +plan tests => repeat_each() * (blocks() * 10) - 16; my $pwd = cwd(); @@ -618,4 +620,73 @@ custom-header-2: this is a lua variable", --- no_error_log [error] +=== TEST 10: test validator handler can execute subrequests from order 2 and 3 (without order 1 declared) +--- http_config eval: $::HttpConfig +--- config + include ../../api-gateway/default_validators.conf; + + error_log ../test-logs/validatorhandler_test10_error.log debug; + + set $custom_prop1 "unset"; + set $temp_prop ''; + + location /validator_1 { + set $temp_prop $arg_k; + content_by_lua ' + ngx.ctx.custom_prop1 = ngx.var.temp_prop + ngx.header["Response-Time"] = ngx.now() - ngx.req.start_time() + ngx.say("OK") + '; + } + location /validator_2 { + set $temp_prop $arg_key; + content_by_lua ' + ngx.header["Response-Time"] = ngx.now() - ngx.req.start_time() + if ( ngx.ctx.custom_prop1 == ngx.var.temp_prop) then + ngx.say("OK") + ngx.exit(ngx.OK) + end + ngx.status = 401 + ngx.print("you called me too soon") + ngx.exit(ngx.OK) + '; + } + + location /validate-request-test { + set $custom_prop1 "unset"; + set $request_validator_1 "on; path=/validator_1?k=123; order=2;"; + set $request_validator_2 "on; path=/validator_2?key=123; order=3;"; + access_by_lua "ngx.apiGateway.validation.validateRequest()"; + content_by_lua ' + ngx.say("request is valid:" .. ngx.var.custom_prop1) + '; + } + location /validate-reverse-order { + set $custom_prop1 "unset"; + set $request_validator_1 "on; path=/validator_2?key=123; order=2;"; + set $request_validator_2 "on; path=/validator_1?k=123; order=3;"; + access_by_lua "ngx.apiGateway.validation.validateRequest()"; + content_by_lua ' + ngx.say("request is valid.") + '; + } +--- pipelined_requests eval +[ +"GET /validate-request-test?debug=true", +"GET /validate-reverse-order?debug=true" +] +--- response_body_like eval +["request is valid:\\d+.*", "^you called me too soon"] +--- response_headers_like eval +[ +"X-Debug-Validation-Response-Times: /validator_1\\?k=\\d+, \\d+ ms, status:200, request_validator \\[order:2\\], \\d+ ms, status:200, /validator_2\\?key=\\d+, \\d+ ms, status:200, request_validator \\[order:3\\], \\d+ ms, status:200 +Content-Type: text/plain", +"X-Debug-Validation-Response-Times: /validator_2\\?key=\\d+, \\d+ ms, status:401, request_validator \\[order:2\\], \\d+ ms, status:401 +Content-Type: text/plain" +] +--- no_error_log +[error] +--- error_code_like eval +[200,401] + diff --git a/test/resources/api-gateway/api_key_service.conf b/test/resources/api-gateway/api_key_service.conf index 328ccb2..ce2494d 100644 --- a/test/resources/api-gateway/api_key_service.conf +++ b/test/resources/api-gateway/api_key_service.conf @@ -33,132 +33,138 @@ # Sample query to list all keys; # curl http://localhost:9191/cache/redis_query?KEYS%20*cachedkey* -u user:password | grep cachedkey | awk -F ":" '{printf "%+ 32s %+ 20s \n",$2,$3}' | sort location /cache/redis_query { - # auth_basic "Redis Master"; - # auth_basic_user_file /path/to/htpasswd; - allow 127.0.0.1; - set_unescape_uri $query $query_string; - redis2_raw_query '$query\r\n'; - redis2_pass api-gateway-redis; + # auth_basic "Redis Master"; + # auth_basic_user_file /path/to/htpasswd; + allow 127.0.0.1; + set_unescape_uri $query $query_string; + redis2_raw_query '$query\r\n'; + redis2_pass api-gateway-redis; } location ~ /cache/api_key/set { - internal; - - limit_except POST OPTIONS { - deny all; - } - - set $key $arg_key; - set $key_secret $arg_secret; - set $realm $arg_realm; - set $service_id $arg_service_id; - set $service_name $arg_service_name; - set $consumer_org_name $arg_consumer_org_name; - set $app_name $arg_app_name; - set $plan_name $arg_plan_name; - - set_if_empty $key_secret '-'; - set_if_empty $realm sandbox; - set_if_empty $service_id _undefined_; - set_if_empty $service_name _undefined_; - set_if_empty $consumer_org_name _undefined_; - set_if_empty $app_name _undefined_; - set_if_empty $plan_name _undefined_; - - set $metadata '{ - "key":"$key", - "key_secret":"$key_secret", - "realm":"$realm", - "service_id":"$service_id", - "service_name":"$service_name", - "consumer_org_name":"$consumer_org_name", - "app_name":"$app_name", - "plan_name":"$plan_name" - }'; - - default_type application/json; - - content_by_lua ' - local BaseValidator = require "api-gateway.validation.validator" - local validator = BaseValidator:new() - validator:setKeyInRedis("cachedkey:" .. ngx.var.key .. ":" .. ngx.var.service_id, "metadata", nil, ngx.var.metadata) - ngx.say(ngx.var.metadata) - '; - - header_filter_by_lua ' - ngx.header.Description="Method used to add a new API-KEY into the cache. "; - ngx.header.Allowed_Query_Params="key, secret, realm, service_id, service_name, consumer_org_name, app_name, plan_name"; - ngx.header.Key_Description="Parameter representing the API KEY"; - ngx.header.Realm_Description="The environment where the api-key is applicable. It should only be \'sandbox\' or \'prod\'. Other values are saved but ignored."; - ngx.header.Sample_query="curl -i -X POST \'http://" .. ngx.var.host .. "/cache/api_key?key=k-123&service_id=s-123&realm=sandbox\'"; - ngx.header["Content-Type"] = "text/plain"; - '; + internal; + + limit_except POST OPTIONS { + deny all; + } + + set $key $arg_key; + set $key_secret $arg_secret; + set $realm $arg_realm; + set $service_id $arg_service_id; + set $service_name $arg_service_name; + set $consumer_org_name $arg_consumer_org_name; + set $app_name $arg_app_name; + set $plan_name $arg_plan_name; + + set_if_empty $key_secret '-'; + set_if_empty $realm sandbox; + set_if_empty $service_id _undefined_; + set_if_empty $service_name _undefined_; + set_if_empty $consumer_org_name _undefined_; + set_if_empty $app_name _undefined_; + set_if_empty $plan_name _undefined_; + + set $metadata '{ + "key":"$key", + "key_secret":"$key_secret", + "realm":"$realm", + "service_id":"$service_id", + "service_name":"$service_name", + "consumer_org_name":"$consumer_org_name", + "app_name":"$app_name", + "plan_name":"$plan_name" + }'; + + default_type application/json; + + content_by_lua ' + local BaseValidator = require "api-gateway.validation.validator" + local validator = BaseValidator:new() + validator:setKeyInRedis("cachedkey:" .. ngx.var.key .. ":" .. ngx.var.service_id, "metadata", nil, ngx.var.metadata) + ngx.say(ngx.var.metadata) + '; + + header_filter_by_lua ' + ngx.header.Description="Method used to add a new API-KEY into the cache. "; + ngx.header.Allowed_Query_Params="key, secret, realm, service_id, service_name, consumer_org_name, app_name, plan_name"; + ngx.header.Key_Description="Parameter representing the API KEY"; + ngx.header.Realm_Description="The environment where the api-key is applicable. It should only be \'sandbox\' or \'prod\'. Other values are saved but ignored."; + ngx.header.Sample_query="curl -i -X POST \'http://" .. ngx.var.host .. "/cache/api_key?key=k-123&service_id=s-123&realm=sandbox\'"; + ngx.header["Content-Type"] = "text/plain"; + '; } location ~ /cache/api_key/del { - limit_except DELETE { - deny all; - } + limit_except DELETE { + deny all; + } - set $key $arg_key; - set $service_id $arg_service_id; + set $key $arg_key; + set $service_id $arg_service_id; - set $redis_cmd "DEL cachedkey:$key:$service_id"; + set $redis_key "DEL cachedkey:$key:$service_id"; - # limit_except OPTIONS - proxy_pass http://127.0.0.1:9191/cache/redis_query?$redis_cmd; + # limit_except OPTIONS + # proxy_pass http://127.0.0.1:9191/cache/redis_query?$redis_cmd; + content_by_lua ' + local BaseValidator = require "api-gateway.validation.validator" + local validator = BaseValidator:new() + local response = validator:deleteKeyFromRedis(ngx.var.redis_key) + ngx.say("Delete key response: "..response) + '; - header_filter_by_lua ' - ngx.header.Description="Deletes a key associated to a service from the cache"; - ngx.header.Allowed_Query_Params="key, service_id"; - ngx.header.Sample_query="curl -i -X DELETE \'http://" .. ngx.var.host .. "/cache/api_key?key=k-123&service_id=s-123\'"; - ngx.header["Content-Type"] = "text/plain"; - '; + header_filter_by_lua ' + ngx.header.Description="Deletes a key associated to a service from the cache"; + ngx.header.Allowed_Query_Params="key, service_id"; + ngx.header.Sample_query="curl -i -X DELETE \'http://" .. ngx.var.host .. "/cache/api_key?key=k-123&service_id=s-123\'"; + ngx.header["Content-Type"] = "text/plain"; + '; } location ~ /cache/api_key/get { - # TODO: restrict outside access, - # but test first if features are not broken by turning this endpoint to internal - # internal; - - uninitialized_variable_warn off; - - limit_except GET OPTIONS { - deny all; - } - - set $api_key $arg_key; - set $service_id $arg_service_id; - - header_filter_by_lua ' - ngx.header.Description="Retrieves all the fields of the key associated to the service_id parameter."; - ngx.header.Allowed_Query_Params="key, service_id"; - ngx.header.Sample_query="curl -i \'http://" .. ngx.var.host .. "/cache/api_key?key=k-123&service_id=s-123\'"; - ngx.header["Content-Type"] = "application/json"; - if ( ngx.var.arg_debug == "true" ) then - local request_time = ngx.now() - ngx.req.start_time(); - ngx.header["ResponseTime-Api-Key-Get"] = request_time; - end; - '; - - content_by_lua 'ngx.apiGateway.validation.validateApiKey()'; + # TODO: restrict outside access, + # but test first if features are not broken by turning this endpoint to internal + # internal; + + uninitialized_variable_warn off; + + limit_except GET OPTIONS { + deny all; + } + + set $api_key $arg_key; + set $service_id $arg_service_id; + + header_filter_by_lua ' + ngx.header.Description="Retrieves all the fields of the key associated to the service_id parameter."; + ngx.header.Allowed_Query_Params="key, service_id"; + ngx.header.Sample_query="curl -i \'http://" .. ngx.var.host .. "/cache/api_key?key=k-123&service_id=s-123\'"; + ngx.header["Content-Type"] = "application/json"; + if ( ngx.var.arg_debug == "true" ) then + local request_time = ngx.now() - ngx.req.start_time(); + ngx.header["ResponseTime-Api-Key-Get"] = request_time; + end; + '; + + content_by_lua 'ngx.apiGateway.validation.validateApiKey()'; } # pure REST API URI where POST goes to /set, GET to /get, DELETE to /del through internal redirect location = /cache/api_key { - uninitialized_variable_warn off; + uninitialized_variable_warn off; - if ($request_method = POST) { - rewrite ^/cache/api_key(.*)$ /cache/api_key/set$1 last; - } + if ($request_method = POST) { + rewrite ^/cache/api_key(.*)$ /cache/api_key/set$1 last; + } - if ($request_method = DELETE) { - rewrite ^/cache/api_key(.*)$ /cache/api_key/del$1 last; - } + if ($request_method = DELETE) { + rewrite ^/cache/api_key(.*)$ /cache/api_key/del$1 last; + } - if ($request_method ~* ^(GET|OPTIONS)$ ) { - rewrite ^/cache/api_key(.*)$ /cache/api_key/get$1 last; - } + if ($request_method ~* ^(GET|OPTIONS)$ ) { + rewrite ^/cache/api_key(.*)$ /cache/api_key/get$1 last; + } } diff --git a/test/resources/api-gateway/api_key_service_deprecated.conf b/test/resources/api-gateway/api_key_service_deprecated.conf deleted file mode 100644 index 448b78b..0000000 --- a/test/resources/api-gateway/api_key_service_deprecated.conf +++ /dev/null @@ -1,151 +0,0 @@ -#/* -# * Copyright (c) 2015 Adobe Systems Incorporated. All rights reserved. -# * -# * Permission is hereby granted, free of charge, to any person obtaining a -# * copy of this software and associated documentation files (the "Software"), -# * to deal in the Software without restriction, including without limitation -# * the rights to use, copy, modify, merge, publish, distribute, sublicense, -# * and/or sell copies of the Software, and to permit persons to whom the -# * Software is furnished to do so, subject to the following conditions: -# * -# * The above copyright notice and this permission notice shall be included in -# * all copies or substantial portions of the Software. -# * -# * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# * DEALINGS IN THE SOFTWARE. -# * -# */ -# This is a sample file containing a basic API for Managing API-KEYs with Redis - -# -# ---------------------- -# Elasti Cache Proxy -# for api-key management -# ---------------------- -# - -# sample query: curl -i http://localhost:9191/cache/redis_query?KEYS%20*cachedkey* -u user:password -# Sample query to list all keys; -# curl http://localhost:9191/cache/redis_query?KEYS%20*cachedkey* -u user:password | grep cachedkey | awk -F ":" '{printf "%+ 32s %+ 20s \n",$2,$3}' | sort -location /cache/redis_query { - # auth_basic "Redis Master"; - # auth_basic_user_file /path/to/htpasswd; - allow 127.0.0.1; - set_unescape_uri $query $query_string; - redis2_raw_query '$query\r\n'; - redis2_pass api-gateway-redis; -} - -location ~ /cache/api_key/set { - internal; - - limit_except POST OPTIONS { - deny all; - } - - set $key $arg_key; - set $key_secret $arg_secret; - set $realm $arg_realm; - set $service_id $arg_service_id; - set $service_name $arg_service_name; - set $consumer_org_name $arg_consumer_org_name; - set $app_name $arg_app_name; - set $plan_name $arg_plan_name; - - set_if_empty $key_secret '-'; - set_if_empty $realm sandbox; - set_if_empty $service_id _undefined_; - set_if_empty $service_name _undefined_; - set_if_empty $consumer_org_name _undefined_; - set_if_empty $app_name _undefined_; - set_if_empty $plan_name _undefined_; - - set $redis_cmd "HMSET cachedkey:$key:$service_id key_secret $key_secret service-id $service_id service-name $service_name realm $realm consumer-org-name $consumer_org_name app-name $app_name plan-name $plan_name"; - - proxy_pass http://127.0.0.1:$server_port/cache/redis_query?$redis_cmd; - - header_filter_by_lua ' - ngx.header.Description="Method used to add a new API-KEY into the cache. "; - ngx.header.Allowed_Query_Params="key, secret, realm, service_id, service_name, consumer_org_name, app_name, plan_name"; - ngx.header.Key_Description="Parameter representing the API KEY"; - ngx.header.Realm_Description="The environment where the api-key is applicable. It should only be \'sandbox\' or \'prod\'. Other values are saved but ignored."; - ngx.header.Sample_query="curl -i -X POST \'http://" .. ngx.var.host .. "/cache/api_key?key=k-123&service_id=s-123&realm=sandbox\'"; - ngx.header["Content-Type"] = "text/plain"; - '; -} - -location ~ /cache/api_key/del { - - limit_except DELETE { - deny all; - } - - set $key $arg_key; - set $service_id $arg_service_id; - - set $redis_cmd "DEL cachedkey:$key:$service_id"; - - # limit_except OPTIONS - proxy_pass http://127.0.0.1:9191/cache/redis_query?$redis_cmd; - - header_filter_by_lua ' - ngx.header.Description="Deletes a key associated to a service from the cache"; - ngx.header.Allowed_Query_Params="key, service_id"; - ngx.header.Sample_query="curl -i -X DELETE \'http://" .. ngx.var.host .. "/cache/api_key?key=k-123&service_id=s-123\'"; - ngx.header["Content-Type"] = "text/plain"; - '; - -} - -location ~ /cache/api_key/get { - # TODO: restrict outside access, - # but test first if features are not broken by turning this endpoint to internal - # internal; - - uninitialized_variable_warn off; - - limit_except GET OPTIONS { - deny all; - } - - set $api_key $arg_key; - set $service_id $arg_service_id; - - # set $redis_cmd "HMGET cachedkey:$key:$service_id service-id service-name realm consumer-org-name app-name plan-name"; - # proxy_pass http://127.0.0.1:9191/cache/redis_query?$redis_cmd; - - header_filter_by_lua ' - ngx.header.Description="Retrieves all the fields of the key associated to the service_id parameter."; - ngx.header.Allowed_Query_Params="key, service_id"; - ngx.header.Sample_query="curl -i \'http://" .. ngx.var.host .. "/cache/api_key?key=k-123&service_id=s-123\'"; - ngx.header["Content-Type"] = "application/json"; - if ( ngx.var.arg_debug == "true" ) then - local request_time = ngx.now() - ngx.req.start_time(); - ngx.header["ResponseTime-Api-Key-Get"] = request_time; - end; - '; - - content_by_lua 'ngx.apiGateway.validation.validateApiKey()'; -} - -# pure REST API URI where POST goes to /set, GET to /get, DELETE to /del through internal redirect -location = /cache/api_key { - uninitialized_variable_warn off; - - if ($request_method = POST) { - rewrite ^/cache/api_key(.*)$ /cache/api_key/set$1 last; - } - - if ($request_method = DELETE) { - rewrite ^/cache/api_key(.*)$ /cache/api_key/del$1 last; - } - - if ($request_method ~* ^(GET|OPTIONS)$ ) { - rewrite ^/cache/api_key(.*)$ /cache/api_key/get$1 last; - } -} diff --git a/test/resources/api-gateway/default_validators.conf b/test/resources/api-gateway/default_validators.conf index 0bd4e80..8e131ed 100644 --- a/test/resources/api-gateway/default_validators.conf +++ b/test/resources/api-gateway/default_validators.conf @@ -26,6 +26,7 @@ set $validate_request_response_body 'na'; set $validate_request_response_time -1; # where to redirect to send an error response set $validation_error_page '@handle_gateway_validation_error'; +set_if_empty $request_validator_error_code ''; # the next vars are automatically set from the api_key values # here they are initialized just so that they can be set later on by api_key_validator.lua @@ -39,6 +40,7 @@ set $consumer_org_name TBD; set $app_name TBD; set $plan_name TBD; set $service_env 'sandbox'; +set $hashing_algorithm TBD; # # default request validation impl diff --git a/test/resources/api-gateway/redis-upstream.conf b/test/resources/api-gateway/redis-upstream.conf index 7ab9d62..2f096d3 100644 --- a/test/resources/api-gateway/redis-upstream.conf +++ b/test/resources/api-gateway/redis-upstream.conf @@ -20,9 +20,16 @@ # * DEALINGS IN THE SOFTWARE. # * # */ + upstream api-gateway-redis { server 127.0.0.1:6379; } upstream api-gateway-redis-replica { server 127.0.0.1:6379; +} +upstream oauth-redis-ro-upstream { + server 127.0.0.1:6379; +} +upstream oauth-redis-rw-upstream { + server 127.0.0.1:6379; } \ No newline at end of file diff --git a/test/scripts/start-redis.sh b/test/scripts/start-redis.sh new file mode 100755 index 0000000..2e83c2b --- /dev/null +++ b/test/scripts/start-redis.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +echo "Checking for Redis authentication" +readonly redis_password=$1 + +if [ -z ${redis_password} ] +then + echo "Redis is not password protected" + redis-server +else + echo "Redis is password protected" + redis-server --requirepass ${redis_password} +fi diff --git a/test/unit-tests/api-gateway/redis/redisHealthCheckTest.lua b/test/unit-tests/api-gateway/redis/redisHealthCheckTest.lua new file mode 100644 index 0000000..a2d156b --- /dev/null +++ b/test/unit-tests/api-gateway/redis/redisHealthCheckTest.lua @@ -0,0 +1,354 @@ +--- +--- Generated by EmmyLua(https://github.com/EmmyLua) +--- Created by vdatcu. +--- DateTime: 05/07/2018 11:04 +--- + +local CLASS_UNDER_TEST = 'api-gateway.redis.redisHealthCheck' + +local ngxUpstreamMock, ngxSocketMock, shared + +beforeEach(function() + ngxUpstreamMock = mock('ngx.upstream', { 'get_primary_peers', 'get_backup_peers' }) + ngxSocketMock = mock('ngx.socket.tcp', { 'connect', 'receive', 'send', 'close', 'settimeout' }) + ngx.socket = { + tcp = function() + return ngxSocketMock + end + } + + shared = mock("ngx.shared", { "safe_set", "delete", "get" }) + ngx.shared = { + cachedOauthTokens = shared + } +end) + +test('Successful flow with no password, should return one healthy host', function() + local classUnderTest = require(CLASS_UNDER_TEST):new() + ngxUpstreamMock.__get_primary_peers.doReturn = function() + local primaryPeers = {} + table.insert(primaryPeers, { name = "127.0.0.1:6379" }) + return primaryPeers, nil + end + + ngxUpstreamMock.__get_backup_peers.doReturn = function() + return {}, nil + end + + ngxSocketMock.__connect.doReturn = function() + return true, nil + end + + ngxSocketMock.__settimeout.doReturn = function() + return true + end + + ngxSocketMock.__send.doReturn = function(self, message) + + if string.match(message, 'AUTH') then + ngxSocketMock.__receive.doReturn = function() + return 'OK', nil, nil + end + end + + if string.match(message, 'PING') then + ngxSocketMock.__receive.doReturn = function() + return 'PONG', nil, nil + end + end + + return 0, nil + end + + local healthyHost, host, port = classUnderTest:getHealthyRedisNode('api-gateway-read-replica', '') + assertNotNil(healthyHost) + assertNotNil(host) + assertNotNil(port) + assertEquals('127.0.0.1', host) + assertEquals('6379', tostring(port)) +end) + +test('Successful flow with password, should return one healthy host', function() + local classUnderTest = require(CLASS_UNDER_TEST):new() + ngxUpstreamMock.__get_primary_peers.doReturn = function() + local primaryPeers = {} + table.insert(primaryPeers, { name = "127.0.0.1:6379" }) + return primaryPeers, nil + end + + ngxUpstreamMock.__get_backup_peers.doReturn = function() + return {}, nil + end + + ngxSocketMock.__connect.doReturn = function() + return true, nil + end + + ngxSocketMock.__settimeout.doReturn = function() + return true + end + + ngxSocketMock.__send.doReturn = function(self, message) + + if string.match(message, 'AUTH') then + ngxSocketMock.__receive.doReturn = function() + return 'OK', nil, nil + end + end + + if string.match(message, 'PING') then + ngxSocketMock.__receive.doReturn = function() + return 'PONG', nil, nil + end + end + + return 0, nil + end + + local healthyHost, host, port = classUnderTest:getHealthyRedisNode('api-gateway-read-replica', 'password') + assertNotNil(healthyHost) + assertNotNil(host) + assertNotNil(port) + assertEquals('127.0.0.1', host) + assertEquals('6379', tostring(port)) +end) + +test('Faulty flow with wrong password, should not return any host', function() + ngx.var["enable_redis_advanced_healthcheck"] = "true" + local classUnderTest = require(CLASS_UNDER_TEST):new() + ngxUpstreamMock.__get_primary_peers.doReturn = function() + local primaryPeers = {} + table.insert(primaryPeers, { name = "127.0.0.1:6379" }) + return primaryPeers, nil + end + + ngxUpstreamMock.__get_backup_peers.doReturn = function() + return {}, nil + end + + ngxSocketMock.__connect.doReturn = function() + return true, nil + end + + ngxSocketMock.__settimeout.doReturn = function() + return true + end + + ngxSocketMock.__send.doReturn = function(self, message) + + if string.match(message, 'AUTH') then + ngxSocketMock.__receive.doReturn = function() + return 'ERROR', 'ERROR', nil + end + end + + if string.match(message, 'PING') then + ngxSocketMock.__receive.doReturn = function() + return 'PONG', nil, nil + end + end + + return 0, nil + end + + local healthyHost, host, port = classUnderTest:getHealthyRedisNode('api-gateway-read-replica', 'password') + assertNil(healthyHost) + assertNil(host) + assertNil(port) +end) + +test('Backup peers successful flow with password, should return one healthy host', function() + local classUnderTest = require(CLASS_UNDER_TEST):new() + ngxUpstreamMock.__get_primary_peers.doReturn = function() + return {}, nil + end + + ngxUpstreamMock.__get_backup_peers.doReturn = function() + local secondaryPeers = {} + table.insert(secondaryPeers, { name = "127.0.0.1:6379" }) + return secondaryPeers, nil + end + + ngxSocketMock.__connect.doReturn = function() + return true, nil + end + + ngxSocketMock.__settimeout.doReturn = function() + return true + end + + ngxSocketMock.__send.doReturn = function(self, message) + + if string.match(message, 'AUTH') then + ngxSocketMock.__receive.doReturn = function() + return 'OK', nil, nil + end + end + + if string.match(message, 'PING') then + ngxSocketMock.__receive.doReturn = function() + return 'PONG', nil, nil + end + end + + return 0, nil + end + + local healthyHost, host, port = classUnderTest:getHealthyRedisNode('api-gateway-read-replica', 'password') + assertNotNil(healthyHost) + assertNotNil(host) + assertNotNil(port) + assertEquals('127.0.0.1', host) + assertEquals('6379', tostring(port)) +end) + +test('Multiple peers successful flow with password, should return first healthy host', function() + ngx.var["enable_redis_advanced_healthcheck"] = "true" + local classUnderTest = require(CLASS_UNDER_TEST):new() + + local primaryPeers = { + { + name = "127.0.0.2:7000" + }, + { + name = "127.0.0.1.6379" + } + } + ngxUpstreamMock.__get_primary_peers.doReturn = function() + + return primaryPeers, nil + end + + ngxUpstreamMock.__get_backup_peers.doReturn = function() + return {}, nil + end + + ngxSocketMock.__connect.doReturn = function() + return true, nil + end + + ngxSocketMock.__settimeout.doReturn = function() + return true + end + + ngxSocketMock.__send.doReturn = function(self, message) + + if string.match(message, 'AUTH') then + ngxSocketMock.__receive.doReturn = function() + return 'OK', nil, nil + end + end + + if string.match(message, 'PING') then + ngxSocketMock.__receive.doReturn = function() + return 'PONG', nil, nil + end + end + + return 0, nil + end + + local healthyHost, host, port = classUnderTest:getHealthyRedisNode('api-gateway-read-replica', 'password') + assertNotNil(healthyHost) + assertNotNil(host) + assertNotNil(port) + assertEquals(primaryPeers[1].name, healthyHost) + assertEquals('127.0.0.2', host) + assertEquals('7000', tostring(port)) +end) + +test('No tcp connection should fail', function() + ngx.var["enable_redis_advanced_healthcheck"] = "true" + local classUnderTest = require(CLASS_UNDER_TEST):new() + + local primaryPeers = { + { + name = "127.0.0.2:7000" + }, + { + name = "127.0.0.1.6379" + } + } + ngxUpstreamMock.__get_primary_peers.doReturn = function() + + return primaryPeers, nil + end + + ngxUpstreamMock.__get_backup_peers.doReturn = function() + return {}, nil + end + + ngxSocketMock.__connect.doReturn = function() + return false, {} + end + + ngxSocketMock.__settimeout.doReturn = function() + return true + end + + ngxSocketMock.__send.doReturn = function(self, message) + + if string.match(message, 'AUTH') then + ngxSocketMock.__receive.doReturn = function() + return 'OK', nil, nil + end + end + + if string.match(message, 'PING') then + ngxSocketMock.__receive.doReturn = function() + return 'PONG', nil, nil + end + end + + return 0, nil + end + + local healthyHost, host, port = classUnderTest:getHealthyRedisNode('api-gateway-read-replica', 'password') + assertNil(healthyHost) + assertNil(host) + assertNil(port) +end) + +test('Primary and backup peers error should fail', function() + local classUnderTest = require(CLASS_UNDER_TEST):new() + + ngxUpstreamMock.__get_primary_peers.doReturn = function() + + return nil, 'ERROR' + end + + ngxUpstreamMock.__get_backup_peers.doReturn = function() + return nil, 'ERROR' + end + + ngxSocketMock.__connect.doReturn = function() + return false, {} + end + + ngxSocketMock.__settimeout.doReturn = function() + return true + end + + ngxSocketMock.__send.doReturn = function(self, message) + + if string.match(message, 'AUTH') then + ngxSocketMock.__receive.doReturn = function() + return 'OK', nil, nil + end + end + + if string.match(message, 'PING') then + ngxSocketMock.__receive.doReturn = function() + return 'PONG', nil, nil + end + end + + return 0, nil + end + + local healthyHost, host, port = classUnderTest:getHealthyRedisNode('api-gateway-read-replica', 'password') + assertNil(healthyHost) + assertNil(host) + assertNil(port) +end) + diff --git a/test/unit-tests/api-gateway/util/hasherTest.lua b/test/unit-tests/api-gateway/util/hasherTest.lua new file mode 100644 index 0000000..e64a706 --- /dev/null +++ b/test/unit-tests/api-gateway/util/hasherTest.lua @@ -0,0 +1,72 @@ +--- +--- Generated by EmmyLua(https://github.com/EmmyLua) +--- Created by trifan. +--- DateTime: 11/04/2018 10:13 +--- +local restyStringMock, sha256Mock, sha224Mock, sha512Mock, sha384Mock + +beforeEach(function() + restyStringMock = mock("resty.string", {"to_hex"}) + sha256Mock = mock("resty.sha256", {"new", "update", "final"}) + sha224Mock = mock("resty.sha224", {"new", "update", "final"}) + sha512Mock = mock("resty.sha512", {"new", "update", "final"}) + sha384Mock = mock("resty.sha384", {"new", "update", "final"}) + + ngx.var.hashing_algorithm = nil + when(sha256Mock).final.fake(function(self, str) + return "sha256" + end) + + when(sha224Mock).final.fake(function(self, str) + return "sha224" + end) + + when(sha512Mock).final.fake(function(self, str) + return "sha512" + end) + + when(sha384Mock).final.fake(function(self, str) + return "sha384" + end) + + when(restyStringMock).to_hex.fake(function(digest) + return digest + end) +end) + + +test('missing algorithm should fall to sha256', function() + local classUnderTest = require("api-gateway.util.hasher") + local result = classUnderTest.hash("test") + assertEquals(result, "sha256") +end) + +test('wrong algorithm should fall to sha256', function() + ngx.var.hashing_algorithm = "sha1101" + local classUnderTest = require("api-gateway.util.hasher") + local result = classUnderTest.hash("test") + assertEquals(result, "sha256") +end) + +test('correct algorithm should require the desired alg', function() + local classUnderTest = require("api-gateway.util.hasher") + + ngx.var.hashing_algorithm = "sha256" + local result = classUnderTest.hash("test") + assertEquals(result, "sha256") + + + ngx.var.hashing_algorithm = "sha512" + result = classUnderTest.hash("test") + assertEquals(result, "sha512") + + + ngx.var.hashing_algorithm = "sha224" + result = classUnderTest.hash("test") + assertEquals(result, "sha224") + + + ngx.var.hashing_algorithm = "sha384" + result = classUnderTest.hash("test") + assertEquals(result, "sha384") +end) diff --git a/test/unit-tests/api-gateway/validation/oauth2/oauthTokenValidatorTest.lua b/test/unit-tests/api-gateway/validation/oauth2/oauthTokenValidatorTest.lua new file mode 100644 index 0000000..b1a652b --- /dev/null +++ b/test/unit-tests/api-gateway/validation/oauth2/oauthTokenValidatorTest.lua @@ -0,0 +1,148 @@ +-- Copyright (c) 2018 Adobe Systems Incorporated. All rights reserved. +-- +-- Permission is hereby granted, free of charge, to any person obtaining a +-- copy of this software and associated documentation files (the "Software"), +-- to deal in the Software without restriction, including without limitation +-- the rights to use, copy, modify, merge, publish, distribute, sublicense, +-- and/or sell copies of the Software, and to permit persons to whom the +-- Software is furnished to do so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in +-- all copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +-- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +-- DEALINGS IN THE SOFTWARE. + +local safeCjson, redisMock, shared, RedisConnectionProviderMock, + sha256Mock, hasherMock + +local RESPONSES = { + INVALID_CLIENT_ID = { error_code = "403201", message = "Client ID not allowed to call this service" }, + MISSING_TOKEN = { error_code = "403010", message = "Oauth token is missing" }, + INVALID_TOKEN = { error_code = "401013", message = "Oauth token is not valid" }, + -- TOKEN_MISSMATCH is reserved for classes overwriting the isTokenValid method + TOKEN_MISSMATCH = { error_code = "401014", message = "Token not allowed in the current context" }, + SCOPE_MISMATCH = { error_code = "401015", message = "Scope mismatch" }, + UNKNOWN_ERROR = { error_code = "503010", message = "Could not validate the oauth token" } +} + +beforeEach(function() + + safeCjson = require "cjson.safe" + redisMock = mock("resty.redis", {"new"}) + shared = mock("ngx.shared", {"safe_set", "delete", "get"}) + RedisConnectionProviderMock = mock("api-gateway.redis.redisConnectionProvider", { + "new", "getConnection", "closeConnection" + }) + sha256Mock = mock("resty.sha256", {"new", "update", "final"}) + hasherMock = mock("api-gateway.util.hasher", {"hash"}) + + ngx.header = {} + + ngx.config = { + debug = false + } + + ngx.__time.doReturn = function() + return 123 + end + + ngx.shared = { + cachedOauthTokens = shared + } + + ngx.__now.doReturn = function() + return os.time() + end + + ngx.req.__start_time.doReturn = function() + return os.time() - 10 + end + + hasherMock.__hash.doReturn = function(self) + return "hashedToken" + end + + ngx.var.oauth_host = "oauth-na1.adobelogin.com" +end) + +test('checkResponseFromAuth: should return false for an invalid json', function() + local classUnderTest = require('api-gateway.validation.oauth2.oauthTokenValidator'):new() + local tokenValidity, error = classUnderTest:checkResponseFromAuth("invalid_json", "key") + assertEquals(tokenValidity, false) +end) + +test('checkResponseFromAuth: should return true for a valid json', function() + local classUnderTest = require('api-gateway.validation.oauth2.oauthTokenValidator'):new() + local token = { + id = "1234", scope = "openid email profile", user_id = "21961FF44F97F8A10A490D36", expires_in = "86400000", client_id = "client_id_test_2", type = "access_token" + } + + local body = { + valid = true, expires_at = 3400, token = safeCjson.encode(token) + } + + local testJson = { + status = 200, + body = safeCjson.encode(body) + } + + local tokenValidity = classUnderTest:checkResponseFromAuth(testJson, "key") + assertEquals(tokenValidity, true) +end) + +test('validateOauthToken: should return 200 for a valid json', function() + ngx.var.authtoken = "token" + + local tokenInfo = { + access_token = "token", + expires_in = 210, + oauth_token_client_id = 'client_id', + oauth_token_scope = 'system' + } + + shared.__get.doReturn = function(self, key) + return safeCjson.encode(tokenInfo) + end + + ngx.location.__capture.doReturn = function(self, location, args) + return { + status = 200, + body = safeCjson.encode(tokenInfo) + } + end + + local classUnderTest = require('api-gateway.validation.oauth2.oauthTokenValidator'):new() + local response_code, response_body = classUnderTest:validateOAuthToken() + + assertEquals(response_code, ngx.HTTP_OK) +end) + +test('validateOauthToken: should return 401 for an invalid json', function() + ngx.var.authtoken = "bad_token" + + local tokenInfo = "invalid_json" + + shared.__get.doReturn = function(self, key) + return tokenInfo + end + + ngx.location.__capture.doReturn = function(self, location, args) + return { + status = 200, + body = tokenInfo + } + end + + local classUnderTest = require('api-gateway.validation.oauth2.oauthTokenValidator'):new() + local responseCode, responseBody = classUnderTest:validateOAuthToken() + + assertEquals(responseCode, RESPONSES.INVALID_TOKEN.error_code) + assertEquals(RESPONSES.INVALID_TOKEN.message, string.match(responseBody, RESPONSES.INVALID_TOKEN.message)) +end) + diff --git a/test/unit-tests/api-gateway/validation/oauth2/userProfileValidatorTest.lua b/test/unit-tests/api-gateway/validation/oauth2/userProfileValidatorTest.lua new file mode 100644 index 0000000..3d27cb6 --- /dev/null +++ b/test/unit-tests/api-gateway/validation/oauth2/userProfileValidatorTest.lua @@ -0,0 +1,14 @@ +--- +--- Generated by EmmyLua(https://github.com/EmmyLua) +--- Created by trifan. +--- DateTime: 03/07/2018 15:18 +--- + +beforeEach(function() + ngx.config = {} + mock("resty.redis", {"new"}) + mock("resty.string", {"new"}) +end) +test('empty test', function() + require "api-gateway.validation.oauth2.userProfileValidator" +end) \ No newline at end of file diff --git a/test/unit-tests/api-gateway/validation/redisApiKeyValidatorTest.lua b/test/unit-tests/api-gateway/validation/redisApiKeyValidatorTest.lua new file mode 100644 index 0000000..0251218 --- /dev/null +++ b/test/unit-tests/api-gateway/validation/redisApiKeyValidatorTest.lua @@ -0,0 +1,303 @@ +--- +--- Created by purcarea. +--- DateTime: 30/03/2018 +--- + +local cjson = require "cjson" + +local BaseValidatorMock, RedisConnectionProviderMock + +local EXPECTED_RESPONSES = { + MISSING_KEY = { error_code = "403000", message = '{"message":"Api KEY is missing","error_code":"403000"}' }, + UNKNOWN_ERROR = { error_code = "503000", message = '{"message":"Could not validate API KEY","error_code":"503000"}' }, + INVALID_KEY = { error_code = "403003", message = '{"message":"Api KEY is invalid","error_code":"403003"}' } +} + +beforeEach(function() + BaseValidatorMock = mock("api-gateway.validation.validator", { + "new", "exitFn", "getKeyFromLocalCache", "setContextProperties", + "getKeyFromRedis", "setKeyInLocalCache" + }) + RedisConnectionProviderMock = mock("api-gateway.redis.redisConnectionProvider", { + "new", "getConnection", "closeConnection" + }) + ngx.HTTP_SERVICE_UNAVAILABLE = 503 + ngx.HTTP_NOT_FOUND = 404 + + BaseValidatorMock.__exitFn.doReturn = function(self, arg1, arg2) + return arg1, arg2 + end +end) + +test('validateRequest: should return 403000 if api key is missing', function() + -- given + ngx.var.api_key = nil + ngx.var.service_id = "test-service" + + -- when + local classUnderTest = require('api-gateway.validation.key.redisApiKeyValidator'):new() + local error_code, response_body = classUnderTest:validateRequest() + + -- then + assertEquals(error_code, EXPECTED_RESPONSES.MISSING_KEY.error_code) + assertEquals(response_body, EXPECTED_RESPONSES.MISSING_KEY.message) + + calls(BaseValidatorMock.__exitFn, 1, EXPECTED_RESPONSES.MISSING_KEY.error_code, EXPECTED_RESPONSES.MISSING_KEY.message) +end) + +test('validateRequest: should return OK and skip searching in redis if api key is in local cache', function() + -- given + ngx.var.api_key = "test-api-key" + ngx.var.service_id = "test-service" + + BaseValidatorMock.__getKeyFromLocalCache.doReturn = function() + return '{"key": "test-api-key", "realm": "sandbox", "service_id": "test-service", "service_name": "test-service-name"}' + end + + -- when + local classUnderTest = require('api-gateway.validation.key.redisApiKeyValidator'):new() + local response_code, response_body = classUnderTest:validateRequest() + + -- then + assertEquals(ngx.HTTP_OK, response_code) + assertEquals('{"valid":true}', response_body) + + calls(BaseValidatorMock.__exitFn, 1, ngx.HTTP_OK, '{"valid":true}') + calls(BaseValidatorMock.__getKeyFromLocalCache, 1, "test-api-key:test-service", "cachedkeys") + calls(BaseValidatorMock.__getKeyFromRedis, 0) + + local expected_api_key_object = cjson.decode('{"key": "test-api-key", "realm": "sandbox", "service_id": "test-service", "service_name": "test-service-name"}') + calls(BaseValidatorMock.__setContextProperties, 1, expected_api_key_object) + +end) + +test('validateRequest: should return OK and search in redis if api key is not in local cache and redis hash has metadata field', function() + -- given + ngx.var.api_key = "test-api-key" + ngx.var.service_id = "test-service" + + BaseValidatorMock.__getKeyFromLocalCache.doReturn = function() + return nil + end + + BaseValidatorMock.__getKeyFromRedis.doReturn = function() + return '{"key": "test-api-key", "realm": "sandbox", "service_id": "test-service", "service_name": "test-service-name"}' + end + + -- when + local classUnderTest = require('api-gateway.validation.key.redisApiKeyValidator'):new() + local response_code, response_body = classUnderTest:validateRequest() + + -- then + assertEquals(response_code, ngx.HTTP_OK) + assertEquals(response_body, '{"valid":true}') + + calls(BaseValidatorMock.__exitFn, 1, ngx.HTTP_OK, '{"valid":true}') + calls(BaseValidatorMock.__getKeyFromLocalCache, 1, "test-api-key:test-service", "cachedkeys") + calls(BaseValidatorMock.__setKeyInLocalCache, 1) + calls(BaseValidatorMock.__getKeyFromRedis, 1) + + local expected_api_key_object = cjson.decode('{"key": "test-api-key", "realm": "sandbox", "service_id": "test-service", "service_name": "test-service-name"}') + calls(BaseValidatorMock.__setContextProperties, 1, expected_api_key_object) +end) + +test('validateRequest: should return OK and search in redis for the old format if api key is not in local cache and redis hash does not have metadata field', function() + -- given + ngx.var.api_key = "test-api-key" + ngx.var.service_id = "test-service" + + BaseValidatorMock.__getKeyFromLocalCache.doReturn = function() + return nil + end + + BaseValidatorMock.__getKeyFromRedis.doReturn = function() + return nil + end + + RedisConnectionProviderMock.__getConnection.doReturn = function() + local redis = {} + redis.hmget = function (redis_key, ...) + return {"test-api-key", "sandbox", "test-service", "test-service-name"}, nil + end + + return true, redis + end + + -- when + local classUnderTest = require('api-gateway.validation.key.redisApiKeyValidator'):new() + local response_code, response_body = classUnderTest:validateRequest() + + -- then + assertEquals(response_code, ngx.HTTP_OK) + assertEquals(response_body, '{"valid":true}') + + calls(BaseValidatorMock.__exitFn, 1, ngx.HTTP_OK, '{"valid":true}') + calls(BaseValidatorMock.__getKeyFromLocalCache, 1, "test-api-key:test-service", "cachedkeys") + calls(BaseValidatorMock.__setKeyInLocalCache, 1) + calls(BaseValidatorMock.__getKeyFromRedis, 1) + calls(RedisConnectionProviderMock.__getConnection, 1) + calls(RedisConnectionProviderMock.__closeConnection, 1) + + local expected_api_key_object = cjson.decode('{"key": "test-api-key", "realm": "sandbox", "service_id": "test-service", "service_name": "test-service-name"}') + calls(BaseValidatorMock.__setContextProperties, 1, expected_api_key_object) +end) + +test('validateRequest: should return 503000 if api key is not in local cache and connecting to redis fails', function() + -- given + ngx.var.api_key = "test-api-key" + ngx.var.service_id = "test-service" + + BaseValidatorMock.__getKeyFromLocalCache.doReturn = function() + return nil + end + + BaseValidatorMock.__getKeyFromRedis.doReturn = function() + return nil + end + + RedisConnectionProviderMock.__getConnection.doReturn = function() + local redis = {} + redis.hmget = function (redis_key, ...) + return {"test-api-key", "sandbox", "test-service", "test-service-name"}, nil + end + + return false, redis + end + + -- when + local classUnderTest = require('api-gateway.validation.key.redisApiKeyValidator'):new() + local response_code, response_body = classUnderTest:validateRequest() + + -- then + assertEquals(response_code, EXPECTED_RESPONSES.UNKNOWN_ERROR.error_code) + assertEquals(response_body, EXPECTED_RESPONSES.UNKNOWN_ERROR.message) + + calls(BaseValidatorMock.__exitFn, 1, EXPECTED_RESPONSES.UNKNOWN_ERROR.error_code, EXPECTED_RESPONSES.UNKNOWN_ERROR.message) + calls(BaseValidatorMock.__getKeyFromLocalCache, 1, "test-api-key:test-service", "cachedkeys") + calls(BaseValidatorMock.__getKeyFromRedis, 1) + calls(RedisConnectionProviderMock.__getConnection, 1) + + calls(BaseValidatorMock.__setKeyInLocalCache, 0) + calls(RedisConnectionProviderMock.__closeConnection, 0) + calls(BaseValidatorMock.__setContextProperties, 0) +end) + +test('validateRequest: should return 503000 if api key is not in local cache and redis operation returns an error', function() + -- given + ngx.var.api_key = "test-api-key" + ngx.var.service_id = "test-service" + + BaseValidatorMock.__getKeyFromLocalCache.doReturn = function() + return nil + end + + BaseValidatorMock.__getKeyFromRedis.doReturn = function() + return nil + end + + RedisConnectionProviderMock.__getConnection.doReturn = function() + local redis = {} + redis.hmget = function (redis_key, ...) + return nil, "error" + end + + return true, redis + end + + -- when + local classUnderTest = require('api-gateway.validation.key.redisApiKeyValidator'):new() + local response_code, response_body = classUnderTest:validateRequest() + + -- then + assertEquals(response_code, EXPECTED_RESPONSES.UNKNOWN_ERROR.error_code) + assertEquals(response_body, EXPECTED_RESPONSES.UNKNOWN_ERROR.message) + + calls(BaseValidatorMock.__exitFn, 1, EXPECTED_RESPONSES.UNKNOWN_ERROR.error_code, EXPECTED_RESPONSES.UNKNOWN_ERROR.message) + calls(BaseValidatorMock.__getKeyFromLocalCache, 1, "test-api-key:test-service", "cachedkeys") + calls(BaseValidatorMock.__getKeyFromRedis, 1) + calls(RedisConnectionProviderMock.__getConnection, 1) + + calls(BaseValidatorMock.__setKeyInLocalCache, 0) + calls(RedisConnectionProviderMock.__closeConnection, 1) + calls(BaseValidatorMock.__setContextProperties, 0) +end) + +test('validateRequest: should return 403003 if api key is not in local cache and redis operation returns a nil value', function() + -- given + ngx.var.api_key = "test-api-key" + ngx.var.service_id = "test-service" + + BaseValidatorMock.__getKeyFromLocalCache.doReturn = function() + return nil + end + + BaseValidatorMock.__getKeyFromRedis.doReturn = function() + return nil + end + + RedisConnectionProviderMock.__getConnection.doReturn = function() + local redis = {} + redis.hmget = function (redis_key, ...) + return nil, nil + end + + return true, redis + end + + -- when + local classUnderTest = require('api-gateway.validation.key.redisApiKeyValidator'):new() + local response_code, response_body = classUnderTest:validateRequest() + + -- then + assertEquals(response_code, EXPECTED_RESPONSES.INVALID_KEY.error_code) + assertEquals(response_body, EXPECTED_RESPONSES.INVALID_KEY.message) + + calls(BaseValidatorMock.__exitFn, 1, EXPECTED_RESPONSES.INVALID_KEY.error_code, EXPECTED_RESPONSES.INVALID_KEY.message) + calls(BaseValidatorMock.__getKeyFromLocalCache, 1, "test-api-key:test-service", "cachedkeys") + calls(BaseValidatorMock.__getKeyFromRedis, 1) + calls(RedisConnectionProviderMock.__getConnection, 1) + calls(RedisConnectionProviderMock.__closeConnection, 1) + + calls(BaseValidatorMock.__setKeyInLocalCache, 0) + calls(BaseValidatorMock.__setContextProperties, 0) +end) + +test('validateRequest: should return 403003 if api key is not in local cache and redis entry in an empty table', function() + -- given + ngx.var.api_key = "test-api-key" + ngx.var.service_id = "test-service" + + BaseValidatorMock.__getKeyFromLocalCache.doReturn = function() + return nil + end + + BaseValidatorMock.__getKeyFromRedis.doReturn = function() + return nil + end + + RedisConnectionProviderMock.__getConnection.doReturn = function() + local redis = {} + redis.hmget = function (redis_key, ...) + return {}, nil + end + + return true, redis + end + + -- when + local classUnderTest = require('api-gateway.validation.key.redisApiKeyValidator'):new() + local response_code, response_body = classUnderTest:validateRequest() + + -- then + assertEquals(response_code, EXPECTED_RESPONSES.INVALID_KEY.error_code) + assertEquals(response_body, EXPECTED_RESPONSES.INVALID_KEY.message) + + calls(BaseValidatorMock.__exitFn, 1, EXPECTED_RESPONSES.INVALID_KEY.error_code, EXPECTED_RESPONSES.INVALID_KEY.message) + calls(BaseValidatorMock.__getKeyFromLocalCache, 1, "test-api-key:test-service", "cachedkeys") + calls(BaseValidatorMock.__getKeyFromRedis, 1) + calls(RedisConnectionProviderMock.__getConnection, 1) + calls(RedisConnectionProviderMock.__closeConnection, 1) + + calls(BaseValidatorMock.__setKeyInLocalCache, 0) + calls(BaseValidatorMock.__setContextProperties, 0) +end) \ No newline at end of file