diff --git a/CHANGELOG.md b/CHANGELOG.md index 27e18d4fa..5d8ac538b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Ability to modify query parameters in the URL rewriting policy [PR #724](https://github.com/3scale/apicast/pull/724) - 3scale referrer policy [PR #728](https://github.com/3scale/apicast/pull/728) - Liquid templating support in the rate-limit policy [PR #719](https://github.com/3scale/apicast/pull/719) +- Default credentials policy [PR #741](https://github.com/3scale/apicast/pull/741), [THREESCALE-586](https://issues.jboss.org/browse/THREESCALE-586) ### Changed diff --git a/gateway/src/apicast/policy/default_credentials/apicast-policy.json b/gateway/src/apicast/policy/default_credentials/apicast-policy.json new file mode 100644 index 000000000..1c2289e9f --- /dev/null +++ b/gateway/src/apicast/policy/default_credentials/apicast-policy.json @@ -0,0 +1,69 @@ +{ + "$schema": "http://apicast.io/policy-v1/schema#manifest#", + "name": "Default credentials", + "summary": "Provides default credentials for unauthenticated requests", + "description": + ["This policy allows to expose a service without authentication. ", + "It can be useful, for example, for legacy apps that cannot be adapted to ", + "send the auth params. ", + "When the credentials are not provided in the request, this policy ", + "provides the default ones configured. ", + "An app_id + app_key or a user_key should be configured."], + "version": "builtin", + "configuration": { + "type":"object", + "properties":{ + "auth_type":{ + "type":"string", + "enum":[ + "user_key", + "app_id_and_app_key" + ], + "default":"user_key" + } + }, + "required":[ + "auth_type" + ], + "dependencies":{ + "auth_type":{ + "oneOf":[ + { + "properties":{ + "auth_type":{ + "enum":[ + "user_key" + ] + }, + "user_key":{ + "type":"string" + } + }, + "required":[ + "user_key" + ] + }, + { + "properties":{ + "auth_type":{ + "enum":[ + "app_id_and_app_key" + ] + }, + "app_id":{ + "type":"string" + }, + "app_key":{ + "type":"string" + } + }, + "required":[ + "app_id", + "app_key" + ] + } + ] + } + } + } +} diff --git a/gateway/src/apicast/policy/default_credentials/default_credentials.lua b/gateway/src/apicast/policy/default_credentials/default_credentials.lua new file mode 100644 index 000000000..fd37f169b --- /dev/null +++ b/gateway/src/apicast/policy/default_credentials/default_credentials.lua @@ -0,0 +1,102 @@ +--- Default credentials policy + +local tostring = tostring + +local policy = require('apicast.policy') +local _M = policy.new('Default credentials policy') + +local new = _M.new + +function _M.new(config) + local self = new(config) + + if config then + self.default_credentials = { + user_key = config.user_key, + app_id = config.app_id, + app_key = config.app_key + } + else + self.default_credentials = {} + end + + return self +end + +local function creds_missing(service) + local service_creds = service:extract_credentials() + if not service_creds then return true end + + local backend_version = tostring(service.backend_version) + + if backend_version == '1' then + return service_creds.user_key == nil + elseif backend_version == '2' then + return service_creds.app_id == nil and service_creds.app_key == nil + end +end + +local function provide_creds_for_version_1(service, default_creds) + if default_creds.user_key then + -- follows same format as Service.extract_credentials() + service.extracted_credentials = { + default_creds.user_key, + user_key = default_creds.user_key + } + + ngx.log(ngx.DEBUG, 'Provided default creds for request') + else + ngx.log(ngx.WARN, 'No default user key configured') + end +end + +local function provide_creds_for_version_2(service, default_creds) + if default_creds.app_id and default_creds.app_key then + -- follows same format as Service.extract_credentials() + service.extracted_credentials = { + default_creds.app_id, + default_creds.app_key, + app_id = default_creds.app_id, + app_key = default_creds.app_key + } + + ngx.log(ngx.DEBUG, 'Provided default creds for request') + else + ngx.log(ngx.WARN, 'No default app_id + app_key configured') + end +end + +local creds_provider = { + ["1"] = provide_creds_for_version_1, + ["2"] = provide_creds_for_version_2 +} + +local function backend_version_is_supported(backend_version) + return creds_provider[backend_version] ~= nil +end + +local function provide_creds(service, default_creds) + local backend_version = tostring(service.backend_version) + creds_provider[backend_version](service, default_creds) +end + +function _M:rewrite(context) + local service = context.service + + if not service then + ngx.log(ngx.ERR, 'No service in the context') + return + end + + local backend_version = tostring(service.backend_version) + if not backend_version_is_supported(backend_version) then + ngx.log(ngx.ERR, 'Incompatible backend version: ', backend_version) + return + end + + if creds_missing(service) then + provide_creds(service, self.default_credentials) + end +end + +return _M diff --git a/gateway/src/apicast/policy/default_credentials/init.lua b/gateway/src/apicast/policy/default_credentials/init.lua new file mode 100644 index 000000000..3550c01e0 --- /dev/null +++ b/gateway/src/apicast/policy/default_credentials/init.lua @@ -0,0 +1 @@ +return require('default_credentials') diff --git a/gateway/src/apicast/proxy.lua b/gateway/src/apicast/proxy.lua index e2433e270..cf8186329 100644 --- a/gateway/src/apicast/proxy.lua +++ b/gateway/src/apicast/proxy.lua @@ -225,7 +225,13 @@ function _M:rewrite(service, context) ngx.var.secret_token = service.secret_token - local credentials, err = service:extract_credentials() + -- Another policy might have already extracted the creds. + local credentials = service.extracted_credentials + + local err + if not credentials then + credentials, err = service:extract_credentials() + end if not credentials then ngx.log(ngx.WARN, "cannot get credentials: ", err or 'unknown error') diff --git a/spec/policy/default_credentials/default_credentials_spec.lua b/spec/policy/default_credentials/default_credentials_spec.lua new file mode 100644 index 000000000..d999f15dc --- /dev/null +++ b/spec/policy/default_credentials/default_credentials_spec.lua @@ -0,0 +1,238 @@ +local ipairs = ipairs + +local DefaultCredentialsPolicy = require('apicast.policy.default_credentials') + +describe('Default credentials policy', function() + local policy_user_key = 'policy_uk' + local policy_app_id = 'policy_ai' + local policy_app_key = 'policy_ak' + local request_user_key = 'req_uk' + local request_app_id = 'req_ai' + local request_app_key = 'req_ak' + + local function policy_with_default_user_key() + return DefaultCredentialsPolicy.new( + { auth_type = 'user_key', user_key = policy_user_key } + ) + end + + local function policy_with_default_app_id_and_key() + return DefaultCredentialsPolicy.new( + { + auth_type = 'app_id_and_app_key', + app_id = policy_app_id, + app_key = policy_app_key + } + ) + end + + local function service_with_user_key_in_req() + return { + backend_version = '1', + extract_credentials = function() + return { request_user_key, user_key = request_user_key } + end + } + end + + local function service_without_user_key_in_req() + return { + backend_version = '1', + extract_credentials = function() return { } end + } + end + + local function service_with_app_id_and_key_in_req() + return { + backend_version = '2', + extract_credentials = function() + return { request_app_id, request_app_key, + app_id = request_app_id, app_key = request_app_key } + end + } + end + + local function service_with_app_id_in_req() + return { + backend_version = '2', + extract_credentials = function() + return { request_app_id, app_id = request_app_id } + end + } + end + + local function service_with_app_key_in_req() + return { + backend_version = '2', + extract_credentials = function() + return { request_app_key, app_key = request_app_key } + end + } + end + + local function service_without_app_id_nor_key_in_req() + return { + backend_version = '2', + extract_credentials = function() return {} end + } + end + + describe('.rewrite', function() + describe('when the service in the context has backend_version = 1', function() + describe('and the request includes a user key', function() + describe('and there is a default user key defined', function() + it('does not set a user key in the service creds', function() + local context = { service = service_with_user_key_in_req() } + + policy_with_default_user_key():rewrite(context) + + assert.is_nil(context.service.extracted_credentials) + end) + end) + + describe('and there is not a default user key defined', function() + it('does not set a user key in the service creds', function() + local context = { service = service_with_user_key_in_req() } + + policy_with_default_app_id_and_key():rewrite(context) + + assert.is_nil(context.service.extracted_credentials) + end) + end) + end) + + describe('and the request does not include a user key', function() + describe('and there is a default user key defined', function() + it('sets the default user key in the service creds', function() + local context = { service = service_without_user_key_in_req() } + + policy_with_default_user_key():rewrite(context) + + assert.same({ policy_user_key, user_key = policy_user_key }, + context.service.extracted_credentials) + end) + end) + + describe('and there is not a default user key defined', function() + it('does not set a user key in the service creds', function() + local context = { service = service_without_user_key_in_req() } + + policy_with_default_app_id_and_key():rewrite(context) + + assert.is_nil(context.service.extracted_credentials) + end) + end) + end) + end) + + describe('when the service in the context has backend_version = 2', function() + describe('and the request includes an app id and an app key', function() + describe('and there is a default app id and key defined', function() + it('does not set app id and key in the service creds', function() + local context = { service = service_with_app_id_and_key_in_req() } + + policy_with_default_app_id_and_key():rewrite(context) + + assert.is_nil(context.service.extracted_credentials) + end) + end) + + describe('and there is not a default app and key defined', function() + it('does not set app id and key in the service creds', function() + local context = { service = service_with_app_id_and_key_in_req() } + + policy_with_default_user_key():rewrite(context) + + assert.is_nil(context.service.extracted_credentials) + end) + end) + end) + + describe('and the request includes an app id but no app key', function() + describe('and there is a default app id and key defined', function() + it('does not set app id and key in the service creds', function() + local context = { service = service_with_app_id_in_req() } + + policy_with_default_app_id_and_key():rewrite(context) + + assert.is_nil(context.service.extracted_credentials) + end) + end) + + describe('and there is not a default app and key defined', function() + it('does not set app id and key in the service creds', function() + local context = { service = service_with_app_id_in_req() } + + policy_with_default_user_key():rewrite(context) + + assert.is_nil(context.service.extracted_credentials) + end) + end) + end) + + describe('and the request includes an app key but not an app id', function() + describe('and there is a default app id and key defined', function() + it('does not set app id and key in the service creds', function() + local context = { service = service_with_app_key_in_req() } + + policy_with_default_app_id_and_key():rewrite(context) + + assert.is_nil(context.service.extracted_credentials) + end) + end) + + describe('and there is not a default app and key defined', function() + it('does not set app id and key in the service creds', function() + local context = { service = service_with_app_key_in_req() } + + policy_with_default_user_key():rewrite(context) + + assert.is_nil(context.service.extracted_credentials) + end) + end) + end) + + describe('and the request does not include an app key nor an app id', function() + describe('and there is a default app id and key defined', function() + it('sets the app and key in the service creds', function() + local context = { service = service_without_app_id_nor_key_in_req() } + + policy_with_default_app_id_and_key():rewrite(context) + + assert.same( + { policy_app_id, policy_app_key, + app_id = policy_app_id, app_key = policy_app_key }, + context.service.extracted_credentials + ) + end) + end) + + describe('and there is not a default app and key defined', function() + it('does not set app id and key in the service creds', function() + local context = { service = service_without_app_id_nor_key_in_req() } + + policy_with_default_user_key():rewrite(context) + + assert.is_nil(context.service.extracted_credentials) + end) + end) + end) + end) + + describe('when the service in the context has backend_version != 1 and != 2', function() + it('does not set app id and key in the service creds', function() + local context = { service = { backend_version = 'oauth' } } + + local policies = { + policy_with_default_user_key(), + policy_with_default_app_id_and_key() + } + + for _, policy in ipairs(policies) do + policy:rewrite(context) + assert.is_nil(context.service.extracted_credentials) + end + end) + end) + end) +end) diff --git a/t/apicast-policy-default-credentials.t b/t/apicast-policy-default-credentials.t new file mode 100644 index 000000000..c8ed29f36 --- /dev/null +++ b/t/apicast-policy-default-credentials.t @@ -0,0 +1,204 @@ +use lib 't'; +use Test::APIcast::Blackbox 'no_plan'; + +run_tests(); + +__DATA__ + +=== TEST 1: sets default user key for request without credentials +--- configuration +{ + "services": [ + { + "id": 42, + "backend_version": 1, + "backend_authentication_type": "service_token", + "backend_authentication_value": "token-value", + "proxy": { + "policy_chain": [ + { + "name": "apicast.policy.default_credentials", + "configuration": { + "auth_type": "user_key", + "user_key": "uk" + } + }, + { + "name": "apicast.policy.apicast" + } + ], + "api_backend": "http://test:$TEST_NGINX_SERVER_PORT/", + "proxy_rules": [ + { "pattern": "/", "http_method": "GET", "metric_system_name": "hits", "delta": 2 } + ] + } + } + ] +} +--- backend + location /transactions/authrep.xml { + content_by_lua_block { + local expected = "service_token=token-value&service_id=42&usage%5Bhits%5D=2&user_key=uk" + require('luassert').same(ngx.decode_args(expected), ngx.req.get_uri_args(0)) + } + } +--- upstream + location / { + echo 'yay, api backend'; + } +--- request +GET / +--- response_body +yay, api backend +--- error_code: 200 +--- no_error_log +[error] + +=== TEST 2: sets default app_id + app_key for request without credentials +--- configuration +{ + "services": [ + { + "id": 42, + "backend_version": 2, + "backend_authentication_type": "service_token", + "backend_authentication_value": "token-value", + "proxy": { + "policy_chain": [ + { + "name": "apicast.policy.default_credentials", + "configuration": { + "auth_type": "app_id_and_app_key", + "app_id": "some_id", + "app_key": "some_key" + } + }, + { + "name": "apicast.policy.apicast" + } + ], + "api_backend": "http://test:$TEST_NGINX_SERVER_PORT/", + "proxy_rules": [ + { "pattern": "/", "http_method": "GET", "metric_system_name": "hits", "delta": 2 } + ] + } + } + ] +} +--- backend + location /transactions/authrep.xml { + content_by_lua_block { + local expected = "service_token=token-value&service_id=42&usage%5Bhits%5D=2&app_id=some_id&app_key=some_key" + require('luassert').same(ngx.decode_args(expected), ngx.req.get_uri_args(0)) + } + } +--- upstream + location / { + echo 'yay, api backend'; + } +--- request +GET / +--- response_body +yay, api backend +--- error_code: 200 +--- no_error_log +[error] + +=== TEST 3: does not set user key when it is in the request +--- configuration +{ + "services": [ + { + "id": 42, + "backend_version": 1, + "backend_authentication_type": "service_token", + "backend_authentication_value": "token-value", + "proxy": { + "policy_chain": [ + { + "name": "apicast.policy.default_credentials", + "configuration": { + "auth_type": "user_key", + "user_key": "set_by_the_policy" + } + }, + { + "name": "apicast.policy.apicast" + } + ], + "api_backend": "http://test:$TEST_NGINX_SERVER_PORT/", + "proxy_rules": [ + { "pattern": "/", "http_method": "GET", "metric_system_name": "hits", "delta": 2 } + ] + } + } + ] +} +--- backend + location /transactions/authrep.xml { + content_by_lua_block { + local expected = "service_token=token-value&service_id=42&usage%5Bhits%5D=2&user_key=uk" + require('luassert').same(ngx.decode_args(expected), ngx.req.get_uri_args(0)) + } + } +--- upstream + location / { + echo 'yay, api backend'; + } +--- request +GET /?user_key=uk +--- response_body +yay, api backend +--- error_code: 200 +--- no_error_log +[error] + +=== TEST 4: does not set app_id + app_key when they are in the request +--- configuration +{ + "services": [ + { + "id": 42, + "backend_version": 2, + "backend_authentication_type": "service_token", + "backend_authentication_value": "token-value", + "proxy": { + "policy_chain": [ + { + "name": "apicast.policy.default_credentials", + "configuration": { + "auth_type": "app_id_and_app_key", + "app_id": "id_set_by_the_policy", + "app_key": "key_set_by_the_policy" + } + }, + { + "name": "apicast.policy.apicast" + } + ], + "api_backend": "http://test:$TEST_NGINX_SERVER_PORT/", + "proxy_rules": [ + { "pattern": "/", "http_method": "GET", "metric_system_name": "hits", "delta": 2 } + ] + } + } + ] +} +--- backend + location /transactions/authrep.xml { + content_by_lua_block { + local expected = "service_token=token-value&service_id=42&usage%5Bhits%5D=2&app_id=some_id&app_key=some_key" + require('luassert').same(ngx.decode_args(expected), ngx.req.get_uri_args(0)) + } + } +--- upstream + location / { + echo 'yay, api backend'; + } +--- request +GET /?app_id=some_id&app_key=some_key +--- response_body +yay, api backend +--- error_code: 200 +--- no_error_log +[error]