From 68a0c29cb528d2639a0a36bd161d50f461746089 Mon Sep 17 00:00:00 2001 From: thefosk Date: Wed, 8 Jun 2016 12:41:54 -0700 Subject: [PATCH 01/29] Auto-Generating SSL certificates --- kong-0.8.2-0.rockspec | 1 + kong/cmd/utils/nginx_conf_compiler.lua | 19 +++- kong/cmd/utils/ssl.lua | 71 ++++++++++++++ kong/conf_loader.lua | 4 +- spec/01-unit/01-conf/01-conf_loader_spec.lua | 7 ++ .../01-conf/02-conf_compilation_spec.lua | 96 +++++++++++++++++-- spec/01-unit/16-ssl_spec.lua | 26 +++++ spec/kong_tests.conf | 4 +- 8 files changed, 214 insertions(+), 14 deletions(-) create mode 100644 kong/cmd/utils/ssl.lua create mode 100644 spec/01-unit/16-ssl_spec.lua diff --git a/kong-0.8.2-0.rockspec b/kong-0.8.2-0.rockspec index f142c28c5a69..d8d3fbe3f0a9 100644 --- a/kong-0.8.2-0.rockspec +++ b/kong-0.8.2-0.rockspec @@ -68,6 +68,7 @@ build = { ["kong.cmd.utils.nginx_conf_compiler"] = "kong/cmd/utils/nginx_conf_compiler.lua", ["kong.cmd.utils.nginx_signals"] = "kong/cmd/utils/nginx_signals.lua", ["kong.cmd.utils.serf_signals"] = "kong/cmd/utils/serf_signals.lua", + ["kong.cmd.utils.ssl"] = "kong/cmd/utils/ssl.lua", ["kong.api.init"] = "kong/api/init.lua", ["kong.api.api_helpers"] = "kong/api/api_helpers.lua", diff --git a/kong/cmd/utils/nginx_conf_compiler.lua b/kong/cmd/utils/nginx_conf_compiler.lua index 8ddd2b265f49..7b646f3ee6ab 100644 --- a/kong/cmd/utils/nginx_conf_compiler.lua +++ b/kong/cmd/utils/nginx_conf_compiler.lua @@ -29,6 +29,7 @@ local pl_utils = require "pl.utils" local pl_file = require "pl.file" local pl_path = require "pl.path" local pl_dir = require "pl.dir" +local ssl = require "kong.cmd.utils.ssl" local log = require "kong.cmd.utils.log" local function gather_system_infos(compile_env) @@ -63,10 +64,20 @@ local function compile_conf(kong_config, conf_template) compile_env.nginx_vars[k] = v end + local ssl_data, err = ssl.get_ssl_cert_and_key(kong_config, kong_config.prefix) + if not ssl_data then return nil, err end + if kong_config.cassandra_ssl and kong_config.cassandra_ssl_trusted_cert then compile_env["lua_ssl_trusted_certificate"] = kong_config.cassandra_ssl_trusted_cert - --compile_env["ssl_certificate"] = - --compile_env["ssl_certificate_key"] = + end + + if kong_config.ssl then + compile_env["ssl_cert"] = ssl_data.ssl_cert + compile_env["ssl_cert_key"] = ssl_data.ssl_cert_key + end + + if kong_config.dnsmasq then + compile_env["dns_resolver"] = "127.0.0.1:"..kong_config.dnsmasq_port end if kong_config.nginx_optimizations then @@ -119,6 +130,10 @@ local function prepare_prefix(kong_config, nginx_prefix) local ok, _, _, stderr = touch(acc_logs_path) if not ok then return nil, stderr end + -- auto-generate default SSL certificate + local ok, err = ssl.prepare_ssl_cert_and_key(nginx_prefix) + if not ok then return nil, err end + local nginx_config_path = pl_path.join(nginx_prefix, "nginx.conf") local kong_nginx_conf_path = pl_path.join(nginx_prefix, "nginx-kong.conf") diff --git a/kong/cmd/utils/ssl.lua b/kong/cmd/utils/ssl.lua new file mode 100644 index 000000000000..0257d7f3d0ca --- /dev/null +++ b/kong/cmd/utils/ssl.lua @@ -0,0 +1,71 @@ +local utils = require "kong.tools.utils" +local pl_path = require "pl.path" +local pl_utils = require "pl.utils" +local pl_dir = require "pl.dir" +local log = require "kong.cmd.utils.log" +local fmt = string.format + +local _M = {} + +local SSL_FOLDER = "ssl" +local SSL_CERT = "kong-default.crt" +local SSL_CERT_KEY = "kong-default.key" +local SSL_CERT_CSR = "kong-default.csr" + +function _M.get_ssl_cert_and_key(kong_config, nginx_prefix) + local ssl_cert, ssl_cert_key + if kong_config.ssl_cert and kong_config.ssl_cert_key then + ssl_cert = kong_config.ssl_cert + ssl_cert_key = kong_config.ssl_cert_key + else + ssl_cert = pl_path.join(nginx_prefix, SSL_FOLDER, SSL_CERT) + ssl_cert_key = pl_path.join(nginx_prefix, SSL_FOLDER, SSL_CERT_KEY) + end + + -- Check that the files exist + if ssl_cert and not pl_path.exists(ssl_cert) then + return false, "Can't find SSL certificate at: "..ssl_cert + end + if ssl_cert_key and not pl_path.exists(ssl_cert_key) then + return false, "Can't find SSL key at: "..ssl_cert_key + end + + return { ssl_cert = ssl_cert, ssl_cert_key = ssl_cert_key } +end + +function _M.prepare_ssl_cert_and_key(prefix) + -- Create SSL directory + local ssl_path = pl_path.join(prefix, SSL_FOLDER) + local ok, err = pl_dir.makepath(ssl_path) + if not ok then return nil, err end + + local ssl_cert = pl_path.join(prefix, SSL_FOLDER, SSL_CERT) + local ssl_cert_key = pl_path.join(prefix, SSL_FOLDER, SSL_CERT_KEY) + local ssl_cert_csr = pl_path.join(prefix, SSL_FOLDER, SSL_CERT_CSR) + + if not (pl_path.exists(ssl_cert) and pl_path.exists(ssl_cert_key)) then + -- Autogenerating the certificates for the first time + log.verbose("Auto-generating the default SSL certificate and key..") + + local passphrase = utils.random_string() + local commands = { + fmt("openssl genrsa -des3 -out %s -passout pass:%s 1024", ssl_cert_key, passphrase), + fmt("openssl req -new -key %s -out %s -subj \"/C=US/ST=California/L=San Francisco/O=Kong/OU=IT Department/CN=localhost\" -passin pass:%s", ssl_cert_key, ssl_cert_csr, passphrase), + fmt("cp %s %s.org", ssl_cert_key, ssl_cert_key), + fmt("openssl rsa -in %s.org -out %s -passin pass:%s", ssl_cert_key, ssl_cert_key, passphrase), + fmt("openssl x509 -req -in %s -signkey %s -out %s", ssl_cert_csr, ssl_cert_key, ssl_cert), + fmt("rm %s", ssl_cert_csr), + fmt("rm %s.org", ssl_cert_key) + } + for _, cmd in ipairs(commands) do + local ok, _, _, stderr = pl_utils.executeex(cmd) + if not ok then + return nil, "There was an error when auto-generating the default SSL certificate: "..stderr + end + end + + return true + end +end + +return _M \ No newline at end of file diff --git a/kong/conf_loader.lua b/kong/conf_loader.lua index 740f6ae6ebd7..3b56e5169f8e 100644 --- a/kong/conf_loader.lua +++ b/kong/conf_loader.lua @@ -117,6 +117,8 @@ local function check_and_infer(conf) if conf.dns_resolver and conf.dnsmasq then errors[#errors+1] = "when specifying a custom DNS resolver you must turn off dnsmasq" + elseif not conf.dns_resolver and not conf.dnsmasq then + errors[#errors+1] = "you must specify at least dnsmasq or a custom DNS resolver" end local ipv4_port_pattern = "^(%d+)%.(%d+)%.(%d+)%.(%d+):(%d+)$" @@ -126,7 +128,7 @@ local function check_and_infer(conf) if not conf.cluster_listen_rpc:match(ipv4_port_pattern) then errors[#errors+1] = "cluster_listen_rpc must be in the form of IPv4:port" end - if cluster_advertise and not conf.cluster_advertise:match(ipv4_port_pattern) then + if conf.cluster_advertise and not conf.cluster_advertise:match(ipv4_port_pattern) then errors[#errors+1] = "cluster_advertise must be in the form of IPv4:port" end if conf.cluster_ttl_on_failure < 60 then diff --git a/spec/01-unit/01-conf/01-conf_loader_spec.lua b/spec/01-unit/01-conf/01-conf_loader_spec.lua index e8c378fba2de..7f8d32994372 100644 --- a/spec/01-unit/01-conf/01-conf_loader_spec.lua +++ b/spec/01-unit/01-conf/01-conf_loader_spec.lua @@ -199,6 +199,13 @@ describe("Configuration loader", function() assert.equal("when specifying a custom DNS resolver you must turn off dnsmasq", err) assert.is_nil(conf) + local conf, err = conf_loader(nil, { + dnsmasq = false, + dns_resolver = nil + }) + assert.equal("you must specify at least dnsmasq or a custom DNS resolver", err) + assert.is_nil(conf) + conf, err = conf_loader(nil, { dnsmasq = false, dns_resolver = "8.8.8.8:53" diff --git a/spec/01-unit/01-conf/02-conf_compilation_spec.lua b/spec/01-unit/01-conf/02-conf_compilation_spec.lua index ce4eccb39ef0..237657e84be5 100644 --- a/spec/01-unit/01-conf/02-conf_compilation_spec.lua +++ b/spec/01-unit/01-conf/02-conf_compilation_spec.lua @@ -1,6 +1,8 @@ local nginx_conf_compiler = require "kong.cmd.utils.nginx_conf_compiler" local conf_loader = require "kong.conf_loader" local helpers = require "spec.helpers" +local pl_file = require "pl.file" +local pl_tablex = require "pl.tablex" describe("NGINX conf compiler", function() local custom_conf @@ -34,14 +36,6 @@ describe("NGINX conf compiler", function() assert.matches("listen 0.0.0.0:80;", kong_nginx_conf, nil, true) assert.matches("listen 127.0.0.1:8001;", kong_nginx_conf, nil, true) end) - it("disables SSL", function() - local kong_nginx_conf = nginx_conf_compiler.compile_kong_conf(custom_conf) - assert.not_matches("listen %d+%.%d+%.%d+%.%d+:%d+ ssl;", kong_nginx_conf) - assert.not_matches("ssl_certificate", kong_nginx_conf) - assert.not_matches("ssl_certificate_key", kong_nginx_conf) - assert.not_matches("ssl_protocols", kong_nginx_conf) - assert.not_matches("ssl_certificate_by_lua_block", kong_nginx_conf) - end) it("sets lua_ssl_trusted_certificate", function() local conf = assert(conf_loader(helpers.test_conf_path, { cassandra_ssl = true, @@ -50,6 +44,88 @@ describe("NGINX conf compiler", function() local kong_nginx_conf = nginx_conf_compiler.compile_kong_conf(conf) assert.matches("lua_ssl_trusted_certificate '/path/to/ca.cert';", kong_nginx_conf, nil, true) end) + + describe("SSL", function() + local custom_conf_default_ssl, custom_conf_other_ssl + setup(function() + custom_conf_default_ssl = assert(conf_loader(helpers.test_conf_path, { + ssl = true + })) + + custom_conf_other_ssl = assert(conf_loader(helpers.test_conf_path, { + ssl = true, + ssl_cert = "/tmp/custom.crt", + ssl_cert_key = "/tmp/custom.key" + })) + + assert(pl_file.write("/tmp/custom.crt", "CUSTOM")) + assert(pl_file.write("/tmp/custom.key", "CUSTOM")) + end) + + teardown(function() + assert(pl_file.delete("/tmp/custom.crt")) + assert(pl_file.delete("/tmp/custom.key")) + end) + + it("default SSL", function() + local kong_nginx_conf = nginx_conf_compiler.compile_kong_conf(custom_conf_default_ssl) + assert.matches("ssl_certificate spec/fixtures/kong_spec.crt;", kong_nginx_conf) + assert.matches("ssl_certificate_key spec/fixtures/kong_spec.key;", kong_nginx_conf) + end) + it("custom SSL", function() + local kong_nginx_conf = nginx_conf_compiler.compile_kong_conf(custom_conf_other_ssl) + assert.matches("ssl_certificate /tmp/custom.crt;", kong_nginx_conf) + assert.matches("ssl_certificate_key /tmp/custom.key;", kong_nginx_conf) + end) + it("disables SSL", function() + local kong_nginx_conf = nginx_conf_compiler.compile_kong_conf(custom_conf) + assert.not_matches("listen %d+%.%d+%.%d+%.%d+:%d+ ssl;", kong_nginx_conf) + assert.not_matches("ssl_certificate", kong_nginx_conf) + assert.not_matches("ssl_certificate_key", kong_nginx_conf) + assert.not_matches("ssl_protocols", kong_nginx_conf) + assert.not_matches("ssl_certificate_by_lua_block", kong_nginx_conf) + end) + it("should return an error if the files do not exist", function() + local conf = pl_tablex.deepcopy(custom_conf_other_ssl) + conf.ssl_cert = "/hello.crt" + local _, err = nginx_conf_compiler.compile_kong_conf(conf) + assert.equal("Can't find SSL certificate at: /hello.crt", err) + + conf = pl_tablex.deepcopy(custom_conf_other_ssl) + conf.ssl_cert_key = "/hello.key" + local _, err = nginx_conf_compiler.compile_kong_conf(conf) + assert.equal("Can't find SSL key at: /hello.key", err) + end) + end) + + describe("DNS", function() + local custom_conf_dnsmasq, custom_conf_resolver + setup(function() + custom_conf_dnsmasq = assert(conf_loader(helpers.test_conf_path, { + dnsmasq = true, + dns_resolver = "" + })) + custom_conf_resolver = assert(conf_loader(helpers.test_conf_path, { + dnsmasq = false, + dns_resolver = "8.8.8.8:43" + })) + end) + + it("compiles with dnsmasq", function() + local kong_nginx_conf = nginx_conf_compiler.compile_kong_conf(custom_conf_dnsmasq) + assert.matches("resolver 127.0.0.1:8053 ipv6=off;", kong_nginx_conf) + end) + it("compiles with dnsmasq and a custom port", function() + local conf = pl_tablex.deepcopy(custom_conf_dnsmasq) + conf.dnsmasq_port = 4000 + local kong_nginx_conf = nginx_conf_compiler.compile_kong_conf(conf) + assert.matches("resolver 127.0.0.1:4000 ipv6=off;", kong_nginx_conf) + end) + it("compiles with custom resolver", function() + local kong_nginx_conf = nginx_conf_compiler.compile_kong_conf(custom_conf_resolver) + assert.matches("resolver 8.8.8.8:43 ipv6=off;", kong_nginx_conf) + end) + end) end) describe("compile_nginx_conf()", function() @@ -106,12 +182,14 @@ describe("NGINX conf compiler", function() assert.equal(tmp.." is not a directory", err) assert.is_nil(ok) end) - it("creates NGINX conf and log files", function() + it("creates NGINX conf, log files and default SSL certs", function() assert(nginx_conf_compiler.prepare_prefix(helpers.test_conf, prefix)) assert.truthy(exists(join(prefix, "nginx.conf"))) assert.truthy(exists(join(prefix, "nginx-kong.conf"))) assert.truthy(exists(join(prefix, "logs", "error.log"))) assert.truthy(exists(join(prefix, "logs", "access.log"))) + assert.truthy(exists(join(prefix, "ssl", "kong-default.crt"))) + assert.truthy(exists(join(prefix, "ssl", "kong-default.key"))) end) end) end) diff --git a/spec/01-unit/16-ssl_spec.lua b/spec/01-unit/16-ssl_spec.lua new file mode 100644 index 000000000000..aed5788d7fa8 --- /dev/null +++ b/spec/01-unit/16-ssl_spec.lua @@ -0,0 +1,26 @@ +local pl_path = require "pl.path" +local pl_dir = require "pl.dir" +local ssl = require "kong.cmd.utils.ssl" + +describe("SSL Utils", function() + + setup(function() + pcall(pl_dir.rmtree, "/tmp/ssl") + end) + + it("should auto-generate an SSL certificate and key", function() + assert(ssl.prepare_ssl_cert_and_key("/tmp")) + assert(pl_path.exists("/tmp/ssl/kong-default.crt")) + assert(pl_path.exists("/tmp/ssl/kong-default.key")) + end) + + it("retrieve the default SSL certificate and key", function() + local ssl_data, err = ssl.get_ssl_cert_and_key({}, "/tmp") + assert.is_table(ssl_data) + assert.is_nil(err) + + assert.is_string(ssl_data.ssl_cert) + assert.is_string(ssl_data.ssl_cert_key) + end) + +end) \ No newline at end of file diff --git a/spec/kong_tests.conf b/spec/kong_tests.conf index 5616d32dcf12..9de0383b8e0c 100644 --- a/spec/kong_tests.conf +++ b/spec/kong_tests.conf @@ -4,8 +4,8 @@ proxy_listen = 0.0.0.0:9000 proxy_listen_ssl = 0.0.0.0:9443 cluster_listen = 0.0.0.0:9946 cluster_listen_rpc = 127.0.0.1:9373 -ssl_cert = ../spec/fixtures/kong_spec.crt -ssl_cert_key = ../spec/fixtures/kong_spec.key +ssl_cert = spec/fixtures/kong_spec.crt +ssl_cert_key = spec/fixtures/kong_spec.key dnsmasq = off dns_resolver = 8.8.8.8 database = postgres From ce3406224e21899e699c3fc66f722a5b11398c0e Mon Sep 17 00:00:00 2001 From: thefosk Date: Thu, 9 Jun 2016 12:17:34 -0700 Subject: [PATCH 02/29] dnsmasq signals --- kong/cmd/reload.lua | 2 + kong/cmd/start.lua | 4 +- kong/cmd/stop.lua | 3 + kong/cmd/utils/dnsmasq_signals.lua | 81 +++++++++++++++++++ kong/conf_loader.lua | 2 +- .../01-cmd/02-start_stop_spec.lua | 43 ++++++++-- spec/02-integration/01-cmd/04-reload_spec.lua | 8 +- spec/kong_tests.conf | 4 +- 8 files changed, 134 insertions(+), 13 deletions(-) create mode 100644 kong/cmd/utils/dnsmasq_signals.lua diff --git a/kong/cmd/reload.lua b/kong/cmd/reload.lua index 679a229e244f..1a48438dabe6 100644 --- a/kong/cmd/reload.lua +++ b/kong/cmd/reload.lua @@ -1,6 +1,7 @@ local nginx_conf_compiler = require "kong.cmd.utils.nginx_conf_compiler" local nginx_signals = require "kong.cmd.utils.nginx_signals" local serf_signals = require "kong.cmd.utils.serf_signals" +local dnsmasq_signals = require "kong.cmd.utils.dnsmasq_signals" local conf_loader = require "kong.conf_loader" local DAOFactory = require "kong.dao.factory" local log = require "kong.cmd.utils.log" @@ -11,6 +12,7 @@ local function execute(args) })) assert(nginx_conf_compiler.prepare_prefix(conf, conf.prefix)) + assert(dnsmasq_signals.start(conf, conf.prefix)) assert(serf_signals.start(conf, conf.prefix, DAOFactory(conf))) assert(nginx_signals.reload(conf.prefix)) log("Reloaded") diff --git a/kong/cmd/start.lua b/kong/cmd/start.lua index befbc6712836..9c08cca33593 100644 --- a/kong/cmd/start.lua +++ b/kong/cmd/start.lua @@ -1,6 +1,7 @@ local nginx_conf_compiler = require "kong.cmd.utils.nginx_conf_compiler" local nginx_signals = require "kong.cmd.utils.nginx_signals" local serf_signals = require "kong.cmd.utils.serf_signals" +local dnsmasq_signals = require "kong.cmd.utils.dnsmasq_signals" local conf_loader = require "kong.conf_loader" local DAOFactory = require "kong.dao.factory" local log = require "kong.cmd.utils.log" @@ -19,8 +20,9 @@ local function execute(args) local dao = DAOFactory(conf) assert(dao:run_migrations()) - assert(nginx_conf_compiler.prepare_prefix(conf, conf.prefix)) + assert(dnsmasq_signals.start(conf, conf.prefix)) assert(serf_signals.start(conf, conf.prefix, dao)) + assert(nginx_conf_compiler.prepare_prefix(conf, conf.prefix)) assert(nginx_signals.start(conf.prefix)) log("Started") end diff --git a/kong/cmd/stop.lua b/kong/cmd/stop.lua index 351e5d13fee1..bb3d81385f34 100644 --- a/kong/cmd/stop.lua +++ b/kong/cmd/stop.lua @@ -1,5 +1,6 @@ local nginx_signals = require "kong.cmd.utils.nginx_signals" local serf_signals = require "kong.cmd.utils.serf_signals" +local dnsmasq_signals = require "kong.cmd.utils.dnsmasq_signals" local conf_loader = require "kong.conf_loader" local log = require "kong.cmd.utils.log" @@ -12,6 +13,8 @@ local function execute(args) assert(nginx_signals.stop(conf.prefix)) assert(serf_signals.stop(conf.prefix)) + assert(serf_signals.stop(conf.prefix)) + assert(dnsmasq_signals.stop(conf.prefix)) log("Stopped") end diff --git a/kong/cmd/utils/dnsmasq_signals.lua b/kong/cmd/utils/dnsmasq_signals.lua new file mode 100644 index 000000000000..c0a965464958 --- /dev/null +++ b/kong/cmd/utils/dnsmasq_signals.lua @@ -0,0 +1,81 @@ +local pl_utils = require "pl.utils" +local pl_path = require "pl.path" +local pl_file = require "pl.file" +local log = require "kong.cmd.utils.log" +local kill = require "kong.cmd.utils.kill" +local fmt = string.format + +local _M = {} + +local dnsmasq_bin_name = "dnsmasq" +local dnsmasq_pid_name = "dnsmasq.pid" +local dnsmasq_search_paths = { + "/usr/local/sbin", + "/usr/local/bin", + "/usr/sbin", + "/usr/bin", + "/bin", + "" +} + +function _M.find_bin() + log.verbose("searching for 'dnsmasq' executable...") + + local found + for _, path in ipairs(dnsmasq_search_paths) do + local path_to_check = pl_path.join(path, dnsmasq_bin_name) + local cmd = fmt("%s -v", path_to_check) + local ok = pl_utils.executeex(cmd) + if ok then + found = path_to_check + break + end + end + + if not found then + return nil, "could not find 'dnsmasq' executable" + end + + log.verbose("found 'dnsmasq' executable at %s", found) + + return found +end + +local function is_running(pid_path) + if not pl_path.exists(pid_path) then return nil end + local code = kill(pid_path, "-0") + return code == 0 +end + +function _M.start(kong_config, nginx_prefix) + -- is dnsmasq already running in this prefix? + local pid_path = pl_path.join(nginx_prefix, dnsmasq_pid_name) + if is_running(pid_path) then + log.verbose("dnsmasq already running at %s", pid_path) + return true + else + log.verbose("dnsmasq not running, deleting %s", pid_path) + pl_file.delete(pid_path) + end + + -- make sure Serf is in PATH + local dnsmasq_bin, err = _M.find_bin() + if not dnsmasq_bin then return nil, err end + + local cmd = fmt("%s -p %d --pid-file=%s -N -o --listen-address=127.0.0.1", dnsmasq_bin, kong_config.dnsmasq_port, pid_path) + + log.debug("starting dnsmasq: %s", cmd) + + local ok, _, _, stderr = pl_utils.executeex(cmd) + if not ok then return nil, stderr end + + return true +end + +function _M.stop(nginx_prefix) + local pid_path = pl_path.join(nginx_prefix, dnsmasq_pid_name) + log.verbose("stopping dnsmasq at %s", pid_path) + return kill(pid_path, "-9") +end + +return _M \ No newline at end of file diff --git a/kong/conf_loader.lua b/kong/conf_loader.lua index 740f6ae6ebd7..c937fce7b548 100644 --- a/kong/conf_loader.lua +++ b/kong/conf_loader.lua @@ -126,7 +126,7 @@ local function check_and_infer(conf) if not conf.cluster_listen_rpc:match(ipv4_port_pattern) then errors[#errors+1] = "cluster_listen_rpc must be in the form of IPv4:port" end - if cluster_advertise and not conf.cluster_advertise:match(ipv4_port_pattern) then + if conf.cluster_advertise and not conf.cluster_advertise:match(ipv4_port_pattern) then errors[#errors+1] = "cluster_advertise must be in the form of IPv4:port" end if conf.cluster_ttl_on_failure < 60 then diff --git a/spec/02-integration/01-cmd/02-start_stop_spec.lua b/spec/02-integration/01-cmd/02-start_stop_spec.lua index 4ae354ac0196..d61bad05c96a 100644 --- a/spec/02-integration/01-cmd/02-start_stop_spec.lua +++ b/spec/02-integration/01-cmd/02-start_stop_spec.lua @@ -1,5 +1,7 @@ local helpers = require "spec.helpers" +local KILL_ALL = "pkill nginx; pkill serf; pkill dnsmasq" + local function exec(args) args = args or "" return helpers.execute(helpers.bin_path.." "..args) @@ -7,11 +9,11 @@ end describe("kong start/stop", function() setup(function() - helpers.execute "pkill nginx; pkill serf" + helpers.execute(KILL_ALL) helpers.prepare_prefix() end) teardown(function() - helpers.execute "pkill nginx; pkill serf" + helpers.execute(KILL_ALL) helpers.clean_prefix() end) @@ -60,7 +62,7 @@ describe("kong start/stop", function() assert.equal("", stderr) finally(function() - helpers.execute "pkill nginx; pkill serf" + helpers.execute(KILL_ALL) end) end) it("accepts debug", function() @@ -72,7 +74,7 @@ describe("kong start/stop", function() assert.equal("", stderr) finally(function() - helpers.execute "pkill nginx; pkill serf" + helpers.execute(KILL_ALL) end) end) end) @@ -106,6 +108,35 @@ describe("kong start/stop", function() end) end) + describe("#only dnsmasq", function() + it("starts dnsmasq daemon", function() + local ok = exec("start --conf "..helpers.test_conf_path) + assert.True(ok) + + local dnsmasq_pid_path = helpers.path.join(helpers.test_conf.prefix, "dnsmasq.pid") + local cmd = string.format("kill -0 `cat %s` >/dev/null 2>&1", dnsmasq_pid_path) + local ok, code = helpers.execute(cmd) + assert.True(ok) + assert.equal(0, code) + + assert.True(exec("stop --prefix "..helpers.test_conf.prefix)) + end) + it("recovers from expired dnsmasq.pid file", function() + local dnsmasq_pid_path = helpers.path.join(helpers.test_conf.prefix, "dnsmasq.pid") + local ok = helpers.execute("touch "..dnsmasq_pid_path) -- dumb pid + assert.True(ok) + + assert.True(exec("start --conf "..helpers.test_conf_path)) + + local cmd = string.format("kill -0 `cat %s` >/dev/null 2>&1", dnsmasq_pid_path) + local ok, code = helpers.execute(cmd) + assert.True(ok) + assert.equal(0, code) + + assert.True(exec("stop --prefix "..helpers.test_conf.prefix)) + end) + end) + describe("errors", function() it("start inexistent Kong conf file", function() local ok, _, stdout, stderr = exec "start --conf foobar.conf" @@ -134,7 +165,7 @@ describe("kong start/stop", function() assert.matches("Error: could not get Nginx pid", stderr, nil, true) finally(function() - helpers.execute "pkill nginx; pkill serf" + helpers.execute(KILL_ALL) helpers.dir.rmtree(helpers.test_conf.prefix) end) end) @@ -151,7 +182,7 @@ describe("kong start/stop", function() assert.equal("", stdout) assert.matches("Nginx is already running in", stderr) finally(function() - helpers.execute "pkill nginx; pkill serf" + helpers.execute(KILL_ALL) helpers.dir.rmtree(helpers.test_conf.prefix) end) end) diff --git a/spec/02-integration/01-cmd/04-reload_spec.lua b/spec/02-integration/01-cmd/04-reload_spec.lua index 7f78998716b2..b5d6c9e408b9 100644 --- a/spec/02-integration/01-cmd/04-reload_spec.lua +++ b/spec/02-integration/01-cmd/04-reload_spec.lua @@ -1,18 +1,20 @@ local helpers = require "spec.helpers" +local KILL_ALL = "pkill nginx; pkill serf; pkill dnsmasq" + describe("kong reload", function() setup(function() - helpers.execute "pkill nginx; pkill serf" + helpers.execute(KILL_ALL) helpers.prepare_prefix() end) teardown(function() - helpers.execute "pkill nginx; pkill serf" + helpers.execute(KILL_ALL) helpers.clean_prefix() end) it("send a HUP signal to a running nginx master process", function() finally(function() - helpers.execute "pkill nginx; pkill serf" + helpers.execute(KILL_ALL) end) assert(helpers.start_kong()) diff --git a/spec/kong_tests.conf b/spec/kong_tests.conf index 5616d32dcf12..82ad033f0b91 100644 --- a/spec/kong_tests.conf +++ b/spec/kong_tests.conf @@ -6,8 +6,8 @@ cluster_listen = 0.0.0.0:9946 cluster_listen_rpc = 127.0.0.1:9373 ssl_cert = ../spec/fixtures/kong_spec.crt ssl_cert_key = ../spec/fixtures/kong_spec.key -dnsmasq = off -dns_resolver = 8.8.8.8 +dnsmasq = on +dnsmasq_port = 9053 database = postgres pg_host = 127.0.0.1 pg_port = 5432 From 338f72bee819a43a5532356254c311f4a4947096 Mon Sep 17 00:00:00 2001 From: thefosk Date: Thu, 9 Jun 2016 20:22:52 -0700 Subject: [PATCH 03/29] in progress --- kong/cmd/start.lua | 2 +- kong/cmd/utils/nginx_conf_compiler.lua | 19 +++- kong/cmd/utils/ssl.lua | 8 +- kong/conf_loader.lua | 13 ++- spec/01-unit/01-conf/01-conf_loader_spec.lua | 7 -- .../01-conf/02-conf_compilation_spec.lua | 107 +++--------------- .../01-cmd/02-start_stop_spec.lua | 12 +- spec/02-integration/01-cmd/06-check_spec.lua | 4 +- .../05-proxy/01-resolver_spec.lua | 2 +- spec/03-plugins/oauth2/api_old.lua | 2 +- spec/fixtures/invalid.conf | 3 +- 11 files changed, 59 insertions(+), 120 deletions(-) diff --git a/kong/cmd/start.lua b/kong/cmd/start.lua index 9c08cca33593..c2b0485894bd 100644 --- a/kong/cmd/start.lua +++ b/kong/cmd/start.lua @@ -20,9 +20,9 @@ local function execute(args) local dao = DAOFactory(conf) assert(dao:run_migrations()) + assert(nginx_conf_compiler.prepare_prefix(conf, conf.prefix)) assert(dnsmasq_signals.start(conf, conf.prefix)) assert(serf_signals.start(conf, conf.prefix, dao)) - assert(nginx_conf_compiler.prepare_prefix(conf, conf.prefix)) assert(nginx_signals.start(conf.prefix)) log("Started") end diff --git a/kong/cmd/utils/nginx_conf_compiler.lua b/kong/cmd/utils/nginx_conf_compiler.lua index 7b646f3ee6ab..6be44df94260 100644 --- a/kong/cmd/utils/nginx_conf_compiler.lua +++ b/kong/cmd/utils/nginx_conf_compiler.lua @@ -31,6 +31,7 @@ local pl_path = require "pl.path" local pl_dir = require "pl.dir" local ssl = require "kong.cmd.utils.ssl" local log = require "kong.cmd.utils.log" +local fmt = string.format local function gather_system_infos(compile_env) local infos = {} @@ -111,7 +112,9 @@ local function prepare_prefix(kong_config, nginx_prefix) log.verbose("preparing nginx prefix directory at %s", nginx_prefix) if not pl_path.exists(nginx_prefix) then - return nil, nginx_prefix.." does not exist" + log.verbose(fmt("prefix directory %s not found, trying to create it", nginx_prefix)) + local ok, err = pl_dir.makepath(nginx_prefix) + if not ok then return nil, err end elseif not pl_path.isdir(nginx_prefix) then return nil, nginx_prefix.." is not a directory" end @@ -129,7 +132,7 @@ local function prepare_prefix(kong_config, nginx_prefix) if not ok then return nil, stderr end local ok, _, _, stderr = touch(acc_logs_path) if not ok then return nil, stderr end - + -- auto-generate default SSL certificate local ok, err = ssl.prepare_ssl_cert_and_key(nginx_prefix) if not ok then return nil, err end @@ -138,12 +141,16 @@ local function prepare_prefix(kong_config, nginx_prefix) local kong_nginx_conf_path = pl_path.join(nginx_prefix, "nginx-kong.conf") -- write NGINX conf - local nginx_conf = compile_nginx_conf(kong_config) - pl_file.write(nginx_config_path, nginx_conf) + local nginx_conf, err = compile_nginx_conf(kong_config) + if not nginx_conf then return nil, err end + local ok, err = pl_file.write(nginx_config_path, nginx_conf) + if not ok then return nil, err end -- write Kong's NGINX conf - local kong_nginx_conf = compile_kong_conf(kong_config) - pl_file.write(kong_nginx_conf_path, kong_nginx_conf) + local kong_nginx_conf, err = compile_kong_conf(kong_config) + if not kong_nginx_conf then return nil, err end + local ok, err = pl_file.write(kong_nginx_conf_path, kong_nginx_conf) + if not ok then return nil, err end return true end diff --git a/kong/cmd/utils/ssl.lua b/kong/cmd/utils/ssl.lua index 0257d7f3d0ca..96a087331b04 100644 --- a/kong/cmd/utils/ssl.lua +++ b/kong/cmd/utils/ssl.lua @@ -23,10 +23,10 @@ function _M.get_ssl_cert_and_key(kong_config, nginx_prefix) end -- Check that the files exist - if ssl_cert and not pl_path.exists(ssl_cert) then + if not pl_path.exists(ssl_cert) then return false, "Can't find SSL certificate at: "..ssl_cert end - if ssl_cert_key and not pl_path.exists(ssl_cert_key) then + if not pl_path.exists(ssl_cert_key) then return false, "Can't find SSL key at: "..ssl_cert_key end @@ -63,9 +63,9 @@ function _M.prepare_ssl_cert_and_key(prefix) return nil, "There was an error when auto-generating the default SSL certificate: "..stderr end end - - return true end + + return true end return _M \ No newline at end of file diff --git a/kong/conf_loader.lua b/kong/conf_loader.lua index 3b56e5169f8e..ea5a3090ef61 100644 --- a/kong/conf_loader.lua +++ b/kong/conf_loader.lua @@ -117,8 +117,6 @@ local function check_and_infer(conf) if conf.dns_resolver and conf.dnsmasq then errors[#errors+1] = "when specifying a custom DNS resolver you must turn off dnsmasq" - elseif not conf.dns_resolver and not conf.dnsmasq then - errors[#errors+1] = "you must specify at least dnsmasq or a custom DNS resolver" end local ipv4_port_pattern = "^(%d+)%.(%d+)%.(%d+)%.(%d+):(%d+)$" @@ -235,6 +233,17 @@ local function load(path, custom_conf) conf.custom_plugins = nil end + -- Load absolute path + conf.prefix = pl_path.abspath(conf.prefix) + + -- Handles relative paths for the ssl_cert and ssl_cert_key + if conf.ssl_cert and not pl_path.isabs(conf.ssl_cert) then + conf.ssl_cert = pl_path.abspath("")..conf.ssl_cert + end + if conf.ssl_cert_key and not pl_path.isabs(conf.ssl_cert_key) then + conf.ssl_cert_key = pl_path.abspath("")..conf.ssl_cert_key + end + do -- extract ports/listen ips local ip_port_pat = "(.+):([%d]+)$" diff --git a/spec/01-unit/01-conf/01-conf_loader_spec.lua b/spec/01-unit/01-conf/01-conf_loader_spec.lua index 7f8d32994372..e8c378fba2de 100644 --- a/spec/01-unit/01-conf/01-conf_loader_spec.lua +++ b/spec/01-unit/01-conf/01-conf_loader_spec.lua @@ -199,13 +199,6 @@ describe("Configuration loader", function() assert.equal("when specifying a custom DNS resolver you must turn off dnsmasq", err) assert.is_nil(conf) - local conf, err = conf_loader(nil, { - dnsmasq = false, - dns_resolver = nil - }) - assert.equal("you must specify at least dnsmasq or a custom DNS resolver", err) - assert.is_nil(conf) - conf, err = conf_loader(nil, { dnsmasq = false, dns_resolver = "8.8.8.8:53" diff --git a/spec/01-unit/01-conf/02-conf_compilation_spec.lua b/spec/01-unit/01-conf/02-conf_compilation_spec.lua index f05a981e8d9b..4465212699f6 100644 --- a/spec/01-unit/01-conf/02-conf_compilation_spec.lua +++ b/spec/01-unit/01-conf/02-conf_compilation_spec.lua @@ -1,8 +1,6 @@ local nginx_conf_compiler = require "kong.cmd.utils.nginx_conf_compiler" local conf_loader = require "kong.conf_loader" local helpers = require "spec.helpers" -local pl_file = require "pl.file" -local pl_tablex = require "pl.tablex" describe("NGINX conf compiler", function() local custom_conf @@ -36,6 +34,14 @@ describe("NGINX conf compiler", function() assert.matches("listen 0.0.0.0:80;", kong_nginx_conf, nil, true) assert.matches("listen 127.0.0.1:8001;", kong_nginx_conf, nil, true) end) + it("disables SSL", function() + local kong_nginx_conf = nginx_conf_compiler.compile_kong_conf(custom_conf) + assert.not_matches("listen %d+%.%d+%.%d+%.%d+:%d+ ssl;", kong_nginx_conf) + assert.not_matches("ssl_certificate", kong_nginx_conf) + assert.not_matches("ssl_certificate_key", kong_nginx_conf) + assert.not_matches("ssl_protocols", kong_nginx_conf) + assert.not_matches("ssl_certificate_by_lua_block", kong_nginx_conf) + end) it("sets lua_ssl_trusted_certificate", function() local conf = assert(conf_loader(helpers.test_conf_path, { cassandra_ssl = true, @@ -44,88 +50,6 @@ describe("NGINX conf compiler", function() local kong_nginx_conf = nginx_conf_compiler.compile_kong_conf(conf) assert.matches("lua_ssl_trusted_certificate '/path/to/ca.cert';", kong_nginx_conf, nil, true) end) - - describe("SSL", function() - local custom_conf_default_ssl, custom_conf_other_ssl - setup(function() - custom_conf_default_ssl = assert(conf_loader(helpers.test_conf_path, { - ssl = true - })) - - custom_conf_other_ssl = assert(conf_loader(helpers.test_conf_path, { - ssl = true, - ssl_cert = "/tmp/custom.crt", - ssl_cert_key = "/tmp/custom.key" - })) - - assert(pl_file.write("/tmp/custom.crt", "CUSTOM")) - assert(pl_file.write("/tmp/custom.key", "CUSTOM")) - end) - - teardown(function() - assert(pl_file.delete("/tmp/custom.crt")) - assert(pl_file.delete("/tmp/custom.key")) - end) - - it("default SSL", function() - local kong_nginx_conf = nginx_conf_compiler.compile_kong_conf(custom_conf_default_ssl) - assert.matches("ssl_certificate spec/fixtures/kong_spec.crt;", kong_nginx_conf) - assert.matches("ssl_certificate_key spec/fixtures/kong_spec.key;", kong_nginx_conf) - end) - it("custom SSL", function() - local kong_nginx_conf = nginx_conf_compiler.compile_kong_conf(custom_conf_other_ssl) - assert.matches("ssl_certificate /tmp/custom.crt;", kong_nginx_conf) - assert.matches("ssl_certificate_key /tmp/custom.key;", kong_nginx_conf) - end) - it("disables SSL", function() - local kong_nginx_conf = nginx_conf_compiler.compile_kong_conf(custom_conf) - assert.not_matches("listen %d+%.%d+%.%d+%.%d+:%d+ ssl;", kong_nginx_conf) - assert.not_matches("ssl_certificate", kong_nginx_conf) - assert.not_matches("ssl_certificate_key", kong_nginx_conf) - assert.not_matches("ssl_protocols", kong_nginx_conf) - assert.not_matches("ssl_certificate_by_lua_block", kong_nginx_conf) - end) - it("should return an error if the files do not exist", function() - local conf = pl_tablex.deepcopy(custom_conf_other_ssl) - conf.ssl_cert = "/hello.crt" - local _, err = nginx_conf_compiler.compile_kong_conf(conf) - assert.equal("Can't find SSL certificate at: /hello.crt", err) - - conf = pl_tablex.deepcopy(custom_conf_other_ssl) - conf.ssl_cert_key = "/hello.key" - local _, err = nginx_conf_compiler.compile_kong_conf(conf) - assert.equal("Can't find SSL key at: /hello.key", err) - end) - end) - - describe("DNS", function() - local custom_conf_dnsmasq, custom_conf_resolver - setup(function() - custom_conf_dnsmasq = assert(conf_loader(helpers.test_conf_path, { - dnsmasq = true, - dns_resolver = "" - })) - custom_conf_resolver = assert(conf_loader(helpers.test_conf_path, { - dnsmasq = false, - dns_resolver = "8.8.8.8:43" - })) - end) - - it("compiles with dnsmasq", function() - local kong_nginx_conf = nginx_conf_compiler.compile_kong_conf(custom_conf_dnsmasq) - assert.matches("resolver 127.0.0.1:9053 ipv6=off;", kong_nginx_conf) - end) - it("compiles with dnsmasq and a custom port", function() - local conf = pl_tablex.deepcopy(custom_conf_dnsmasq) - conf.dnsmasq_port = 4000 - local kong_nginx_conf = nginx_conf_compiler.compile_kong_conf(conf) - assert.matches("resolver 127.0.0.1:4000 ipv6=off;", kong_nginx_conf) - end) - it("compiles with custom resolver", function() - local kong_nginx_conf = nginx_conf_compiler.compile_kong_conf(custom_conf_resolver) - assert.matches("resolver 8.8.8.8:43 ipv6=off;", kong_nginx_conf) - end) - end) end) describe("compile_nginx_conf()", function() @@ -168,12 +92,15 @@ describe("NGINX conf compiler", function() after_each(function() pl_dir.rmtree(prefix) end) - it("checks nginx_prefix exists", function() + it("auto-creates inexisted prefix", function() local ok, err = nginx_conf_compiler.prepare_prefix(helpers.test_conf, "./inexistent") - assert.equal("./inexistent does not exist", err) - assert.is_nil(ok) + assert.True(ok) + assert.is_nil(err) + finally(function() + helpers.dir.rmtree("inexistent") + end) end) - it("checks nginx_prefix is a directory", function() + it("checks prefix is a directory", function() local tmp = os.tmpname() finally(function() assert(os.remove(tmp)) @@ -182,14 +109,12 @@ describe("NGINX conf compiler", function() assert.equal(tmp.." is not a directory", err) assert.is_nil(ok) end) - it("creates NGINX conf, log files and default SSL certs", function() + it("creates NGINX conf and log files", function() assert(nginx_conf_compiler.prepare_prefix(helpers.test_conf, prefix)) assert.truthy(exists(join(prefix, "nginx.conf"))) assert.truthy(exists(join(prefix, "nginx-kong.conf"))) assert.truthy(exists(join(prefix, "logs", "error.log"))) assert.truthy(exists(join(prefix, "logs", "access.log"))) - assert.truthy(exists(join(prefix, "ssl", "kong-default.crt"))) - assert.truthy(exists(join(prefix, "ssl", "kong-default.key"))) end) end) end) diff --git a/spec/02-integration/01-cmd/02-start_stop_spec.lua b/spec/02-integration/01-cmd/02-start_stop_spec.lua index d61bad05c96a..b505712dc053 100644 --- a/spec/02-integration/01-cmd/02-start_stop_spec.lua +++ b/spec/02-integration/01-cmd/02-start_stop_spec.lua @@ -108,7 +108,7 @@ describe("kong start/stop", function() end) end) - describe("#only dnsmasq", function() + describe("dnsmasq", function() it("starts dnsmasq daemon", function() local ok = exec("start --conf "..helpers.test_conf_path) assert.True(ok) @@ -147,9 +147,13 @@ describe("kong start/stop", function() end) it("start inexistent prefix", function() local ok, _, stdout, stderr = exec "start --prefix foobar" - assert.False(ok) - assert.equal("", stdout) - assert.matches("foobar does not exist", stderr, nil, true) + assert.True(ok) + assert.not_equal("", stdout) + assert.equal("", stderr) + finally(function() + helpers.execute(KILL_ALL) + helpers.dir.rmtree("foobar") + end) end) it("stop inexistent prefix", function() assert(helpers.dir.makepath(helpers.test_conf.prefix)) diff --git a/spec/02-integration/01-cmd/06-check_spec.lua b/spec/02-integration/01-cmd/06-check_spec.lua index a82b81402dcc..3a582e8298e4 100644 --- a/spec/02-integration/01-cmd/06-check_spec.lua +++ b/spec/02-integration/01-cmd/06-check_spec.lua @@ -19,9 +19,9 @@ describe("kong check", function() assert.False(ok) assert.equal("", stdout) assert.matches("[error] cassandra_repl_strategy has", stderr, nil, true) - assert.matches("[error] ssl_cert required", stderr, nil, true) + assert.matches("[error] when specifying a custom DNS resolver you must turn off dnsmasq", stderr, nil, true) end) - it("doesn't like invaldi files", function() + it("doesn't like invalid files", function() local ok, _, stdout, stderr = exec("check inexistent.conf") assert.False(ok) assert.equal("", stdout) diff --git a/spec/02-integration/05-proxy/01-resolver_spec.lua b/spec/02-integration/05-proxy/01-resolver_spec.lua index 6368d259d665..cacaf9bcb1a1 100644 --- a/spec/02-integration/05-proxy/01-resolver_spec.lua +++ b/spec/02-integration/05-proxy/01-resolver_spec.lua @@ -288,7 +288,7 @@ describe("Resolver", function() describe("SSL", function() local ssl_client setup(function() - ssl_client = assert(helpers.http_client("127.0.0.1", helpers.ssl_proxy_port)) + ssl_client = assert(helpers.http_client("127.0.0.1", helpers.test_conf.proxy_ssl_port)) assert(ssl_client:ssl_handshake(false)) end) teardown(function() diff --git a/spec/03-plugins/oauth2/api_old.lua b/spec/03-plugins/oauth2/api_old.lua index da86bb6e492e..169642b1e25a 100644 --- a/spec/03-plugins/oauth2/api_old.lua +++ b/spec/03-plugins/oauth2/api_old.lua @@ -167,7 +167,7 @@ describe("OAuth 2 Credentials API", function() end) describe("PUT", function() - it("#only [SUCCESS] should create a oauth2 token", function() + it("[SUCCESS] should create a oauth2 token", function() local response, status = http_client.put(BASE_URL, {credential_id = credential.id, expires_in = 10}) assert.equal(201, status) token = json.decode(response) diff --git a/spec/fixtures/invalid.conf b/spec/fixtures/invalid.conf index a41ff70139e1..cf901c57be05 100644 --- a/spec/fixtures/invalid.conf +++ b/spec/fixtures/invalid.conf @@ -1,2 +1,3 @@ -ssl = on +dnsmasq = on +dns_resolver = "8.8.8.8" cassandra_repl_strategy = foo From d5b55aa0ec853926e68fac54a5a54386bf60e9d5 Mon Sep 17 00:00:00 2001 From: thefosk Date: Thu, 9 Jun 2016 20:28:54 -0700 Subject: [PATCH 04/29] fixing messages --- kong/cmd/utils/ssl.lua | 6 +++--- kong/serf.lua | 2 +- spec/01-unit/01-conf/02-conf_compilation_spec.lua | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/kong/cmd/utils/ssl.lua b/kong/cmd/utils/ssl.lua index 96a087331b04..3bdf76db0125 100644 --- a/kong/cmd/utils/ssl.lua +++ b/kong/cmd/utils/ssl.lua @@ -24,10 +24,10 @@ function _M.get_ssl_cert_and_key(kong_config, nginx_prefix) -- Check that the files exist if not pl_path.exists(ssl_cert) then - return false, "Can't find SSL certificate at: "..ssl_cert + return nil, "cannot find SSL certificate at: "..ssl_cert end if not pl_path.exists(ssl_cert_key) then - return false, "Can't find SSL key at: "..ssl_cert_key + return nil, "cannot find SSL key at: "..ssl_cert_key end return { ssl_cert = ssl_cert, ssl_cert_key = ssl_cert_key } @@ -60,7 +60,7 @@ function _M.prepare_ssl_cert_and_key(prefix) for _, cmd in ipairs(commands) do local ok, _, _, stderr = pl_utils.executeex(cmd) if not ok then - return nil, "There was an error when auto-generating the default SSL certificate: "..stderr + return nil, "there was an error when auto-generating the default SSL certificate: "..stderr end end end diff --git a/kong/serf.lua b/kong/serf.lua index 1d1e7d495b8c..d203253118b3 100644 --- a/kong/serf.lua +++ b/kong/serf.lua @@ -68,7 +68,7 @@ function Serf:autojoin() local nodes, err = self.dao.nodes:find_all() if err then return nil, tostring(err) elseif #nodes == 0 then - log.warn("could not auto-join the cluster: no nodes found") + log.info("No other Kong nodes were found in the cluster") else -- Sort by newest to oldest (although by TTL would be a better sort) table.sort(nodes, function(a, b) return a.created_at > b.created_at end) diff --git a/spec/01-unit/01-conf/02-conf_compilation_spec.lua b/spec/01-unit/01-conf/02-conf_compilation_spec.lua index 4465212699f6..eafdbb61aa4a 100644 --- a/spec/01-unit/01-conf/02-conf_compilation_spec.lua +++ b/spec/01-unit/01-conf/02-conf_compilation_spec.lua @@ -92,7 +92,7 @@ describe("NGINX conf compiler", function() after_each(function() pl_dir.rmtree(prefix) end) - it("auto-creates inexisted prefix", function() + it("auto-creates inexistent prefix", function() local ok, err = nginx_conf_compiler.prepare_prefix(helpers.test_conf, "./inexistent") assert.True(ok) assert.is_nil(err) From 161ec6829f08cb9a602cc68ae69872050c35cd13 Mon Sep 17 00:00:00 2001 From: thefosk Date: Thu, 9 Jun 2016 20:33:06 -0700 Subject: [PATCH 05/29] better test --- spec/01-unit/01-conf/02-conf_compilation_spec.lua | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/spec/01-unit/01-conf/02-conf_compilation_spec.lua b/spec/01-unit/01-conf/02-conf_compilation_spec.lua index eafdbb61aa4a..62c62b06626f 100644 --- a/spec/01-unit/01-conf/02-conf_compilation_spec.lua +++ b/spec/01-unit/01-conf/02-conf_compilation_spec.lua @@ -93,9 +93,7 @@ describe("NGINX conf compiler", function() pl_dir.rmtree(prefix) end) it("auto-creates inexistent prefix", function() - local ok, err = nginx_conf_compiler.prepare_prefix(helpers.test_conf, "./inexistent") - assert.True(ok) - assert.is_nil(err) + assert(nginx_conf_compiler.prepare_prefix(helpers.test_conf, "./inexistent")) finally(function() helpers.dir.rmtree("inexistent") end) From 11e41e118041659773e00ba7e132b2fa2aa03fd3 Mon Sep 17 00:00:00 2001 From: thefosk Date: Fri, 10 Jun 2016 15:36:59 -0700 Subject: [PATCH 06/29] Disabling dnsmasq for tests --- kong/cmd/utils/dnsmasq_signals.lua | 40 ++++++++++--------- .../01-cmd/02-start_stop_spec.lua | 14 +++++-- spec/fixtures/invalid.conf | 2 +- spec/kong_tests.conf | 4 +- 4 files changed, 35 insertions(+), 25 deletions(-) diff --git a/kong/cmd/utils/dnsmasq_signals.lua b/kong/cmd/utils/dnsmasq_signals.lua index c0a965464958..84a77a5b6854 100644 --- a/kong/cmd/utils/dnsmasq_signals.lua +++ b/kong/cmd/utils/dnsmasq_signals.lua @@ -48,34 +48,38 @@ local function is_running(pid_path) end function _M.start(kong_config, nginx_prefix) - -- is dnsmasq already running in this prefix? - local pid_path = pl_path.join(nginx_prefix, dnsmasq_pid_name) - if is_running(pid_path) then - log.verbose("dnsmasq already running at %s", pid_path) - return true - else - log.verbose("dnsmasq not running, deleting %s", pid_path) - pl_file.delete(pid_path) - end + if kong_config.dnsmasq then + -- is dnsmasq already running in this prefix? + local pid_path = pl_path.join(nginx_prefix, dnsmasq_pid_name) + if is_running(pid_path) then + log.verbose("dnsmasq already running at %s", pid_path) + return true + else + log.verbose("dnsmasq not running, deleting %s", pid_path) + pl_file.delete(pid_path) + end - -- make sure Serf is in PATH - local dnsmasq_bin, err = _M.find_bin() - if not dnsmasq_bin then return nil, err end + local dnsmasq_bin, err = _M.find_bin() + if not dnsmasq_bin then return nil, err end - local cmd = fmt("%s -p %d --pid-file=%s -N -o --listen-address=127.0.0.1", dnsmasq_bin, kong_config.dnsmasq_port, pid_path) + local cmd = fmt("%s -p %d --pid-file=%s -N -o --listen-address=127.0.0.1", dnsmasq_bin, kong_config.dnsmasq_port, pid_path) - log.debug("starting dnsmasq: %s", cmd) + log.debug("starting dnsmasq: %s", cmd) - local ok, _, _, stderr = pl_utils.executeex(cmd) - if not ok then return nil, stderr end + local ok, _, _, stderr = pl_utils.executeex(cmd) + if not ok then return nil, stderr end + end return true end function _M.stop(nginx_prefix) local pid_path = pl_path.join(nginx_prefix, dnsmasq_pid_name) - log.verbose("stopping dnsmasq at %s", pid_path) - return kill(pid_path, "-9") + if pl_path.exists(pid_path) then + log.verbose("stopping dnsmasq at %s", pid_path) + return kill(pid_path, "-9") + end + return true end return _M \ No newline at end of file diff --git a/spec/02-integration/01-cmd/02-start_stop_spec.lua b/spec/02-integration/01-cmd/02-start_stop_spec.lua index b505712dc053..71a6882e290f 100644 --- a/spec/02-integration/01-cmd/02-start_stop_spec.lua +++ b/spec/02-integration/01-cmd/02-start_stop_spec.lua @@ -2,9 +2,15 @@ local helpers = require "spec.helpers" local KILL_ALL = "pkill nginx; pkill serf; pkill dnsmasq" -local function exec(args) +local function exec(args, env) args = args or "" - return helpers.execute(helpers.bin_path.." "..args) + env = env or {} + + local env_vars = "" + for k, v in pairs(env) do + env_vars = string.format("%s KONG_%s=%s", env_vars, k:upper(), v) + end + return helpers.execute(env_vars.." "..helpers.bin_path.." "..args) end describe("kong start/stop", function() @@ -110,7 +116,7 @@ describe("kong start/stop", function() describe("dnsmasq", function() it("starts dnsmasq daemon", function() - local ok = exec("start --conf "..helpers.test_conf_path) + local ok = exec("start --conf "..helpers.test_conf_path, {dnsmasq=true, dns_resolver = ""}) assert.True(ok) local dnsmasq_pid_path = helpers.path.join(helpers.test_conf.prefix, "dnsmasq.pid") @@ -126,7 +132,7 @@ describe("kong start/stop", function() local ok = helpers.execute("touch "..dnsmasq_pid_path) -- dumb pid assert.True(ok) - assert.True(exec("start --conf "..helpers.test_conf_path)) + assert.True(exec("start --conf "..helpers.test_conf_path, {dnsmasq=true, dns_resolver = ""})) local cmd = string.format("kill -0 `cat %s` >/dev/null 2>&1", dnsmasq_pid_path) local ok, code = helpers.execute(cmd) diff --git a/spec/fixtures/invalid.conf b/spec/fixtures/invalid.conf index cf901c57be05..6e1423a79f9f 100644 --- a/spec/fixtures/invalid.conf +++ b/spec/fixtures/invalid.conf @@ -1,3 +1,3 @@ dnsmasq = on -dns_resolver = "8.8.8.8" +dns_resolver = 8.8.8.8 cassandra_repl_strategy = foo diff --git a/spec/kong_tests.conf b/spec/kong_tests.conf index e829f9d933fd..9de0383b8e0c 100644 --- a/spec/kong_tests.conf +++ b/spec/kong_tests.conf @@ -6,8 +6,8 @@ cluster_listen = 0.0.0.0:9946 cluster_listen_rpc = 127.0.0.1:9373 ssl_cert = spec/fixtures/kong_spec.crt ssl_cert_key = spec/fixtures/kong_spec.key -dnsmasq = on -dnsmasq_port = 9053 +dnsmasq = off +dns_resolver = 8.8.8.8 database = postgres pg_host = 127.0.0.1 pg_port = 5432 From 2b8b04df0e3f574c26c47a9a476aa70911585469 Mon Sep 17 00:00:00 2001 From: thefosk Date: Mon, 13 Jun 2016 16:34:49 -0700 Subject: [PATCH 07/29] Adding missing serf commands and cluster CLI --- kong/cmd/cluster.lua | 54 ++++++++++++++++++++++++++ kong/cmd/init.lua | 1 + kong/cmd/stop.lua | 6 ++- kong/cmd/utils/nginx_conf_compiler.lua | 1 + kong/cmd/utils/serf_signals.lua | 41 +++++++++++++++---- kong/kong.lua | 2 +- kong/serf.lua | 43 ++++++++++++++++---- 7 files changed, 130 insertions(+), 18 deletions(-) create mode 100644 kong/cmd/cluster.lua diff --git a/kong/cmd/cluster.lua b/kong/cmd/cluster.lua new file mode 100644 index 000000000000..50e2775cdf72 --- /dev/null +++ b/kong/cmd/cluster.lua @@ -0,0 +1,54 @@ +local pl_app = require "pl.lapp" +local conf_loader = require "kong.conf_loader" +local DAOFactory = require "kong.dao.factory" +local Serf = require "kong.serf" +local log = require "kong.cmd.utils.log" +local fmt = string.format + +local function execute(args) + local conf = assert(conf_loader(args.conf, { + prefix = args.prefix + })) + + local dao = DAOFactory(conf) + local serf = Serf.new(conf, conf.prefix, dao) + + if args.command == "members" then + local members = assert(serf:members(true)) + for _, v in ipairs(members) do + print(fmt("%s\t%s\t%s", v.name, v.addr, v.status)) + end + elseif args.command == "keygen" then + print(assert(serf:keygen())) + elseif args.command == "reachability" then + log("Please wait..") + print(assert(serf:reachability())) + elseif args.command == "force-leave" then + local node_name = args[1] + if not node_name then + pl_app.quit("You need to specify the node name to leave") + end + log(fmt("Force-leaving %s", node_name)) + assert(serf:force_leave(node_name)) + log("Done") + end +end + +local lapp = [[ +Usage: kong cluster COMMAND [OPTIONS] + +The available commands are: + members + force-leave + keygen + reachability + +Options: + -c,--conf (optional string) configuration file +]] + +return { + lapp = lapp, + execute = execute, + sub_commands = {members = true, keygen = true, reachability = true, ["force-leave"] = true} +} diff --git a/kong/cmd/init.lua b/kong/cmd/init.lua index 10a09db1a5ae..45f4ebf90051 100644 --- a/kong/cmd/init.lua +++ b/kong/cmd/init.lua @@ -29,6 +29,7 @@ local cmds = { check = "check", compile = "compile", migrations = "migrations", + cluster = "cluster", version = "version", roar = "roar" } diff --git a/kong/cmd/stop.lua b/kong/cmd/stop.lua index bb3d81385f34..dba614e1c748 100644 --- a/kong/cmd/stop.lua +++ b/kong/cmd/stop.lua @@ -2,6 +2,7 @@ local nginx_signals = require "kong.cmd.utils.nginx_signals" local serf_signals = require "kong.cmd.utils.serf_signals" local dnsmasq_signals = require "kong.cmd.utils.dnsmasq_signals" local conf_loader = require "kong.conf_loader" +local DAOFactory = require "kong.dao.factory" local log = require "kong.cmd.utils.log" local function execute(args) @@ -11,9 +12,10 @@ local function execute(args) prefix = args.prefix })) + local dao = DAOFactory(conf) + assert(nginx_signals.stop(conf.prefix)) - assert(serf_signals.stop(conf.prefix)) - assert(serf_signals.stop(conf.prefix)) + assert(serf_signals.stop(conf, conf.prefix, dao)) assert(dnsmasq_signals.stop(conf.prefix)) log("Stopped") end diff --git a/kong/cmd/utils/nginx_conf_compiler.lua b/kong/cmd/utils/nginx_conf_compiler.lua index 6be44df94260..4fe2551f9a09 100644 --- a/kong/cmd/utils/nginx_conf_compiler.lua +++ b/kong/cmd/utils/nginx_conf_compiler.lua @@ -1,4 +1,5 @@ local NGINX_VARS = { + prefix = true, plugins = true, cluster_listen = true, cluster_listen_rpc = true, diff --git a/kong/cmd/utils/serf_signals.lua b/kong/cmd/utils/serf_signals.lua index c1f088428d88..426e5353b7b3 100644 --- a/kong/cmd/utils/serf_signals.lua +++ b/kong/cmd/utils/serf_signals.lua @@ -10,10 +10,12 @@ local pl_path = require "pl.path" local pl_file = require "pl.file" local kill = require "kong.cmd.utils.kill" local log = require "kong.cmd.utils.log" +local utils = require "kong.tools.utils" local fmt = string.format local serf_bin_name = "serf" local serf_pid_name = "serf.pid" +local serf_node_id = "serf.id" local serf_event_name = "kong" local start_timeout = 2 @@ -55,11 +57,27 @@ client:request { \ resty -e "$CMD" ]] +local function prepare_identifier(kong_config, nginx_prefix) + local id_path = pl_path.join(nginx_prefix, serf_node_id) + if not pl_path.exists(id_path) then + local id = utils.get_hostname().."_"..kong_config.cluster_listen.."_"..utils.random_string() + + log.verbose("saving Serf identifier in %s", id_path) + local ok, err = pl_file.write(id_path, id) + if not ok then return nil, err end + end + return true +end + local function prepare_prefix(kong_config, nginx_prefix, script_path) + local ok, err = prepare_identifier(kong_config, nginx_prefix) + if not ok then return nil, err end + log.verbose("dumping Serf shell script handler in %s", script_path) local script = fmt(script_template, "127.0.0.1", kong_config.admin_port) - pl_file.write(script_path, script) + local ok, err = pl_file.write(script_path, script) + if not ok then return nil, err end local ok, _, _, stderr = pl_utils.executeex("chmod +x "..script_path) if not ok then return nil, stderr end @@ -85,20 +103,20 @@ function _M.start(kong_config, nginx_prefix, dao) pl_file.delete(pid_path) end + -- prepare shell script + local script_path = pl_path.join(nginx_prefix, "serf_event.sh") + local ok, err = prepare_prefix(kong_config, nginx_prefix, script_path) + if not ok then return nil, err end + -- make sure Serf is in PATH local ok, err = check_serf_bin() if not ok then return nil, err end - local serf = Serf.new(kong_config, dao) + local serf = Serf.new(kong_config, nginx_prefix, dao) local node_name = serf.node_name - local script_path = pl_path.join(nginx_prefix, "serf_event.sh") local log_path = pl_path.join(nginx_prefix, "serf.log") - -- prepare shell script - local ok, err = prepare_prefix(kong_config, nginx_prefix, script_path) - if not ok then return nil, err end - local args = setmetatable({ ["-bind"] = kong_config.cluster_listen, ["-rpc-addr"] = kong_config.cluster_listen_rpc, @@ -152,7 +170,14 @@ function _M.start(kong_config, nginx_prefix, dao) return true end -function _M.stop(nginx_prefix) +function _M.stop(kong_config, nginx_prefix, dao) + log.info("Leaving cluster") + + local serf = Serf.new(kong_config, nginx_prefix, dao) + + local ok, err = serf:leave() + if not ok then return nil, err end + local pid_path = pl_path.join(nginx_prefix, serf_pid_name) log.verbose("stopping Serf agent at %s", pid_path) return kill(pid_path, "-9") diff --git a/kong/kong.lua b/kong/kong.lua index 1bc37b486cad..7ef5c2097595 100644 --- a/kong/kong.lua +++ b/kong/kong.lua @@ -126,7 +126,7 @@ function Kong.init(config) -- populate singletons singletons.loaded_plugins = assert(load_plugins(config, dao, events)) - singletons.serf = Serf.new(config, dao) + singletons.serf = Serf.new(config, config.prefix, dao) singletons.dao = dao singletons.events = events singletons.configuration = config diff --git a/kong/serf.lua b/kong/serf.lua index d203253118b3..4483adbebc4b 100644 --- a/kong/serf.lua +++ b/kong/serf.lua @@ -3,13 +3,13 @@ local pl_stringx = require "pl.stringx" local pl_utils = require "pl.utils" +local pl_path = require "pl.path" +local pl_file = require "pl.file" local cjson = require "cjson.safe" local log = require "kong.cmd.utils.log" local fmt = string.format -local ok, _, stdout, stderr = pl_utils.executeex "/bin/hostname" -if not ok then error(stderr) end -local hostname = pl_stringx.strip(stdout) +local serf_node_id = "serf.id" local Serf = {} Serf.__index = Serf @@ -22,9 +22,9 @@ Serf.args_mt = { end } -function Serf.new(kong_config, dao) +function Serf.new(kong_config, nginx_prefix, dao) return setmetatable({ - node_name = hostname.."_"..kong_config.cluster_listen, + node_name = assert(pl_file.read(pl_path.join(nginx_prefix, serf_node_id))), config = kong_config, dao = dao }, Serf) @@ -40,7 +40,7 @@ function Serf:invoke_signal(signal, args, no_rpc) local rpc = no_rpc and "" or "-rpc-addr="..self.config.cluster_listen_rpc local cmd = fmt("serf %s %s %s", signal, rpc, tostring(args)) local ok, code, stdout = pl_utils.executeex(cmd) - if not ok or code ~= 0 then return nil, stdout end + if not ok or code ~= 0 then return nil, pl_stringx.splitlines(stdout)[1] end -- always print the first error line of serf return stdout end @@ -49,6 +49,23 @@ function Serf:join_node(address) return select(2, self:invoke_signal("join", address)) == nil end +function Serf:leave() + local res, err = self:invoke_signal("leave") + if not res then return nil, err end + + local _, err = self.dao.nodes:delete {name = self.node_name} + if err then return nil, err end + + return true +end + +function Serf:force_leave(node_name) + local res, err = self:invoke_signal("force-leave", node_name) + if not res then return nil, err end + + return true +end + function Serf:members() local res, err = self:invoke_signal("members", {["-format"] = "json"}) if not res then return nil, err end @@ -59,6 +76,18 @@ function Serf:members() return json.members end +function Serf:keygen() + local res, err = self:invoke_signal("keygen") + if not res then return nil, err end + return res +end + +function Serf:reachability() + local res, err = self:invoke_signal("reachability") + if not res then return nil, err end + return res +end + function Serf:autojoin() -- Delete current node just in case it was there -- (due to an inconsistency caused by a crash) @@ -110,7 +139,7 @@ function Serf:add_node() local _, err = self.dao.nodes:insert({ name = self.node_name, cluster_listening_address = pl_stringx.strip(addr) - }, {ttl = 3600}) + }, {ttl = self.config.cluster_ttl_on_failure}) if err then return nil, tostring(err) end return true From 9e41cf985f8ebac9f10cced31227ef9e3abc0ee5 Mon Sep 17 00:00:00 2001 From: thefosk Date: Mon, 13 Jun 2016 20:45:14 -0700 Subject: [PATCH 08/29] Continuing serf implementation --- kong-0.8.2-0.rockspec | 1 + kong/cmd/cluster.lua | 4 +- kong/cmd/stop.lua | 5 +- kong/serf.lua | 24 +++++--- .../01-cmd/02-start_stop_spec.lua | 12 ++-- .../02-integration/01-cmd/07-cluster_spec.lua | 56 +++++++++++++++++++ spec/helpers.lua | 2 +- 7 files changed, 84 insertions(+), 20 deletions(-) create mode 100644 spec/02-integration/01-cmd/07-cluster_spec.lua diff --git a/kong-0.8.2-0.rockspec b/kong-0.8.2-0.rockspec index 831eed6415c9..d4d464fc0370 100644 --- a/kong-0.8.2-0.rockspec +++ b/kong-0.8.2-0.rockspec @@ -58,6 +58,7 @@ build = { ["kong.cmd.compile"] = "kong/cmd/compile.lua", ["kong.cmd.init"] = "kong/cmd/init.lua", ["kong.cmd.migrations"] = "kong/cmd/migrations.lua", + ["kong.cmd.cluster"] = "kong/cmd/cluster.lua", ["kong.cmd.reload"] = "kong/cmd/reload.lua", ["kong.cmd.roar"] = "kong/cmd/roar.lua", ["kong.cmd.start"] = "kong/cmd/start.lua", diff --git a/kong/cmd/cluster.lua b/kong/cmd/cluster.lua index 50e2775cdf72..3437b9ad4539 100644 --- a/kong/cmd/cluster.lua +++ b/kong/cmd/cluster.lua @@ -1,4 +1,3 @@ -local pl_app = require "pl.lapp" local conf_loader = require "kong.conf_loader" local DAOFactory = require "kong.dao.factory" local Serf = require "kong.serf" @@ -26,7 +25,7 @@ local function execute(args) elseif args.command == "force-leave" then local node_name = args[1] if not node_name then - pl_app.quit("You need to specify the node name to leave") + error("you need to specify the node name to leave") end log(fmt("Force-leaving %s", node_name)) assert(serf:force_leave(node_name)) @@ -45,6 +44,7 @@ The available commands are: Options: -c,--conf (optional string) configuration file + --prefix (optional string) Nginx prefix path ]] return { diff --git a/kong/cmd/stop.lua b/kong/cmd/stop.lua index dba614e1c748..743a760cc632 100644 --- a/kong/cmd/stop.lua +++ b/kong/cmd/stop.lua @@ -6,9 +6,7 @@ local DAOFactory = require "kong.dao.factory" local log = require "kong.cmd.utils.log" local function execute(args) - -- no conf file loaded, we just want the prefix, - -- potentially overriden by the argument - local conf = assert(conf_loader(nil, { + local conf = assert(conf_loader(args.conf, { prefix = args.prefix })) @@ -24,6 +22,7 @@ local lapp = [[ Usage: kong stop [OPTIONS] Options: + -c,--conf (optional string) configuration file --prefix (optional string) Nginx prefix path ]] diff --git a/kong/serf.lua b/kong/serf.lua index 4483adbebc4b..fe57f1a5bc2f 100644 --- a/kong/serf.lua +++ b/kong/serf.lua @@ -42,7 +42,7 @@ function Serf:invoke_signal(signal, args, no_rpc) local ok, code, stdout = pl_utils.executeex(cmd) if not ok or code ~= 0 then return nil, pl_stringx.splitlines(stdout)[1] end -- always print the first error line of serf - return stdout + return pl_stringx.strip(stdout) -- serf adds a new line on the result, so we strip it end function Serf:join_node(address) @@ -77,15 +77,11 @@ function Serf:members() end function Serf:keygen() - local res, err = self:invoke_signal("keygen") - if not res then return nil, err end - return res + return self:invoke_signal("keygen") end function Serf:reachability() - local res, err = self:invoke_signal("reachability") - if not res then return nil, err end - return res + return self:invoke_signal("reachability") end function Serf:autojoin() @@ -105,7 +101,19 @@ function Serf:autojoin() local joined for _, v in ipairs(nodes) do if self:join_node(v.cluster_listening_address) then - log("Successfully auto-joined %s", v.cluster_listening_address) + log("Successfully auto-joined cluster through %s", v.cluster_listening_address) + local members = assert(self:members()) + local active, left, failed = 0, 0, 0 + for _, v in ipairs(members) do + if v.status == "alive" then + active = active + 1 + elseif v.status == "left" then + left = left + 1 + elseif v.status == "failed" then + failed = failed + 1 + end + end + log(fmt("The Kong cluster now has %d nodes (%d active, %d left, %d failed)", #members, active, left, failed)) joined = true break else diff --git a/spec/02-integration/01-cmd/02-start_stop_spec.lua b/spec/02-integration/01-cmd/02-start_stop_spec.lua index 71a6882e290f..5decf6ddbced 100644 --- a/spec/02-integration/01-cmd/02-start_stop_spec.lua +++ b/spec/02-integration/01-cmd/02-start_stop_spec.lua @@ -54,7 +54,7 @@ describe("kong start/stop", function() assert.not_equal("", stdout) assert.equal("", stderr) - ok, _, stdout, stderr = exec("stop --prefix "..helpers.test_conf.prefix) + ok, _, stdout, stderr = exec("stop --conf "..helpers.test_conf_path) assert.True(ok) assert.not_equal("", stdout) assert.equal("", stderr) @@ -96,7 +96,7 @@ describe("kong start/stop", function() assert.True(ok) assert.equal(0, code) - assert.True(exec("stop --prefix "..helpers.test_conf.prefix)) + assert.True(exec("stop --conf "..helpers.test_conf_path)) end) it("recovers from expired serf.pid file", function() local serf_pid_path = helpers.path.join(helpers.test_conf.prefix, "serf.pid") @@ -110,7 +110,7 @@ describe("kong start/stop", function() assert.True(ok) assert.equal(0, code) - assert.True(exec("stop --prefix "..helpers.test_conf.prefix)) + assert.True(exec("stop --conf "..helpers.test_conf_path)) end) end) @@ -125,7 +125,7 @@ describe("kong start/stop", function() assert.True(ok) assert.equal(0, code) - assert.True(exec("stop --prefix "..helpers.test_conf.prefix)) + assert.True(exec("stop --conf "..helpers.test_conf_path)) end) it("recovers from expired dnsmasq.pid file", function() local dnsmasq_pid_path = helpers.path.join(helpers.test_conf.prefix, "dnsmasq.pid") @@ -139,7 +139,7 @@ describe("kong start/stop", function() assert.True(ok) assert.equal(0, code) - assert.True(exec("stop --prefix "..helpers.test_conf.prefix)) + assert.True(exec("stop --conf "..helpers.test_conf_path)) end) end) @@ -169,7 +169,7 @@ describe("kong start/stop", function() assert.not_equal("", stdout) assert.equal("", stderr) - ok, _, stdout, stderr = exec "stop --prefix inexistent" + ok, _, stdout, stderr = exec("stop --prefix inexistent --conf "..helpers.test_conf_path) assert.False(ok) assert.equal("", stdout) assert.matches("Error: could not get Nginx pid", stderr, nil, true) diff --git a/spec/02-integration/01-cmd/07-cluster_spec.lua b/spec/02-integration/01-cmd/07-cluster_spec.lua new file mode 100644 index 000000000000..243f79455db2 --- /dev/null +++ b/spec/02-integration/01-cmd/07-cluster_spec.lua @@ -0,0 +1,56 @@ +local helpers = require "spec.helpers" + +local KILL_ALL = "pkill nginx; pkill serf; pkill dnsmasq" + +local function exec(args) + args = args or "" + return helpers.execute(helpers.bin_path.." "..args.." --prefix "..helpers.test_conf.prefix.." -c "..helpers.test_conf_path) +end + +describe("kong cluster", function() + setup(function() + helpers.execute(KILL_ALL) + assert(helpers.start_kong()) + end) + teardown(function() + helpers.execute(KILL_ALL) + helpers.clean_prefix() + end) + + it("keygen", function() + local ok, _, stdout, stderr = exec "cluster keygen" + assert.True(ok) + assert.equal("", stderr) + assert.is_string(stdout) + assert.equal(25, string.len(stdout)) -- 25 = 24 for the key, 1 for the newline + end) + it("members", function() + local ok, _, stdout, stderr = exec "cluster members" + assert.True(ok) + assert.equal("", stderr) + assert.is_string(stdout) + assert.True(string.len(stdout) > 10) + end) + it("reachability", function() + local ok, _, stdout, stderr = exec "cluster reachability" + assert.True(ok) + assert.equal("", stderr) + assert.is_string(stdout) + assert.True(string.len(stdout) > 10) + end) + describe("#only force-leave", function() + it("should fail when no node is specified", function() + local ok, _, stdout, stderr = exec "cluster force-leave" + assert.False(ok) + assert.equal("", stdout) + assert.matches("Error: you need to specify the node name to leave", stderr, nil, true) + end) + it("should work when a node is specified", function() + local ok, _, stdout, stderr = exec "cluster force-leave some-node" + assert.True(ok) + assert.equal("", stderr) + assert.is_string(stdout) + assert.matches("Force-leaving some-node", stdout, nil, true) + end) + end) +end) diff --git a/spec/helpers.lua b/spec/helpers.lua index 69261e8c0485..be10ae77cd46 100644 --- a/spec/helpers.lua +++ b/spec/helpers.lua @@ -233,6 +233,6 @@ return { return kong_exec("start --conf "..TEST_CONF_PATH, prefix) end, stop_kong = function(prefix) - return kong_exec("stop ", prefix) + return kong_exec("stop --conf "..TEST_CONF_PATH, prefix) end } From fb3f2761e1d9c3696dd751f836c1470cc98f7464 Mon Sep 17 00:00:00 2001 From: thefosk Date: Tue, 14 Jun 2016 00:26:57 -0700 Subject: [PATCH 09/29] fixing log --- kong/serf.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kong/serf.lua b/kong/serf.lua index fe57f1a5bc2f..d1f45b70c153 100644 --- a/kong/serf.lua +++ b/kong/serf.lua @@ -113,7 +113,7 @@ function Serf:autojoin() failed = failed + 1 end end - log(fmt("The Kong cluster now has %d nodes (%d active, %d left, %d failed)", #members, active, left, failed)) + log("The Kong cluster now has %d nodes (%d active, %d left, %d failed)", #members, active, left, failed) joined = true break else From 771b2ecb0ef1a39ddb9a3ecc7b3bfbb7d7fe81f4 Mon Sep 17 00:00:00 2001 From: thefosk Date: Tue, 14 Jun 2016 21:39:18 -0700 Subject: [PATCH 10/29] Organizing pids, logs and serf files respectively in a "pids", "logs" and "serf" folders. --- kong/cmd/utils/dnsmasq_signals.lua | 4 ++-- kong/cmd/utils/nginx_conf_compiler.lua | 5 +++++ kong/cmd/utils/nginx_signals.lua | 2 +- kong/cmd/utils/serf_signals.lua | 15 ++++++++++----- kong/serf.lua | 2 +- kong/templates/nginx.lua | 1 + spec/02-integration/01-cmd/02-start_stop_spec.lua | 10 +++++----- 7 files changed, 25 insertions(+), 14 deletions(-) diff --git a/kong/cmd/utils/dnsmasq_signals.lua b/kong/cmd/utils/dnsmasq_signals.lua index 84a77a5b6854..4d50c965ffc6 100644 --- a/kong/cmd/utils/dnsmasq_signals.lua +++ b/kong/cmd/utils/dnsmasq_signals.lua @@ -50,7 +50,7 @@ end function _M.start(kong_config, nginx_prefix) if kong_config.dnsmasq then -- is dnsmasq already running in this prefix? - local pid_path = pl_path.join(nginx_prefix, dnsmasq_pid_name) + local pid_path = pl_path.join(nginx_prefix, "pids", dnsmasq_pid_name) if is_running(pid_path) then log.verbose("dnsmasq already running at %s", pid_path) return true @@ -74,7 +74,7 @@ function _M.start(kong_config, nginx_prefix) end function _M.stop(nginx_prefix) - local pid_path = pl_path.join(nginx_prefix, dnsmasq_pid_name) + local pid_path = pl_path.join(nginx_prefix, "pids", dnsmasq_pid_name) if pl_path.exists(pid_path) then log.verbose("stopping dnsmasq at %s", pid_path) return kill(pid_path, "-9") diff --git a/kong/cmd/utils/nginx_conf_compiler.lua b/kong/cmd/utils/nginx_conf_compiler.lua index 4fe2551f9a09..670ca477f29b 100644 --- a/kong/cmd/utils/nginx_conf_compiler.lua +++ b/kong/cmd/utils/nginx_conf_compiler.lua @@ -134,6 +134,11 @@ local function prepare_prefix(kong_config, nginx_prefix) local ok, _, _, stderr = touch(acc_logs_path) if not ok then return nil, stderr end + -- pids folder + local pids_path = pl_path.join(nginx_prefix, "pids") + local ok, err = pl_dir.makepath(pids_path) + if not ok then return nil, err end + -- auto-generate default SSL certificate local ok, err = ssl.prepare_ssl_cert_and_key(nginx_prefix) if not ok then return nil, err end diff --git a/kong/cmd/utils/nginx_signals.lua b/kong/cmd/utils/nginx_signals.lua index 6d5352567c25..b6cadff1d82f 100644 --- a/kong/cmd/utils/nginx_signals.lua +++ b/kong/cmd/utils/nginx_signals.lua @@ -21,7 +21,7 @@ local function is_openresty(bin_path) end local function get_pid_path(nginx_prefix) - local pid_path = pl_path.join(nginx_prefix, "logs", "nginx.pid") + local pid_path = pl_path.join(nginx_prefix, "pids", "nginx.pid") if pl_path.exists(pid_path) then return pid_path end diff --git a/kong/cmd/utils/serf_signals.lua b/kong/cmd/utils/serf_signals.lua index 426e5353b7b3..edd25f73cdd4 100644 --- a/kong/cmd/utils/serf_signals.lua +++ b/kong/cmd/utils/serf_signals.lua @@ -8,6 +8,7 @@ local pl_stringx = require "pl.stringx" local pl_utils = require "pl.utils" local pl_path = require "pl.path" local pl_file = require "pl.file" +local pl_dir = require "pl.dir" local kill = require "kong.cmd.utils.kill" local log = require "kong.cmd.utils.log" local utils = require "kong.tools.utils" @@ -58,7 +59,7 @@ resty -e "$CMD" ]] local function prepare_identifier(kong_config, nginx_prefix) - local id_path = pl_path.join(nginx_prefix, serf_node_id) + local id_path = pl_path.join(nginx_prefix, "serf", serf_node_id) if not pl_path.exists(id_path) then local id = utils.get_hostname().."_"..kong_config.cluster_listen.."_"..utils.random_string() @@ -70,6 +71,10 @@ local function prepare_identifier(kong_config, nginx_prefix) end local function prepare_prefix(kong_config, nginx_prefix, script_path) + local serf_path = pl_path.join(nginx_prefix, "serf") + local ok, err = pl_dir.makepath(serf_path) + if not ok then return nil, err end + local ok, err = prepare_identifier(kong_config, nginx_prefix) if not ok then return nil, err end @@ -94,7 +99,7 @@ local _M = {} function _M.start(kong_config, nginx_prefix, dao) -- is Serf already running in this prefix? - local pid_path = pl_path.join(nginx_prefix, serf_pid_name) + local pid_path = pl_path.join(nginx_prefix, "pids", serf_pid_name) if is_running(pid_path) then log.verbose("Serf agent already running at %s", pid_path) return true @@ -104,7 +109,7 @@ function _M.start(kong_config, nginx_prefix, dao) end -- prepare shell script - local script_path = pl_path.join(nginx_prefix, "serf_event.sh") + local script_path = pl_path.join(nginx_prefix, "serf", "serf_event.sh") local ok, err = prepare_prefix(kong_config, nginx_prefix, script_path) if not ok then return nil, err end @@ -115,7 +120,7 @@ function _M.start(kong_config, nginx_prefix, dao) local serf = Serf.new(kong_config, nginx_prefix, dao) local node_name = serf.node_name - local log_path = pl_path.join(nginx_prefix, "serf.log") + local log_path = pl_path.join(nginx_prefix, "logs", "serf.log") local args = setmetatable({ ["-bind"] = kong_config.cluster_listen, @@ -178,7 +183,7 @@ function _M.stop(kong_config, nginx_prefix, dao) local ok, err = serf:leave() if not ok then return nil, err end - local pid_path = pl_path.join(nginx_prefix, serf_pid_name) + local pid_path = pl_path.join(nginx_prefix, "pids", serf_pid_name) log.verbose("stopping Serf agent at %s", pid_path) return kill(pid_path, "-9") end diff --git a/kong/serf.lua b/kong/serf.lua index d1f45b70c153..7eb111ac6e1c 100644 --- a/kong/serf.lua +++ b/kong/serf.lua @@ -24,7 +24,7 @@ Serf.args_mt = { function Serf.new(kong_config, nginx_prefix, dao) return setmetatable({ - node_name = assert(pl_file.read(pl_path.join(nginx_prefix, serf_node_id))), + node_name = assert(pl_file.read(pl_path.join(nginx_prefix, "serf", serf_node_id))), config = kong_config, dao = dao }, Serf) diff --git a/kong/templates/nginx.lua b/kong/templates/nginx.lua index 00adc9a42c6e..1c490bdc958e 100644 --- a/kong/templates/nginx.lua +++ b/kong/templates/nginx.lua @@ -2,6 +2,7 @@ return [[ worker_processes ${{NGINX_WORKER_PROCESSES}}; daemon ${{NGINX_DAEMON}}; +pid pids/nginx.pid; error_log logs/error.log ${{LOG_LEVEL}}; events { diff --git a/spec/02-integration/01-cmd/02-start_stop_spec.lua b/spec/02-integration/01-cmd/02-start_stop_spec.lua index 5decf6ddbced..cd2361fe6d16 100644 --- a/spec/02-integration/01-cmd/02-start_stop_spec.lua +++ b/spec/02-integration/01-cmd/02-start_stop_spec.lua @@ -48,7 +48,7 @@ describe("kong start/stop", function() assert.equal("", stderr) assert.True(ok) end) - it("start/stop custom Kong conf/prefix", function() + it("#only start/stop custom Kong conf/prefix", function() local ok, _, stdout, stderr = exec("start --conf "..helpers.test_conf_path) assert.True(ok) assert.not_equal("", stdout) @@ -90,7 +90,7 @@ describe("kong start/stop", function() local ok = exec("start --conf "..helpers.test_conf_path) assert.True(ok) - local serf_pid_path = helpers.path.join(helpers.test_conf.prefix, "serf.pid") + local serf_pid_path = helpers.path.join(helpers.test_conf.prefix, "pids", "serf.pid") local cmd = string.format("kill -0 `cat %s` >/dev/null 2>&1", serf_pid_path) local ok, code = helpers.execute(cmd) assert.True(ok) @@ -99,7 +99,7 @@ describe("kong start/stop", function() assert.True(exec("stop --conf "..helpers.test_conf_path)) end) it("recovers from expired serf.pid file", function() - local serf_pid_path = helpers.path.join(helpers.test_conf.prefix, "serf.pid") + local serf_pid_path = helpers.path.join(helpers.test_conf.prefix, "pids", "serf.pid") local ok = helpers.execute("touch "..serf_pid_path) -- dumb pid assert.True(ok) @@ -119,7 +119,7 @@ describe("kong start/stop", function() local ok = exec("start --conf "..helpers.test_conf_path, {dnsmasq=true, dns_resolver = ""}) assert.True(ok) - local dnsmasq_pid_path = helpers.path.join(helpers.test_conf.prefix, "dnsmasq.pid") + local dnsmasq_pid_path = helpers.path.join(helpers.test_conf.prefix, "pids", "dnsmasq.pid") local cmd = string.format("kill -0 `cat %s` >/dev/null 2>&1", dnsmasq_pid_path) local ok, code = helpers.execute(cmd) assert.True(ok) @@ -128,7 +128,7 @@ describe("kong start/stop", function() assert.True(exec("stop --conf "..helpers.test_conf_path)) end) it("recovers from expired dnsmasq.pid file", function() - local dnsmasq_pid_path = helpers.path.join(helpers.test_conf.prefix, "dnsmasq.pid") + local dnsmasq_pid_path = helpers.path.join(helpers.test_conf.prefix, "pids", "dnsmasq.pid") local ok = helpers.execute("touch "..dnsmasq_pid_path) -- dumb pid assert.True(ok) From b377a57f91428635729e5b2d47e1b91b2d88e032 Mon Sep 17 00:00:00 2001 From: thefosk Date: Tue, 14 Jun 2016 21:59:36 -0700 Subject: [PATCH 11/29] log message --- kong/cmd/utils/nginx_conf_compiler.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kong/cmd/utils/nginx_conf_compiler.lua b/kong/cmd/utils/nginx_conf_compiler.lua index 670ca477f29b..a1b14f5c5477 100644 --- a/kong/cmd/utils/nginx_conf_compiler.lua +++ b/kong/cmd/utils/nginx_conf_compiler.lua @@ -110,7 +110,7 @@ local function touch(file_path) end local function prepare_prefix(kong_config, nginx_prefix) - log.verbose("preparing nginx prefix directory at %s", nginx_prefix) + log.verbose("preparing prefix directory at %s", nginx_prefix) if not pl_path.exists(nginx_prefix) then log.verbose(fmt("prefix directory %s not found, trying to create it", nginx_prefix)) From 01e50df29df455bc70ecb15bf8facdd5ef1202d2 Mon Sep 17 00:00:00 2001 From: thefosk Date: Wed, 15 Jun 2016 00:51:05 -0700 Subject: [PATCH 12/29] Starting to work on hooks test --- spec/02-integration/04-core/hooks_spec.lua | 174 +++++++++++++++++++++ spec/kong_tests.conf | 1 + 2 files changed, 175 insertions(+) create mode 100644 spec/02-integration/04-core/hooks_spec.lua diff --git a/spec/02-integration/04-core/hooks_spec.lua b/spec/02-integration/04-core/hooks_spec.lua new file mode 100644 index 000000000000..a0796172979b --- /dev/null +++ b/spec/02-integration/04-core/hooks_spec.lua @@ -0,0 +1,174 @@ +local helpers = require "spec.helpers" +local cjson = require "cjson" +local meta = require "kong.meta" +local cache = require "kong.tools.database_cache" + +local TIMEOUT = 10 + +describe("Core Hooks", function() + local client, api_client + local api2, basic_auth2 + setup(function() + helpers.dao:truncate_tables() + helpers.execute "pkill nginx; pkill serf; pkill dnsmasq" + assert(helpers.prepare_prefix()) + + local consumer = assert(helpers.dao.consumers:insert { + username = "consumer1" + }) + assert(helpers.dao.basicauth_credentials:insert { + username = "user123", + password = "pass123", + consumer_id = consumer.id + }) + + assert(helpers.dao.apis:insert { + request_host = "hooks1.com", + upstream_url = "http://mockbin.com" + }) + + api2 = assert(helpers.dao.apis:insert { + request_host = "hooks-consumer.com", + upstream_url = "http://mockbin.com" + }) + basic_auth2 = assert(helpers.dao.plugins:insert { + name = "basic-auth", + api_id = api2.id, + config = {} + }) + + local api3 = assert(helpers.dao.apis:insert { + request_host = "hooks-plugins.com", + upstream_url = "http://mockbin.com" + }) + assert(helpers.dao.plugins:insert { + name = "basic-auth", + api_id = api3.id, + config = {} + }) + assert(helpers.dao.plugins:insert { + name = "rate-limiting", + api_id = api3.id, + config = { + minute = 10 + } + }) + assert(helpers.dao.plugins:insert { + name = "rate-limiting", + api_id = api3.id, + consumer_id = consumer.id, + config = { + minute = 10 + } + }) + + assert(helpers.start_kong()) + client = assert(helpers.http_client("127.0.0.1", helpers.test_conf.proxy_port)) + api_client = assert(helpers.http_client("127.0.0.1", helpers.test_conf.admin_port)) + end) + teardown(function() + if client then + client:close() + end + if api_client then + api_client:close() + end + helpers.stop_kong() + --helpers.clean_prefix() + end) + + local function timeout_while(timeout, fn) + local start = os.time() + while(os.time() < (start + timeout)) do + if fn() then return end + end + error("Timeout") + end + + describe("Plugin entity invalidation", function() + it("#only should invalidate a plugin when deleting", function() + -- Making a request to populate the cache + local res = assert(client:send { + method = "GET", + path = "/status/200", + headers = { + ["Host"] = "hooks-consumer.com", + ["Authorization"] = "Basic dXNlcjEyMzpwYXNzMTIz" + } + }) + assert.res_status(200, res) + + -- Make sure the cache is populated + local res = assert(api_client:send { + method = "GET", + path = "/cache/"..cache.plugin_key("basic-auth", api2.id, nil), + headers = {} + }) + assert.res_status(200, res) + + -- Delete plugin + local res = assert(api_client:send { + method = "DELETE", + path = "/apis/"..api2.id.."/plugins/"..basic_auth2.id, + headers = {} + }) + assert.res_status(204, res) + + -- Wait for cache to be invalidated + timeout_while(TIMEOUT, function() + os.execute("sleep 0.2") -- Apparently api_client cannot be reused immediately + local res = assert(api_client:send { + method = "GET", + path = "/cache/"..cache.plugin_key("basic-auth", api2.id, nil), + headers = {} + }) + if res.status ~= 200 then + assert.res_status(404, res) + return true + end + end) + end) + + it("should invalidate a consumer-specific plugin when deleting", function() + error("TODO") + end) + + it("should invalidate a consumer-specific plugin when updating", function() + error("TODO") + end) + + it("should invalidate a plugin when updating", function() + error("TODO") + end) + end) + + describe("Consumer entity invalidation", function() + it("should invalidate a consumer when deleting", function() + error("TODO") + end) + + it("should invalidate a consumer when updating", function() + error("TODO") + end) + end) + + describe("API entity invalidation", function() + it("should invalidate ALL_APIS_BY_DICT when adding a new API", function() + error("TODO") + end) + + it("should invalidate ALL_APIS_BY_DICT when updating an API", function() + error("TODO") + end) + + it("should invalidate ALL_APIS_BY_DICT when deleting an API", function() + error("TODO") + end) + end) + + describe("Serf events", function() + it("should syncronize nodes on members events", function() + error("TODO") + end) + end) +end) diff --git a/spec/kong_tests.conf b/spec/kong_tests.conf index 9de0383b8e0c..4267df313f53 100644 --- a/spec/kong_tests.conf +++ b/spec/kong_tests.conf @@ -21,3 +21,4 @@ nginx_worker_processes = 1 nginx_optimizations = off prefix = servroot +log_level=info From f2f62fc189778789a691be8a2f59ba3f890accc3 Mon Sep 17 00:00:00 2001 From: thefosk Date: Wed, 15 Jun 2016 15:54:33 -0700 Subject: [PATCH 13/29] Hooks core test --- kong/cmd/utils/serf_signals.lua | 2 +- spec/02-integration/04-core/03-hooks_spec.lua | 759 ++++++++++++++++++ spec/02-integration/04-core/hooks_old.lua | 491 ----------- spec/02-integration/04-core/hooks_spec.lua | 174 ---- 4 files changed, 760 insertions(+), 666 deletions(-) create mode 100644 spec/02-integration/04-core/03-hooks_spec.lua delete mode 100644 spec/02-integration/04-core/hooks_old.lua delete mode 100644 spec/02-integration/04-core/hooks_spec.lua diff --git a/kong/cmd/utils/serf_signals.lua b/kong/cmd/utils/serf_signals.lua index edd25f73cdd4..b2f465c50cff 100644 --- a/kong/cmd/utils/serf_signals.lua +++ b/kong/cmd/utils/serf_signals.lua @@ -18,7 +18,7 @@ local serf_bin_name = "serf" local serf_pid_name = "serf.pid" local serf_node_id = "serf.id" local serf_event_name = "kong" -local start_timeout = 2 +local start_timeout = 5 local function check_serf_bin() local cmd = fmt("%s -v", serf_bin_name) diff --git a/spec/02-integration/04-core/03-hooks_spec.lua b/spec/02-integration/04-core/03-hooks_spec.lua new file mode 100644 index 000000000000..b200870b8156 --- /dev/null +++ b/spec/02-integration/04-core/03-hooks_spec.lua @@ -0,0 +1,759 @@ +local helpers = require "spec.helpers" +local cjson = require "cjson" +local cache = require "kong.tools.database_cache" +local pl_tablex = require "pl.tablex" +local pl_utils = require "pl.utils" +local pl_path = require "pl.path" +local pl_file = require "pl.file" +local pl_stringx = require "pl.stringx" + +describe("Core Hooks", function() + local client, api_client + local consumer, api1, api2, basic_auth2, api3, rate_limiting_consumer + before_each(function() + helpers.dao:truncate_tables() + helpers.execute "pkill nginx; pkill serf; pkill dnsmasq" + assert(helpers.prepare_prefix()) + + consumer = assert(helpers.dao.consumers:insert { + username = "consumer1" + }) + assert(helpers.dao.basicauth_credentials:insert { + username = "user123", + password = "pass123", + consumer_id = consumer.id + }) + + api1 = assert(helpers.dao.apis:insert { + request_host = "hooks1.com", + upstream_url = "http://mockbin.com" + }) + + api2 = assert(helpers.dao.apis:insert { + request_host = "hooks-consumer.com", + upstream_url = "http://mockbin.com" + }) + basic_auth2 = assert(helpers.dao.plugins:insert { + name = "basic-auth", + api_id = api2.id, + config = {} + }) + + api3 = assert(helpers.dao.apis:insert { + request_host = "hooks-plugins.com", + upstream_url = "http://mockbin.com" + }) + assert(helpers.dao.plugins:insert { + name = "basic-auth", + api_id = api3.id, + config = {} + }) + assert(helpers.dao.plugins:insert { + name = "rate-limiting", + api_id = api3.id, + config = { + minute = 10 + } + }) + rate_limiting_consumer = assert(helpers.dao.plugins:insert { + name = "rate-limiting", + api_id = api3.id, + consumer_id = consumer.id, + config = { + minute = 3 + } + }) + + assert(helpers.start_kong()) + client = assert(helpers.http_client("127.0.0.1", helpers.test_conf.proxy_port)) + api_client = assert(helpers.http_client("127.0.0.1", helpers.test_conf.admin_port)) + end) + after_each(function() + if client then + client:close() + end + if api_client then + api_client:close() + end + helpers.stop_kong() + --helpers.clean_prefix() + end) + + describe("Plugin entity invalidation", function() + it("should invalidate a plugin when deleting", function() + -- Making a request to populate the cache + local res = assert(client:send { + method = "GET", + path = "/status/200", + headers = { + ["Host"] = "hooks-consumer.com", + ["Authorization"] = "Basic dXNlcjEyMzpwYXNzMTIz" + } + }) + assert.res_status(200, res) + + -- Make sure the cache is populated + local res = assert(api_client:send { + method = "GET", + path = "/cache/"..cache.plugin_key("basic-auth", api2.id, nil), + headers = {} + }) + assert.res_status(200, res) + + -- Delete plugin + local res = assert(api_client:send { + method = "DELETE", + path = "/apis/"..api2.id.."/plugins/"..basic_auth2.id, + headers = {} + }) + assert.res_status(204, res) + + -- Wait for cache to be invalidated + helpers.wait_until(function() + local res = assert(api_client:send { + method = "GET", + path = "/cache/"..cache.plugin_key("basic-auth", api2.id, nil), + headers = {} + }) + res:read_body() + return res.status == 404 + end, 3) + + -- Consuming the API again without any authorization + local res = assert(client:send { + method = "GET", + path = "/status/200", + headers = { + ["Host"] = "hooks-consumer.com" + } + }) + assert.res_status(200, res) + end) + + it("should invalidate a consumer-specific plugin when deleting", function() + -- Making a request to populate the cache + local res = assert(client:send { + method = "GET", + path = "/status/200", + headers = { + ["Host"] = "hooks-plugins.com", + ["Authorization"] = "Basic dXNlcjEyMzpwYXNzMTIz" + } + }) + assert.res_status(200, res) + assert.equal(3, tonumber(res.headers["x-ratelimit-limit-minute"])) + + -- Make sure the cache is populated + local res = assert(api_client:send { + method = "GET", + path = "/cache/"..cache.plugin_key("rate-limiting", api3.id, consumer.id), + headers = {} + }) + assert.res_status(200, res) + + -- Delete plugin + local res = assert(api_client:send { + method = "DELETE", + path = "/apis/"..api3.id.."/plugins/"..rate_limiting_consumer.id, + headers = {} + }) + assert.res_status(204, res) + + -- Wait for cache to be invalidated + helpers.wait_until(function() + local res = assert(api_client:send { + method = "GET", + path = "/cache/"..cache.plugin_key("rate-limiting", api3.id, consumer.id), + headers = {} + }) + res:read_body() + return res.status == 404 + end, 3) + + -- Consuming the API again + local res = assert(client:send { + method = "GET", + path = "/status/200", + headers = { + ["Host"] = "hooks-plugins.com", + ["Authorization"] = "Basic dXNlcjEyMzpwYXNzMTIz" + } + }) + assert.res_status(200, res) + assert.equal(10, tonumber(res.headers["x-ratelimit-limit-minute"])) + end) + + it("should invalidate a consumer-specific plugin when updating", function() + -- Making a request to populate the cache + local res = assert(client:send { + method = "GET", + path = "/status/200", + headers = { + ["Host"] = "hooks-plugins.com", + ["Authorization"] = "Basic dXNlcjEyMzpwYXNzMTIz" + } + }) + assert.res_status(200, res) + assert.equal(3, tonumber(res.headers["x-ratelimit-limit-minute"])) + + -- Make sure the cache is populated + local res = assert(api_client:send { + method = "GET", + path = "/cache/"..cache.plugin_key("rate-limiting", api3.id, consumer.id), + headers = {} + }) + assert.res_status(200, res) + + -- Update plugin + local res = assert(api_client:send { + method = "PATCH", + path = "/apis/"..api3.id.."/plugins/"..rate_limiting_consumer.id, + headers = { + ["Content-Type"] = "application/json" + }, + body = cjson.encode({ + enabled = false + }) + }) + assert.res_status(200, res) + + -- Wait for cache to be invalidated + helpers.wait_until(function() + local res = assert(api_client:send { + method = "GET", + path = "/cache/"..cache.plugin_key("rate-limiting", api3.id, consumer.id), + headers = {} + }) + res:read_body() + return res.status == 404 + end, 3) + + -- Consuming the API again + local res = assert(client:send { + method = "GET", + path = "/status/200", + headers = { + ["Host"] = "hooks-plugins.com", + ["Authorization"] = "Basic dXNlcjEyMzpwYXNzMTIz" + } + }) + assert.res_status(200, res) + assert.equal(10, tonumber(res.headers["x-ratelimit-limit-minute"])) + end) + + it("should invalidate a plugin when updating", function() + -- Consuming the API without any authorization + local res = assert(client:send { + method = "GET", + path = "/status/200", + headers = { + ["Host"] = "hooks-consumer.com" + } + }) + assert.res_status(401, res) + + -- Making a request to populate the cache + local res = assert(client:send { + method = "GET", + path = "/status/200", + headers = { + ["Host"] = "hooks-consumer.com", + ["Authorization"] = "Basic dXNlcjEyMzpwYXNzMTIz" + } + }) + assert.res_status(200, res) + + -- Make sure the cache is populated + local res = assert(api_client:send { + method = "GET", + path = "/cache/"..cache.plugin_key("basic-auth", api2.id, nil), + headers = {} + }) + assert.res_status(200, res) + + -- Update plugin + local res = assert(api_client:send { + method = "PATCH", + path = "/apis/"..api2.id.."/plugins/"..basic_auth2.id, + headers = { + ["Content-Type"] = "application/json" + }, + body = cjson.encode({ + enabled = false + }) + }) + assert.res_status(200, res) + + -- Wait for cache to be invalidated + helpers.wait_until(function() + local res = assert(api_client:send { + method = "GET", + path = "/cache/"..cache.plugin_key("basic-auth", api2.id, nil), + headers = {} + }) + res:read_body() + return res.status == 404 + end, 3) + + -- Consuming the API again without any authorization + local res = assert(client:send { + method = "GET", + path = "/status/200", + headers = { + ["Host"] = "hooks-consumer.com" + } + }) + assert.res_status(200, res) + end) + end) + + describe("Consumer entity invalidation", function() + it("should invalidate a consumer when deleting", function() + -- Making a request to populate the cache + local res = assert(client:send { + method = "GET", + path = "/status/200", + headers = { + ["Host"] = "hooks-consumer.com", + ["Authorization"] = "Basic dXNlcjEyMzpwYXNzMTIz" + } + }) + assert.res_status(200, res) + + -- Make sure the cache is populated + local res = assert(api_client:send { + method = "GET", + path = "/cache/"..cache.consumer_key(consumer.id), + headers = {} + }) + assert.res_status(200, res) + + -- Delete consumer + local res = assert(api_client:send { + method = "DELETE", + path = "/consumers/"..consumer.id, + headers = {} + }) + assert.res_status(204, res) + + -- Wait for consumer be invalidated + helpers.wait_until(function() + local res = assert(api_client:send { + method = "GET", + path = "/cache/"..cache.consumer_key(consumer.id), + headers = {} + }) + res:read_body() + return res.status == 404 + end, 3) + + -- Wait for Basic Auth credential to be invalidated + helpers.wait_until(function() + local res = assert(api_client:send { + method = "GET", + path = "/cache/"..cache.basicauth_credential_key("user123"), + headers = {} + }) + res:read_body() + return res.status == 404 + end, 3) + + -- Consuming the API again + local res = assert(client:send { + method = "GET", + path = "/status/200", + headers = { + ["Host"] = "hooks-consumer.com", + ["Authorization"] = "Basic dXNlcjEyMzpwYXNzMTIz" + } + }) + assert.res_status(403, res) + end) + + it("should invalidate a consumer when updating", function() + -- Making a request to populate the cache + local res = assert(client:send { + method = "GET", + path = "/status/200", + headers = { + ["Host"] = "hooks-consumer.com", + ["Authorization"] = "Basic dXNlcjEyMzpwYXNzMTIz" + } + }) + assert.res_status(200, res) + + -- Make sure the cache is populated + local res = assert(api_client:send { + method = "GET", + path = "/cache/"..cache.consumer_key(consumer.id), + headers = {} + }) + assert.res_status(200, res) + + -- Update consumer + local res = assert(api_client:send { + method = "PATCH", + path = "/consumers/"..consumer.id, + headers = { + ["Content-Type"] = "application/json" + }, + body = cjson.encode({ + username = "updated_consumer" + }) + }) + assert.res_status(200, res) + + -- Wait for consumer be invalidated + helpers.wait_until(function() + local res = assert(api_client:send { + method = "GET", + path = "/cache/"..cache.consumer_key(consumer.id), + headers = {} + }) + res:read_body() + return res.status == 404 + end, 3) + + -- Consuming the API again + local res = assert(client:send { + method = "GET", + path = "/status/200", + headers = { + ["Host"] = "hooks-consumer.com", + ["Authorization"] = "Basic dXNlcjEyMzpwYXNzMTIz" + } + }) + assert.res_status(200, res) + + -- Making sure the cache is updated + local res = assert(api_client:send { + method = "GET", + path = "/cache/"..cache.consumer_key(consumer.id), + headers = {} + }) + local body = assert.res_status(200, res) + assert.equal("updated_consumer", cjson.decode(body).username) + end) + end) + + describe("API entity invalidation", function() + it("should invalidate ALL_APIS_BY_DICT when adding a new API", function() + -- Making a request to populate ALL_APIS_BY_DICT + local res = assert(client:send { + method = "GET", + path = "/status/200", + headers = { + ["Host"] = "hooks1.com" + } + }) + assert.res_status(200, res) + + -- Make sure the cache is populated + local res = assert(api_client:send { + method = "GET", + path = "/cache/"..cache.all_apis_by_dict_key(), + headers = {} + }) + assert.res_status(200, res) + + -- Adding a new API + local res = assert(api_client:send { + method = "POST", + path = "/apis/", + headers = { + ["Content-Type"] = "application/json" + }, + body = cjson.encode({ + request_host = "dynamic-hooks.com", + upstream_url = "http://mockbin.org" + }) + }) + assert.res_status(201, res) + + -- Wait for consumer be invalidated + helpers.wait_until(function() + local res = assert(api_client:send { + method = "GET", + path = "/cache/"..cache.all_apis_by_dict_key(), + headers = {} + }) + res:read_body() + return res.status == 404 + end, 3) + + -- Consuming the API again + local res = assert(client:send { + method = "GET", + path = "/status/200", + headers = { + ["Host"] = "hooks1.com" + } + }) + assert.res_status(200, res) + + -- Make sure the cache is populated + local res = assert(api_client:send { + method = "GET", + path = "/cache/"..cache.all_apis_by_dict_key(), + headers = {} + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.is_table(body.by_dns["hooks1.com"]) + assert.is_table(body.by_dns["dynamic-hooks.com"]) + end) + + it("should invalidate ALL_APIS_BY_DICT when updating an API", function() + -- Making a request to populate ALL_APIS_BY_DICT + local res = assert(client:send { + method = "GET", + path = "/status/200", + headers = { + ["Host"] = "hooks1.com" + } + }) + assert.res_status(200, res) + + -- Make sure the cache is populated + local res = assert(api_client:send { + method = "GET", + path = "/cache/"..cache.all_apis_by_dict_key(), + headers = {} + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.equal("http://mockbin.com", body.by_dns["hooks1.com"].upstream_url) + + -- Update API + local res = assert(api_client:send { + method = "PATCH", + path = "/apis/"..api1.id, + headers = { + ["Content-Type"] = "application/json" + }, + body = cjson.encode({ + upstream_url = "http://mockbin.org" + }) + }) + assert.res_status(200, res) + + -- Wait for consumer be invalidated + helpers.wait_until(function() + local res = assert(api_client:send { + method = "GET", + path = "/cache/"..cache.all_apis_by_dict_key(), + headers = {} + }) + res:read_body() + return res.status == 404 + end, 3) + + -- Consuming the API again + local res = assert(client:send { + method = "GET", + path = "/status/200", + headers = { + ["Host"] = "hooks1.com" + } + }) + assert.res_status(200, res) + + -- Make sure the cache is populated with updated value + local res = assert(api_client:send { + method = "GET", + path = "/cache/"..cache.all_apis_by_dict_key(), + headers = {} + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.equal("http://mockbin.org", body.by_dns["hooks1.com"].upstream_url) + assert.equal(3, pl_tablex.size(body.by_dns)) + end) + + it("should invalidate ALL_APIS_BY_DICT when deleting an API", function() + -- Making a request to populate ALL_APIS_BY_DICT + local res = assert(client:send { + method = "GET", + path = "/status/200", + headers = { + ["Host"] = "hooks1.com" + } + }) + assert.res_status(200, res) + + -- Make sure the cache is populated + local res = assert(api_client:send { + method = "GET", + path = "/cache/"..cache.all_apis_by_dict_key(), + headers = {} + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.equal("http://mockbin.com", body.by_dns["hooks1.com"].upstream_url) + + -- Deleting the API + local res = assert(api_client:send { + method = "DELETE", + path = "/apis/"..api1.id, + headers = {} + }) + assert.res_status(204, res) + + -- Wait for consumer be invalidated + helpers.wait_until(function() + local res = assert(api_client:send { + method = "GET", + path = "/cache/"..cache.all_apis_by_dict_key(), + headers = {} + }) + res:read_body() + return res.status == 404 + end, 3) + + -- Consuming the API again + local res = assert(client:send { + method = "GET", + path = "/status/200", + headers = { + ["Host"] = "hooks1.com" + } + }) + assert.res_status(404, res) + + -- Make sure the cache is populated with zero APIs + local res = assert(api_client:send { + method = "GET", + path = "/cache/"..cache.all_apis_by_dict_key(), + headers = {} + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.equal(2, pl_tablex.size(body.by_dns)) + end) + end) + + describe("Serf events", function() + local PID_FILE = "/tmp/serf_test.pid" + local LOG_FILE = "/tmp/serf_test.log" + + local function kill(pid_file, args) + local cmd = string.format([[kill %s `cat %s` >/dev/null 2>&1]], args or "-0", pid_file) + return os.execute(cmd) + end + + local function is_running(pid_path) + if not pl_path.exists(pid_path) then return nil end + local code = kill(pid_path, "-0") + return code == 0 + end + + local function start_serf() + local args = { + ["-node"] = "test_node", + ["-bind"] = "127.0.0.1:10000", + ["-profile"] = "lan", + ["-rpc-addr"] = "127.0.0.1:10001" + } + setmetatable(args, require "kong.tools.printable") + + local cmd = string.format("nohup serf agent %s > %s 2>&1 & echo $! > %s", + tostring(args), + LOG_FILE, PID_FILE) + + -- start Serf agent + local ok = pl_utils.execute(cmd) + if not ok then return error("Cannot start Serf") end + + -- ensure started (just an improved version of previous Serf service) + local start_timeout = 5 + local tstart = ngx.time() + local texp, started = tstart + start_timeout + + repeat + ngx.sleep(0.2) + started = is_running(PID_FILE) + until started or ngx.time() >= texp + + if not started then + -- time to get latest error log from serf.log + local logs = pl_file.read(LOG_FILE) + local tlogs = pl_stringx.split(logs, "\n") + local err = string.gsub(tlogs[#tlogs-1], "==> ", "") + err = pl_stringx.strip(err) + error("could not start Serf:\n "..err) + end + + if not ok then error("Error starting serf") end + end + + local function stop_serf() + os.execute(string.format("kill `cat %s` >/dev/null 2>&1", PID_FILE)) + end + + it("should syncronize nodes on members events", function() + start_serf() + + -- Tell Kong to join the new Serf + local res = assert(api_client:send { + method = "POST", + path = "/cluster/", + headers = { + ["Content-Type"] = "application/json" + }, + body = cjson.encode({address = "127.0.0.1:10000"}) + }) + assert.res_status(200, res) + + -- Wait until the node joins + helpers.wait_until(function() + local res = assert(api_client:send { + method = "GET", + path = "/cluster/", + headers = {} + }) + local body = cjson.decode(assert.res_status(200, res)) + if #body.data == 2 then + local found + for _, v in ipairs(body.data) do + if v.address == "127.0.0.1:10000" then + found = true + assert.equal("test_node", v.name) + assert.equal("alive", v.status) + else + assert.is_string(v.name) + assert.equal("alive", v.status) + end + end + return found + end + end, 5) + + -- Killing serf + stop_serf() + + -- Wait until the node appears as failed + helpers.wait_until(function() + local res = assert(api_client:send { + method = "GET", + path = "/cluster/", + headers = {} + }) + local body = cjson.decode(assert.res_status(200, res)) + local found + if #body.data == 2 then + for _, v in ipairs(body.data) do + if v.address == "127.0.0.1:10000" then + if v.name == "test_node" and v.status == "failed" then + found = true + end + else + assert.is_string(v.name) + assert.equal("alive", v.status) + end + end + end + ngx.sleep(1) + return found + end, 60) + + finally(function() + stop_serf() + end) + end) + end) +end) diff --git a/spec/02-integration/04-core/hooks_old.lua b/spec/02-integration/04-core/hooks_old.lua deleted file mode 100644 index 4c4954262759..000000000000 --- a/spec/02-integration/04-core/hooks_old.lua +++ /dev/null @@ -1,491 +0,0 @@ -local json = require "cjson" -local http_client = require "kong.tools.http_client" -local spec_helper = require "spec.spec_helpers" -local cache = require "kong.tools.database_cache" -local utils = require "kong.tools.utils" -local IO = require "kong.tools.io" - -local STUB_GET_URL = spec_helper.STUB_GET_URL -local API_URL = spec_helper.API_URL - -describe("Core Hooks", function() - - setup(function() - pcall(spec_helper.stop_kong) - spec_helper.prepare_db() - end) - - before_each(function() - spec_helper.drop_db() - spec_helper.start_kong() - spec_helper.insert_fixtures { - api = { - {request_host = "hooks1.com", upstream_url = "http://mockbin.com"}, - {request_host = "hooks-consumer.com", upstream_url = "http://mockbin.com"}, - {request_host = "hooks-plugins.com", upstream_url = "http://mockbin.com"} - }, - consumer = { - {username = "consumer1"} - }, - plugin = { - {name = "basic-auth", config = {}, __api = 2}, - {name = "basic-auth", config = {}, __api = 3}, - {name = "rate-limiting", config = {minute=10}, __api = 3}, - {name = "rate-limiting", config = {minute=3}, __api = 3, __consumer = 1} - }, - basicauth_credential = { - {username = "user123", password = "pass123", __consumer = 1} - } - } - end) - - after_each(function() - pcall(spec_helper.stop_kong()) - end) - - describe("Plugin entity invalidation", function() - it("should invalidate a plugin when deleting", function() - -- Making a request to populate the cache - local _, status = http_client.get(STUB_GET_URL, {}, {host = "hooks-consumer.com", authorization = "Basic dXNlcjEyMzpwYXNzMTIz"}) - assert.equals(200, status) - - -- Make sure the cache is populated - local response, status = http_client.get(API_URL.."/apis", {request_host="hooks-consumer.com"}) - assert.equals(200, status) - local api_id = json.decode(response).data[1].id - assert.truthy(api_id) - - local _, status = http_client.get(API_URL.."/cache/"..cache.plugin_key("basic-auth", api_id, nil)) - assert.equals(200, status) - - -- Delete plugin - local response, status = http_client.get(API_URL.."/apis/"..api_id.."/plugins/", {name="basic-auth"}) - assert.equals(200, status) - local plugin_id = json.decode(response).data[1].id - assert.truthy(plugin_id) - - local _, status = http_client.delete(API_URL.."/apis/"..api_id.."/plugins/"..plugin_id) - assert.equals(204, status) - - -- Wait for cache to be invalidated - local exists = true - while(exists) do - local _, status = http_client.get(API_URL.."/cache/"..cache.plugin_key("basic-auth", api_id, nil)) - if status ~= 200 then - exists = false - end - end - - -- Consuming the API again without any authorization - local _, status = http_client.get(STUB_GET_URL, {}, {host = "hooks-consumer.com"}) - assert.equals(200, status) - end) - - it("should invalidate a consumer-specific plugin when deleting", function() - -- Making a request to populate the cache - local _, status, headers = http_client.get(STUB_GET_URL, {}, {host = "hooks-plugins.com", authorization = "Basic dXNlcjEyMzpwYXNzMTIz"}) - assert.equals(200, status) - assert.equals(3, tonumber(headers["x-ratelimit-limit-minute"])) - - -- Make sure the cache is populated - local response, status = http_client.get(API_URL.."/apis", {request_host="hooks-plugins.com"}) - assert.equals(200, status) - local api_id = json.decode(response).data[1].id - assert.truthy(api_id) - - local response, status = http_client.get(API_URL.."/consumers/consumer1") - assert.equals(200, status) - local consumer_id = json.decode(response).id - assert.truthy(consumer_id) - - local _, status = http_client.get(API_URL.."/cache/"..cache.plugin_key("rate-limiting", api_id, consumer_id)) - assert.equals(200, status) - - -- Delete plugin - local response, status = http_client.get(API_URL.."/apis/"..api_id.."/plugins/", {name="rate-limiting", consumer_id=consumer_id}) - assert.equals(200, status) - local plugin_id = json.decode(response).data[1].id - assert.truthy(plugin_id) - - local _, status = http_client.delete(API_URL.."/apis/"..api_id.."/plugins/"..plugin_id) - assert.equals(204, status) - - -- Wait for cache to be invalidated - local exists = true - while(exists) do - local _, status = http_client.get(API_URL.."/cache/"..cache.plugin_key("rate-limiting", api_id, consumer_id)) - if status ~= 200 then - exists = false - end - end - - -- Consuming the API again - local _, status, headers = http_client.get(STUB_GET_URL, {}, {host = "hooks-plugins.com", authorization = "Basic dXNlcjEyMzpwYXNzMTIz"}) - assert.equals(200, status) - assert.equals(10, tonumber(headers["x-ratelimit-limit-minute"])) - end) - - it("should invalidate a consumer-specific plugin when updating", function() - -- Making a request to populate the cache - local _, status, headers = http_client.get(STUB_GET_URL, {}, {host = "hooks-plugins.com", authorization = "Basic dXNlcjEyMzpwYXNzMTIz"}) - assert.equals(200, status) - assert.equals(3, tonumber(headers["x-ratelimit-limit-minute"])) - - -- Make sure the cache is populated - local response, status = http_client.get(API_URL.."/apis", {request_host="hooks-plugins.com"}) - assert.equals(200, status) - local api_id = json.decode(response).data[1].id - assert.truthy(api_id) - - local response, status = http_client.get(API_URL.."/consumers/consumer1") - assert.equals(200, status) - local consumer_id = json.decode(response).id - assert.truthy(consumer_id) - - local _, status = http_client.get(API_URL.."/cache/"..cache.plugin_key("rate-limiting", api_id, consumer_id)) - assert.equals(200, status) - - -- Update plugin - local response, status = http_client.get(API_URL.."/apis/"..api_id.."/plugins/", {name="rate-limiting", consumer_id=consumer_id}) - assert.equals(200, status) - local plugin_id = json.decode(response).data[1].id - assert.truthy(plugin_id) - - local _, status = http_client.patch(API_URL.."/apis/"..api_id.."/plugins/"..plugin_id, {enabled=false}) - assert.equals(200, status) - - -- Wait for cache to be invalidated - local exists = true - while(exists) do - local _, status = http_client.get(API_URL.."/cache/"..cache.plugin_key("rate-limiting", api_id, consumer_id)) - if status ~= 200 then - exists = false - end - end - - -- Consuming the API again - local _, status, headers = http_client.get(STUB_GET_URL, {}, {host = "hooks-plugins.com", authorization = "Basic dXNlcjEyMzpwYXNzMTIz"}) - assert.equals(200, status) - assert.equals(10, tonumber(headers["x-ratelimit-limit-minute"])) - end) - - it("should invalidate a plugin when updating", function() - -- Making a request to populate the cache - local _, status = http_client.get(STUB_GET_URL, {}, {host = "hooks-consumer.com", authorization = "Basic dXNlcjEyMzpwYXNzMTIz"}) - assert.equals(200, status) - - -- Make sure the cache is populated - local response, status = http_client.get(API_URL.."/apis", {request_host="hooks-consumer.com"}) - assert.equals(200, status) - local api_id = json.decode(response).data[1].id - assert.truthy(api_id) - - local _, status = http_client.get(API_URL.."/cache/"..cache.plugin_key("basic-auth", api_id, nil)) - assert.equals(200, status) - - -- Delete plugin - local response, status = http_client.get(API_URL.."/apis/"..api_id.."/plugins/", {name="basic-auth"}) - assert.equals(200, status) - local plugin_id = json.decode(response).data[1].id - assert.truthy(plugin_id) - - local _, status = http_client.patch(API_URL.."/apis/"..api_id.."/plugins/"..plugin_id, {enabled=false}) - assert.equals(200, status) - - -- Wait for cache to be invalidated - local exists = true - while(exists) do - local _, status = http_client.get(API_URL.."/cache/"..cache.plugin_key("basic-auth", api_id, nil)) - if status ~= 200 then - exists = false - end - end - - -- Consuming the API again without any authorization - local _, status = http_client.get(STUB_GET_URL, {}, {host = "hooks-consumer.com"}) - assert.equals(200, status) - end) - end) - - describe("Consumer entity invalidation", function() - it("should invalidate a consumer when deleting", function() - -- Making a request to populate the cache - local _, status = http_client.get(STUB_GET_URL, {}, {host = "hooks-consumer.com", authorization = "Basic dXNlcjEyMzpwYXNzMTIz"}) - assert.equals(200, status) - - -- Make sure the cache is populated - local response, status = http_client.get(API_URL.."/consumers/consumer1") - assert.equals(200, status) - local consumer_id = json.decode(response).id - assert.truthy(consumer_id) - - local response, status = http_client.get(API_URL.."/cache/"..cache.consumer_key(consumer_id)) - assert.equals(200, status) - assert.equals("consumer1", json.decode(response).username) - - -- Delete consumer - local _, status = http_client.delete(API_URL.."/consumers/consumer1") - assert.equals(204, status) - - -- Wait for consumer be invalidated - local exists = true - while(exists) do - local _, status = http_client.get(API_URL.."/cache/"..cache.consumer_key(consumer_id)) - if status ~= 200 then - exists = false - end - end - - -- Wait for Basic Auth credential to be invalidated - local exists = true - while(exists) do - local _, status = http_client.get(API_URL.."/cache/"..cache.basicauth_credential_key("user123")) - if status ~= 200 then - exists = false - end - end - - -- Consuming the API again - local _, status = http_client.get(STUB_GET_URL, {}, {host = "hooks-consumer.com", authorization = "Basic dXNlcjEyMzpwYXNzMTIz"}) - assert.equals(403, status) - end) - - it("should invalidate a consumer when updating", function() - -- Making a request to populate the cache - local _, status = http_client.get(STUB_GET_URL, {}, {host = "hooks-consumer.com", authorization = "Basic dXNlcjEyMzpwYXNzMTIz"}) - assert.equals(200, status) - - -- Make sure the cache is populated - local response, status = http_client.get(API_URL.."/consumers/consumer1") - assert.equals(200, status) - local consumer_id = json.decode(response).id - assert.truthy(consumer_id) - - local response, status = http_client.get(API_URL.."/cache/"..cache.consumer_key(consumer_id)) - assert.equals(200, status) - assert.equals("consumer1", json.decode(response).username) - - -- Update consumer - local _, status = http_client.patch(API_URL.."/consumers/consumer1", {username="updated_consumer1"}) - assert.equals(200, status) - - -- Wait for cache to be invalidated - local exists = true - while(exists) do - local _, status = http_client.get(API_URL.."/cache/"..cache.consumer_key(consumer_id)) - if status ~= 200 then - exists = false - end - end - - -- Consuming the API again - local _, status = http_client.get(STUB_GET_URL, {}, {host = "hooks-consumer.com", authorization = "Basic dXNlcjEyMzpwYXNzMTIz"}) - assert.equals(200, status) - - -- Making sure the cache is updated - local response, status = http_client.get(API_URL.."/cache/"..cache.consumer_key(consumer_id)) - assert.equals(200, status) - assert.equals("updated_consumer1", json.decode(response).username) - end) - end) - - describe("API entity invalidation", function() - it("should invalidate ALL_APIS_BY_DICT when adding a new API", function() - -- Making a request to populate ALL_APIS_BY_DICT - local _, status = http_client.get(STUB_GET_URL, {}, {host = "hooks1.com"}) - assert.equals(200, status) - - -- Make sure the cache is populated - local response, status = http_client.get(API_URL.."/cache/"..cache.all_apis_by_dict_key()) - assert.equals(200, status) - assert.truthy(json.decode(response).by_dns["hooks1.com"]) - assert.falsy(json.decode(response).by_dns["dynamic-hooks.com"]) - - -- Adding a new API - local _, status = http_client.post(API_URL.."/apis", {request_host="dynamic-hooks.com", upstream_url="http://mockbin.org"}) - assert.equals(201, status) - - -- Wait for cache to be invalidated - local exists = true - while(exists) do - local _, status = http_client.get(API_URL.."/cache/"..cache.all_apis_by_dict_key()) - if status ~= 200 then - exists = false - end - end - - -- Consuming the API again - local _, status = http_client.get(STUB_GET_URL, {}, {host = "hooks1.com"}) - assert.equals(200, status) - - -- Make sure the cache is populated - local response, status = http_client.get(API_URL.."/cache/"..cache.all_apis_by_dict_key()) - assert.equals(200, status) - assert.truthy(json.decode(response).by_dns["hooks1.com"]) - assert.truthy(json.decode(response).by_dns["dynamic-hooks.com"]) - end) - - it("should invalidate ALL_APIS_BY_DICT when updating an API", function() - -- Making a request to populate ALL_APIS_BY_DICT - local _, status = http_client.get(STUB_GET_URL, {}, {host = "hooks1.com"}) - assert.equals(200, status) - - -- Make sure the cache is populated - local response, status = http_client.get(API_URL.."/cache/"..cache.all_apis_by_dict_key()) - assert.equals(200, status) - assert.truthy(json.decode(response).by_dns["hooks1.com"]) - assert.equals("http://mockbin.com", json.decode(response).by_dns["hooks1.com"].upstream_url) - - -- Updating API - local response, status = http_client.get(API_URL.."/apis", {request_host="hooks1.com"}) - assert.equals(200, status) - local api_id = json.decode(response).data[1].id - assert.truthy(api_id) - - local _, status = http_client.patch(API_URL.."/apis/"..api_id, {upstream_url="http://mockbin.org"}) - assert.equals(200, status) - - -- Wait for cache to be invalidated - local exists = true - while(exists) do - local _, status = http_client.get(API_URL.."/cache/"..cache.all_apis_by_dict_key()) - if status ~= 200 then - exists = false - end - end - - -- Consuming the API again - local _, status = http_client.get(STUB_GET_URL, {}, {host = "hooks1.com"}) - assert.equals(200, status) - - -- Make sure the cache is populated with updated value - local response, status = http_client.get(API_URL.."/cache/"..cache.all_apis_by_dict_key()) - assert.equals(200, status) - assert.truthy(json.decode(response).by_dns["hooks1.com"]) - assert.equals("http://mockbin.org", json.decode(response).by_dns["hooks1.com"].upstream_url) - end) - - it("should invalidate ALL_APIS_BY_DICT when deleting an API", function() - -- Making a request to populate ALL_APIS_BY_DICT - local _, status = http_client.get(STUB_GET_URL, {}, {host = "hooks1.com"}) - assert.equals(200, status) - - -- Make sure the cache is populated - local response, status = http_client.get(API_URL.."/cache/"..cache.all_apis_by_dict_key()) - assert.equals(200, status) - assert.truthy(1, utils.table_size(json.decode(response).by_dns)) - assert.truthy(json.decode(response).by_dns["hooks1.com"]) - - -- Deleting API - local response, status = http_client.get(API_URL.."/apis", {request_host="hooks1.com"}) - assert.equals(200, status) - local api_id = json.decode(response).data[1].id - assert.truthy(api_id) - - local _, status = http_client.delete(API_URL.."/apis/"..api_id) - assert.equals(204, status) - - -- Wait for cache to be invalidated - local exists = true - while(exists) do - local _, status = http_client.get(API_URL.."/cache/"..cache.all_apis_by_dict_key()) - if status ~= 200 then - exists = false - end - end - - -- Consuming the API again - local _, status = http_client.get(STUB_GET_URL, {}, {host = "hooks1.com"}) - assert.equals(404, status) - - -- Make sure the cache is populated - local response, status = http_client.get(API_URL.."/cache/"..cache.all_apis_by_dict_key()) - assert.equals(200, status) - assert.truthy(0, utils.table_size(json.decode(response).by_dns)) - end) - end) - - describe("Serf events", function() - - local PID_FILE = "/tmp/serf_test.pid" - - local function start_serf() - local cmd_args = { - ["-node"] = "test_node", - ["-bind"] = "127.0.0.1:9000", - ["-profile"] = "wan", - ["-rpc-addr"] = "127.0.0.1:9001" - } - setmetatable(cmd_args, require "kong.tools.printable") - - local res, code = IO.os_execute("nohup serf agent "..tostring(cmd_args).." 2>&1 & echo $! > "..PID_FILE) - if code ~= 0 then - error("Error starting serf: "..res) - end - end - - local function stop_serf() - local pid = IO.read_file(PID_FILE) - IO.os_execute("kill "..pid) - end - - it("should syncronize nodes on members events", function() - start_serf() - - os.execute("sleep 5") -- Wait for both the first member to join, and for the seconday serf to start - - -- Tell Kong to join the new serf - local _, code = http_client.post(API_URL.."/cluster/", {address = "127.0.0.1:9000"}) - assert.equals(200, code) - - os.execute("sleep 3") - - local res, code = http_client.get(API_URL.."/cluster/") - local body = json.decode(res) - assert.equals(200, code) - assert.equals(2, #body.data) - - local found - for _, v in ipairs(body.data) do - if v.address == "127.0.0.1:9000" then - found = true - assert.equal("test_node", v.name) - assert.equal("alive", v.status) - else - assert.truthy(v.name) - assert.equal("alive", v.status) - end - end - assert.True(found) - - -- Killing serf - stop_serf() - - -- Triggering the status check - local _, code = IO.os_execute("serf reachability") - assert.equals(1, code) - - -- Wait a little bit to propagate the data - os.execute("sleep 45") - - -- Check again - local res, code = http_client.get(API_URL.."/cluster/") - local body = json.decode(res) - assert.equals(200, code) - assert.equals(2, #body.data) - - local found - for _, v in ipairs(body.data) do - if v.address == "127.0.0.1:9000" then - found = true - assert.equal("test_node", v.name) - assert.equal("failed", v.status) - else - assert.truthy(v.name) - assert.equal("alive", v.status) - end - end - assert.True(found) - end) - end) -end) diff --git a/spec/02-integration/04-core/hooks_spec.lua b/spec/02-integration/04-core/hooks_spec.lua deleted file mode 100644 index a0796172979b..000000000000 --- a/spec/02-integration/04-core/hooks_spec.lua +++ /dev/null @@ -1,174 +0,0 @@ -local helpers = require "spec.helpers" -local cjson = require "cjson" -local meta = require "kong.meta" -local cache = require "kong.tools.database_cache" - -local TIMEOUT = 10 - -describe("Core Hooks", function() - local client, api_client - local api2, basic_auth2 - setup(function() - helpers.dao:truncate_tables() - helpers.execute "pkill nginx; pkill serf; pkill dnsmasq" - assert(helpers.prepare_prefix()) - - local consumer = assert(helpers.dao.consumers:insert { - username = "consumer1" - }) - assert(helpers.dao.basicauth_credentials:insert { - username = "user123", - password = "pass123", - consumer_id = consumer.id - }) - - assert(helpers.dao.apis:insert { - request_host = "hooks1.com", - upstream_url = "http://mockbin.com" - }) - - api2 = assert(helpers.dao.apis:insert { - request_host = "hooks-consumer.com", - upstream_url = "http://mockbin.com" - }) - basic_auth2 = assert(helpers.dao.plugins:insert { - name = "basic-auth", - api_id = api2.id, - config = {} - }) - - local api3 = assert(helpers.dao.apis:insert { - request_host = "hooks-plugins.com", - upstream_url = "http://mockbin.com" - }) - assert(helpers.dao.plugins:insert { - name = "basic-auth", - api_id = api3.id, - config = {} - }) - assert(helpers.dao.plugins:insert { - name = "rate-limiting", - api_id = api3.id, - config = { - minute = 10 - } - }) - assert(helpers.dao.plugins:insert { - name = "rate-limiting", - api_id = api3.id, - consumer_id = consumer.id, - config = { - minute = 10 - } - }) - - assert(helpers.start_kong()) - client = assert(helpers.http_client("127.0.0.1", helpers.test_conf.proxy_port)) - api_client = assert(helpers.http_client("127.0.0.1", helpers.test_conf.admin_port)) - end) - teardown(function() - if client then - client:close() - end - if api_client then - api_client:close() - end - helpers.stop_kong() - --helpers.clean_prefix() - end) - - local function timeout_while(timeout, fn) - local start = os.time() - while(os.time() < (start + timeout)) do - if fn() then return end - end - error("Timeout") - end - - describe("Plugin entity invalidation", function() - it("#only should invalidate a plugin when deleting", function() - -- Making a request to populate the cache - local res = assert(client:send { - method = "GET", - path = "/status/200", - headers = { - ["Host"] = "hooks-consumer.com", - ["Authorization"] = "Basic dXNlcjEyMzpwYXNzMTIz" - } - }) - assert.res_status(200, res) - - -- Make sure the cache is populated - local res = assert(api_client:send { - method = "GET", - path = "/cache/"..cache.plugin_key("basic-auth", api2.id, nil), - headers = {} - }) - assert.res_status(200, res) - - -- Delete plugin - local res = assert(api_client:send { - method = "DELETE", - path = "/apis/"..api2.id.."/plugins/"..basic_auth2.id, - headers = {} - }) - assert.res_status(204, res) - - -- Wait for cache to be invalidated - timeout_while(TIMEOUT, function() - os.execute("sleep 0.2") -- Apparently api_client cannot be reused immediately - local res = assert(api_client:send { - method = "GET", - path = "/cache/"..cache.plugin_key("basic-auth", api2.id, nil), - headers = {} - }) - if res.status ~= 200 then - assert.res_status(404, res) - return true - end - end) - end) - - it("should invalidate a consumer-specific plugin when deleting", function() - error("TODO") - end) - - it("should invalidate a consumer-specific plugin when updating", function() - error("TODO") - end) - - it("should invalidate a plugin when updating", function() - error("TODO") - end) - end) - - describe("Consumer entity invalidation", function() - it("should invalidate a consumer when deleting", function() - error("TODO") - end) - - it("should invalidate a consumer when updating", function() - error("TODO") - end) - end) - - describe("API entity invalidation", function() - it("should invalidate ALL_APIS_BY_DICT when adding a new API", function() - error("TODO") - end) - - it("should invalidate ALL_APIS_BY_DICT when updating an API", function() - error("TODO") - end) - - it("should invalidate ALL_APIS_BY_DICT when deleting an API", function() - error("TODO") - end) - end) - - describe("Serf events", function() - it("should syncronize nodes on members events", function() - error("TODO") - end) - end) -end) From 1abeffa79fc443e46601f290061320dbaf5d2bdf Mon Sep 17 00:00:00 2001 From: thefosk Date: Wed, 15 Jun 2016 19:28:02 -0700 Subject: [PATCH 14/29] cluster test --- .../01-cmd/02-start_stop_spec.lua | 2 +- .../02-integration/01-cmd/07-cluster_spec.lua | 2 +- .../06-cluster/cluster_spec.lua | 385 ++++++++++++++++++ 3 files changed, 387 insertions(+), 2 deletions(-) create mode 100644 spec/02-integration/06-cluster/cluster_spec.lua diff --git a/spec/02-integration/01-cmd/02-start_stop_spec.lua b/spec/02-integration/01-cmd/02-start_stop_spec.lua index cd2361fe6d16..bcbd3691aac9 100644 --- a/spec/02-integration/01-cmd/02-start_stop_spec.lua +++ b/spec/02-integration/01-cmd/02-start_stop_spec.lua @@ -48,7 +48,7 @@ describe("kong start/stop", function() assert.equal("", stderr) assert.True(ok) end) - it("#only start/stop custom Kong conf/prefix", function() + it("start/stop custom Kong conf/prefix", function() local ok, _, stdout, stderr = exec("start --conf "..helpers.test_conf_path) assert.True(ok) assert.not_equal("", stdout) diff --git a/spec/02-integration/01-cmd/07-cluster_spec.lua b/spec/02-integration/01-cmd/07-cluster_spec.lua index 243f79455db2..3da372af55e4 100644 --- a/spec/02-integration/01-cmd/07-cluster_spec.lua +++ b/spec/02-integration/01-cmd/07-cluster_spec.lua @@ -38,7 +38,7 @@ describe("kong cluster", function() assert.is_string(stdout) assert.True(string.len(stdout) > 10) end) - describe("#only force-leave", function() + describe("force-leave", function() it("should fail when no node is specified", function() local ok, _, stdout, stderr = exec "cluster force-leave" assert.False(ok) diff --git a/spec/02-integration/06-cluster/cluster_spec.lua b/spec/02-integration/06-cluster/cluster_spec.lua new file mode 100644 index 000000000000..a47ae893b76d --- /dev/null +++ b/spec/02-integration/06-cluster/cluster_spec.lua @@ -0,0 +1,385 @@ +local helpers = require "spec.helpers" +local cache = require "kong.tools.database_cache" +local pl_stringx = require "pl.stringx" +local pl_tablex = require "pl.tablex" +local cjson = require "cjson" +local KILL_ALL = "pkill nginx; pkill serf; pkill dnsmasq" + +local function exec(args, env) + args = args or "" + env = env or {} + + local env_vars = "" + for k, v in pairs(env) do + env_vars = string.format("%s KONG_%s=%s", env_vars, k:upper(), v) + end + return helpers.execute(env_vars.." "..helpers.bin_path.." "..args) +end + +local NODES = { + servroot1 = { + prefix = "servroot1", + proxy_listen = "127.0.0.1:9000", + proxy_listen_ssl = "127.0.0.1:9443", + admin_listen = "0.0.0.0:9001", + cluster_listen = "0.0.0.0:9946", + cluster_listen_rpc = "0.0.0.0:9373" + }, + servroot2 = { + prefix = "servroot2", + proxy_listen = "127.0.0.1:10000", + proxy_listen_ssl = "127.0.0.1:10443", + admin_listen = "0.0.0.0:10001", + cluster_listen = "0.0.0.0:10946", + cluster_listen_rpc = "0.0.0.0:10373" + }, + servroot3 = { + prefix = "servroot3", + proxy_listen = "127.0.0.1:20000", + proxy_listen_ssl = "127.0.0.1:20443", + admin_listen = "0.0.0.0:20001", + cluster_listen = "0.0.0.0:20946", + cluster_listen_rpc = "0.0.0.0:20373" + } +} + +describe("Cluster", function() + before_each(function() + helpers.dao:truncate_tables() + helpers.execute(KILL_ALL) + end) + after_each(function() + helpers.execute(KILL_ALL) + for k, v in pairs(NODES) do + helpers.clean_prefix(k) + end + end) + + describe("Nodes", function() + it("should register the node on startup", function() + assert(exec("start --conf "..helpers.test_conf_path, NODES.servroot1)) + + -- Wait for node to be registered + helpers.wait_until(function() + return helpers.dao.nodes:count() == 1 + end) + + local node = helpers.dao.nodes:find_all()[1] + assert.is_number(node.created_at) + assert.is_string(node.name) + assert.is_string(node.cluster_listening_address) + + local api_client = assert(helpers.http_client("127.0.0.1", pl_stringx.split(NODES.servroot1.admin_listen, ":")[2])) + local res = assert(api_client:send { + method = "GET", + path = "/cluster/", + headers = {} + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.equal(1, body.total) + assert.equal(node.name, body.data[1].name) + assert.equal(node.cluster_listening_address, body.data[1].address) + assert.equal("alive", body.data[1].status) + end) + + it("should register the node on startup with the advertised address", function() + local conf = pl_tablex.deepcopy(NODES.servroot1) + conf.cluster_advertise = "5.5.5.5:1234" + + assert(exec("start --conf "..helpers.test_conf_path, conf)) + + -- Wait for node to be registered + helpers.wait_until(function() + return helpers.dao.nodes:count() == 1 + end) + + local node = helpers.dao.nodes:find_all()[1] + assert.is_number(node.created_at) + assert.is_string(node.name) + assert.equal("5.5.5.5:1234", node.cluster_listening_address) + + local api_client = assert(helpers.http_client("127.0.0.1", pl_stringx.split(NODES.servroot1.admin_listen, ":")[2])) + local res = assert(api_client:send { + method = "GET", + path = "/cluster/", + headers = {} + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.equal(1, body.total) + assert.equal(node.name, body.data[1].name) + assert.equal(node.cluster_listening_address, body.data[1].address) + assert.equal("alive", body.data[1].status) + end) + end) + + describe("Auto-join", function() + it("should register the second node on startup and auto-join sequentially", function() + assert(exec("start --conf "..helpers.test_conf_path, NODES.servroot1)) + -- Wait for first node to be registered + helpers.wait_until(function() + return helpers.dao.nodes:count() == 1 + end) + + assert(exec("start --conf "..helpers.test_conf_path, NODES.servroot2)) + -- Wait for second node to be registered + helpers.wait_until(function() + return helpers.dao.nodes:count() == 2 + end) + + local nodes = helpers.dao.nodes:find_all() + assert.is_number(nodes[1].created_at) + assert.is_string(nodes[1].name) + assert.is_string(nodes[1].cluster_listening_address) + assert.is_number(nodes[2].created_at) + assert.is_string(nodes[2].name) + assert.is_string(nodes[2].cluster_listening_address) + + -- Wait for nodes to be registered in Serf in both nodes + for _, v in ipairs({NODES.servroot1, NODES.servroot2}) do + local api_client = assert(helpers.http_client("127.0.0.1", pl_stringx.split(v.admin_listen, ":")[2])) + helpers.wait_until(function() + local res = assert(api_client:send { + method = "GET", + path = "/cluster/", + headers = {} + }) + local body = cjson.decode(assert.res_status(200, res)) + return body.total == 2 + end, 3) + end + end) + + it("should register the second node on startup and auto-join asyncronously", function() + assert(exec("start --conf "..helpers.test_conf_path, NODES.servroot1)) + assert(exec("start --conf "..helpers.test_conf_path, NODES.servroot2)) + assert(exec("start --conf "..helpers.test_conf_path, NODES.servroot3)) + + -- We need to wait a few seconds for the async job to kick in and join all the nodes together + helpers.wait_until(function() + local ok, _, stdout = helpers.execute("serf members -format=json -rpc-addr="..NODES.servroot1.cluster_listen_rpc) + assert.True(ok) + return #cjson.decode(stdout).members == 3 + end, 5) + + -- Wait for nodes to be registered in Serf in both nodes + for _, v in ipairs({NODES.servroot1, NODES.servroot2, NODES.servroot3}) do + local api_client = assert(helpers.http_client("127.0.0.1", pl_stringx.split(v.admin_listen, ":")[2])) + helpers.wait_until(function() + local res = assert(api_client:send { + method = "GET", + path = "/cluster/", + headers = {} + }) + local body = cjson.decode(assert.res_status(200, res)) + return body.total == 3 + end, 3) + end + end) + end) + + describe("Cache purges", function() + it("must purge cache on all nodes on member-join", function() + assert(exec("start --conf "..helpers.test_conf_path, NODES.servroot1)) + -- Wait for first node to be registered + helpers.wait_until(function() + return helpers.dao.nodes:count() == 1 + end) + + -- Adding an API + local api_client = assert(helpers.http_client("127.0.0.1", pl_stringx.split(NODES.servroot1.admin_listen, ":")[2])) + local res = assert(api_client:send { + method = "POST", + path = "/apis/", + headers = { + ["Content-Type"] = "application/json" + }, + body = { + request_host = "test.com", + upstream_url = "http://mockbin.org" + } + }) + assert.res_status(201, res) + + ngx.sleep(5) -- Wait for invalidation of API creation to propagate + + -- Populate the cache + local client = assert(helpers.http_client("127.0.0.1", pl_stringx.split(NODES.servroot1.proxy_listen, ":")[2])) + local res = assert(client:send { + method = "GET", + path = "/status/200/", + headers = { + ["Host"] = "test.com" + } + }) + assert.res_status(200, res) + + -- Checking the element in the cache + local res = assert(api_client:send { + method = "GET", + path = "/cache/"..cache.all_apis_by_dict_key(), + headers = {} + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.equal(1, pl_tablex.size(body.by_dns)) + assert.is_table(body.by_dns["test.com"]) + + -- Starting second node + assert(exec("start --conf "..helpers.test_conf_path, NODES.servroot2)) + -- Wait for the second node to be registered + helpers.wait_until(function() + return helpers.dao.nodes:count() == 2 + end) + + -- The cache on the first node should be invalidated, and the second node has no cache either because it was never invoked + for _, v in ipairs({NODES.servroot1, NODES.servroot2}) do + local api_client = assert(helpers.http_client("127.0.0.1", pl_stringx.split(v.admin_listen, ":")[2])) + helpers.wait_until(function() + local res = assert(api_client:send { + method = "GET", + path = "/cache/"..cache.all_apis_by_dict_key(), + headers = {} + }) + res:read_body() + return res.status == 404 + end) + end + end) + + it("must purge cache on all nodes when a failed serf starts again (member-join event - simulation of a crash in a 3-node setup)", function() + assert(exec("start --conf "..helpers.test_conf_path, NODES.servroot1)) + assert(exec("start --conf "..helpers.test_conf_path, NODES.servroot2)) + assert(exec("start --conf "..helpers.test_conf_path, NODES.servroot3)) + + local api_client = assert(helpers.http_client("127.0.0.1", pl_stringx.split(NODES.servroot1.admin_listen, ":")[2])) + helpers.wait_until(function() + local res = assert(api_client:send { + method = "GET", + path = "/cluster/", + headers = {} + }) + local body = cjson.decode(assert.res_status(200, res)) + return body.total == 3 + end, 5) + + -- Now we have three nodes connected to each other, let's create and consume an API + -- Adding an API + local api_client = assert(helpers.http_client("127.0.0.1", pl_stringx.split(NODES.servroot1.admin_listen, ":")[2])) + local res = assert(api_client:send { + method = "POST", + path = "/apis/", + headers = { + ["Content-Type"] = "application/json" + }, + body = { + request_host = "test.com", + upstream_url = "http://mockbin.org" + } + }) + assert.res_status(201, res) + + ngx.sleep(5) -- Wait for invalidation of API creation to propagate + + -- Populate the cache on every node + for _, v in ipairs({NODES.servroot1, NODES.servroot2, NODES.servroot3}) do + local client = assert(helpers.http_client("127.0.0.1", pl_stringx.split(v.proxy_listen, ":")[2])) + local res = assert(client:send { + method = "GET", + path = "/status/200", + headers = { + ["Host"] = "test.com" + } + }) + assert.res_status(200, res) + end + + -- Check the cache on every node + for _, v in ipairs({NODES.servroot1, NODES.servroot2, NODES.servroot3}) do + local api_client = assert(helpers.http_client("127.0.0.1", pl_stringx.split(v.admin_listen, ":")[2])) + local res = assert(api_client:send { + method = "GET", + path = "/cache/"..cache.all_apis_by_dict_key(), + headers = {} + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.equal(1, pl_tablex.size(body.by_dns)) + end + + -- The cluster status is "active" for all three nodes + local node_name + for _, v in ipairs({NODES.servroot1, NODES.servroot2, NODES.servroot3}) do + local api_client = assert(helpers.http_client("127.0.0.1", pl_stringx.split(v.admin_listen, ":")[2])) + local res = assert(api_client:send { + method = "GET", + path = "/cluster/", + headers = {} + }) + local body = cjson.decode(assert.res_status(200, res)) + for _, v in ipairs(body.data) do + assert.equal("alive", v.status) + if not node_name and pl_stringx.split(v.address, ":")[2] == pl_stringx.split(NODES.servroot2.cluster_listen, ":")[2] then + node_name = v.name + end + end + end + + -- Kill one Serf + os.execute(string.format("kill `cat %s` >/dev/null 2>&1", NODES.servroot2.prefix.."/pids/serf.pid")) + + -- Wait until the node becomes failed + helpers.wait_until(function() + local res = assert(api_client:send { + method = "GET", + path = "/cluster/", + headers = {} + }) + local body = cjson.decode(assert.res_status(200, res)) + for _, v in ipairs(body.data) do + if v.status == "failed" then + return true + else -- No "left" nodes. It's either "alive" or "failed" + assert.equal("alive", v.status) + end + end + ngx.sleep(1) + end, 60) + + -- The member has now failed, let's bring him up again + os.execute(string.format("serf agent -profile=wan -node=%s -rpc-addr=%s -bind=%s -event-handler=member-join,member-leave,member-failed,member-update,member-reap,user:kong=%s/serf/serf_event.sh > /dev/null &", + node_name, NODES.servroot2.cluster_listen_rpc, NODES.servroot2.cluster_listen, NODES.servroot2.prefix)) + + -- Now wait until the nodes becomes active again + helpers.wait_until(function() + local res = assert(api_client:send { + method = "GET", + path = "/cluster/", + headers = {} + }) + local body = cjson.decode(assert.res_status(200, res)) + local all_alive = true + for _, v in ipairs(body.data) do + if v.status == "failed" then + all_alive = false + end + end + if not all_alive then ngx.sleep(1) end + + return all_alive + end, 60) + + -- The cache should have been deleted on every node available + for _, v in ipairs({NODES.servroot1, NODES.servroot2, NODES.servroot3}) do + local api_client = assert(helpers.http_client("127.0.0.1", pl_stringx.split(v.admin_listen, ":")[2])) + helpers.wait_until(function() + local res = assert(api_client:send { + method = "GET", + path = "/cache/"..cache.all_apis_by_dict_key(), + headers = {} + }) + res:read_body() + return res.status == 404 + end, 5) + end + end) + end) + +end) From 995e3dba267416879d76d2945200082671fe747c Mon Sep 17 00:00:00 2001 From: thefosk Date: Wed, 15 Jun 2016 22:48:18 -0700 Subject: [PATCH 15/29] ACL plugin wip --- kong/cmd/cluster.lua | 5 +- kong/cmd/utils/serf_signals.lua | 17 +- kong/serf.lua | 26 +- .../02-integration/01-cmd/07-cluster_spec.lua | 46 +- spec/03-plugins/acl/access_spec.lua | 444 ++++++++++++++++++ 5 files changed, 465 insertions(+), 73 deletions(-) create mode 100644 spec/03-plugins/acl/access_spec.lua diff --git a/kong/cmd/cluster.lua b/kong/cmd/cluster.lua index 3437b9ad4539..fc18a02aad18 100644 --- a/kong/cmd/cluster.lua +++ b/kong/cmd/cluster.lua @@ -1,7 +1,9 @@ +local pl_app = require "pl.lapp" local conf_loader = require "kong.conf_loader" local DAOFactory = require "kong.dao.factory" local Serf = require "kong.serf" local log = require "kong.cmd.utils.log" +local pl_stringx = require "pl.stringx" local fmt = string.format local function execute(args) @@ -25,7 +27,7 @@ local function execute(args) elseif args.command == "force-leave" then local node_name = args[1] if not node_name then - error("you need to specify the node name to leave") + pl_app.quit("You need to specify the node name to leave") end log(fmt("Force-leaving %s", node_name)) assert(serf:force_leave(node_name)) @@ -44,7 +46,6 @@ The available commands are: Options: -c,--conf (optional string) configuration file - --prefix (optional string) Nginx prefix path ]] return { diff --git a/kong/cmd/utils/serf_signals.lua b/kong/cmd/utils/serf_signals.lua index b2f465c50cff..426e5353b7b3 100644 --- a/kong/cmd/utils/serf_signals.lua +++ b/kong/cmd/utils/serf_signals.lua @@ -8,7 +8,6 @@ local pl_stringx = require "pl.stringx" local pl_utils = require "pl.utils" local pl_path = require "pl.path" local pl_file = require "pl.file" -local pl_dir = require "pl.dir" local kill = require "kong.cmd.utils.kill" local log = require "kong.cmd.utils.log" local utils = require "kong.tools.utils" @@ -18,7 +17,7 @@ local serf_bin_name = "serf" local serf_pid_name = "serf.pid" local serf_node_id = "serf.id" local serf_event_name = "kong" -local start_timeout = 5 +local start_timeout = 2 local function check_serf_bin() local cmd = fmt("%s -v", serf_bin_name) @@ -59,7 +58,7 @@ resty -e "$CMD" ]] local function prepare_identifier(kong_config, nginx_prefix) - local id_path = pl_path.join(nginx_prefix, "serf", serf_node_id) + local id_path = pl_path.join(nginx_prefix, serf_node_id) if not pl_path.exists(id_path) then local id = utils.get_hostname().."_"..kong_config.cluster_listen.."_"..utils.random_string() @@ -71,10 +70,6 @@ local function prepare_identifier(kong_config, nginx_prefix) end local function prepare_prefix(kong_config, nginx_prefix, script_path) - local serf_path = pl_path.join(nginx_prefix, "serf") - local ok, err = pl_dir.makepath(serf_path) - if not ok then return nil, err end - local ok, err = prepare_identifier(kong_config, nginx_prefix) if not ok then return nil, err end @@ -99,7 +94,7 @@ local _M = {} function _M.start(kong_config, nginx_prefix, dao) -- is Serf already running in this prefix? - local pid_path = pl_path.join(nginx_prefix, "pids", serf_pid_name) + local pid_path = pl_path.join(nginx_prefix, serf_pid_name) if is_running(pid_path) then log.verbose("Serf agent already running at %s", pid_path) return true @@ -109,7 +104,7 @@ function _M.start(kong_config, nginx_prefix, dao) end -- prepare shell script - local script_path = pl_path.join(nginx_prefix, "serf", "serf_event.sh") + local script_path = pl_path.join(nginx_prefix, "serf_event.sh") local ok, err = prepare_prefix(kong_config, nginx_prefix, script_path) if not ok then return nil, err end @@ -120,7 +115,7 @@ function _M.start(kong_config, nginx_prefix, dao) local serf = Serf.new(kong_config, nginx_prefix, dao) local node_name = serf.node_name - local log_path = pl_path.join(nginx_prefix, "logs", "serf.log") + local log_path = pl_path.join(nginx_prefix, "serf.log") local args = setmetatable({ ["-bind"] = kong_config.cluster_listen, @@ -183,7 +178,7 @@ function _M.stop(kong_config, nginx_prefix, dao) local ok, err = serf:leave() if not ok then return nil, err end - local pid_path = pl_path.join(nginx_prefix, "pids", serf_pid_name) + local pid_path = pl_path.join(nginx_prefix, serf_pid_name) log.verbose("stopping Serf agent at %s", pid_path) return kill(pid_path, "-9") end diff --git a/kong/serf.lua b/kong/serf.lua index 7eb111ac6e1c..4483adbebc4b 100644 --- a/kong/serf.lua +++ b/kong/serf.lua @@ -24,7 +24,7 @@ Serf.args_mt = { function Serf.new(kong_config, nginx_prefix, dao) return setmetatable({ - node_name = assert(pl_file.read(pl_path.join(nginx_prefix, "serf", serf_node_id))), + node_name = assert(pl_file.read(pl_path.join(nginx_prefix, serf_node_id))), config = kong_config, dao = dao }, Serf) @@ -42,7 +42,7 @@ function Serf:invoke_signal(signal, args, no_rpc) local ok, code, stdout = pl_utils.executeex(cmd) if not ok or code ~= 0 then return nil, pl_stringx.splitlines(stdout)[1] end -- always print the first error line of serf - return pl_stringx.strip(stdout) -- serf adds a new line on the result, so we strip it + return stdout end function Serf:join_node(address) @@ -77,11 +77,15 @@ function Serf:members() end function Serf:keygen() - return self:invoke_signal("keygen") + local res, err = self:invoke_signal("keygen") + if not res then return nil, err end + return res end function Serf:reachability() - return self:invoke_signal("reachability") + local res, err = self:invoke_signal("reachability") + if not res then return nil, err end + return res end function Serf:autojoin() @@ -101,19 +105,7 @@ function Serf:autojoin() local joined for _, v in ipairs(nodes) do if self:join_node(v.cluster_listening_address) then - log("Successfully auto-joined cluster through %s", v.cluster_listening_address) - local members = assert(self:members()) - local active, left, failed = 0, 0, 0 - for _, v in ipairs(members) do - if v.status == "alive" then - active = active + 1 - elseif v.status == "left" then - left = left + 1 - elseif v.status == "failed" then - failed = failed + 1 - end - end - log("The Kong cluster now has %d nodes (%d active, %d left, %d failed)", #members, active, left, failed) + log("Successfully auto-joined %s", v.cluster_listening_address) joined = true break else diff --git a/spec/02-integration/01-cmd/07-cluster_spec.lua b/spec/02-integration/01-cmd/07-cluster_spec.lua index 3da372af55e4..33dcd1bd5d24 100644 --- a/spec/02-integration/01-cmd/07-cluster_spec.lua +++ b/spec/02-integration/01-cmd/07-cluster_spec.lua @@ -1,56 +1,16 @@ local helpers = require "spec.helpers" -local KILL_ALL = "pkill nginx; pkill serf; pkill dnsmasq" - local function exec(args) args = args or "" - return helpers.execute(helpers.bin_path.." "..args.." --prefix "..helpers.test_conf.prefix.." -c "..helpers.test_conf_path) + return helpers.execute(helpers.bin_path.." "..args) end describe("kong cluster", function() - setup(function() - helpers.execute(KILL_ALL) - assert(helpers.start_kong()) - end) - teardown(function() - helpers.execute(KILL_ALL) - helpers.clean_prefix() - end) - it("keygen", function() local ok, _, stdout, stderr = exec "cluster keygen" assert.True(ok) assert.equal("", stderr) - assert.is_string(stdout) - assert.equal(25, string.len(stdout)) -- 25 = 24 for the key, 1 for the newline - end) - it("members", function() - local ok, _, stdout, stderr = exec "cluster members" - assert.True(ok) - assert.equal("", stderr) - assert.is_string(stdout) - assert.True(string.len(stdout) > 10) - end) - it("reachability", function() - local ok, _, stdout, stderr = exec "cluster reachability" - assert.True(ok) - assert.equal("", stderr) - assert.is_string(stdout) - assert.True(string.len(stdout) > 10) - end) - describe("force-leave", function() - it("should fail when no node is specified", function() - local ok, _, stdout, stderr = exec "cluster force-leave" - assert.False(ok) - assert.equal("", stdout) - assert.matches("Error: you need to specify the node name to leave", stderr, nil, true) - end) - it("should work when a node is specified", function() - local ok, _, stdout, stderr = exec "cluster force-leave some-node" - assert.True(ok) - assert.equal("", stderr) - assert.is_string(stdout) - assert.matches("Force-leaving some-node", stdout, nil, true) - end) + print(stdout) + assert.matches("init_by_lua_block", stdout) end) end) diff --git a/spec/03-plugins/acl/access_spec.lua b/spec/03-plugins/acl/access_spec.lua new file mode 100644 index 000000000000..f2854e73c640 --- /dev/null +++ b/spec/03-plugins/acl/access_spec.lua @@ -0,0 +1,444 @@ +local helpers = require "spec.helpers" +local cjson = require "cjson" +local meta = require "kong.meta" +local cache = require "kong.tools.database_cache" + +describe("Plugin: ACL", function() + local client, api_client + setup(function() + helpers.dao:truncate_tables() + helpers.execute "pkill nginx; pkill serf; pkill dnsmasq" + assert(helpers.prepare_prefix()) + + local consumer1 = assert(helpers.dao.consumers:insert { + username = "consumer1" + }) + assert(helpers.dao.keyauth_credentials:insert { + key = "apikey123", + consumer_id = consumer1.id + }) + + local consumer2 = assert(helpers.dao.consumers:insert { + username = "consumer2" + }) + assert(helpers.dao.keyauth_credentials:insert { + key = "apikey124", + consumer_id = consumer2.id + }) + assert(helpers.dao.acls:insert { + group = "admin", + consumer_id = consumer2.id + }) + + local consumer3 = assert(helpers.dao.consumers:insert { + username = "consumer3" + }) + assert(helpers.dao.keyauth_credentials:insert { + key = "apikey125", + consumer_id = consumer3.id + }) + assert(helpers.dao.acls:insert { + group = "pro", + consumer_id = consumer3.id + }) + assert(helpers.dao.acls:insert { + group = "hello", + consumer_id = consumer3.id + }) + + local consumer4 = assert(helpers.dao.consumers:insert { + username = "consumer4" + }) + assert(helpers.dao.keyauth_credentials:insert { + key = "apikey126", + consumer_id = consumer4.id + }) + assert(helpers.dao.acls:insert { + group = "free", + consumer_id = consumer4.id + }) + assert(helpers.dao.acls:insert { + group = "hello", + consumer_id = consumer4.id + }) + + local api1 = assert(helpers.dao.apis:insert { + request_host = "acl1.com", + upstream_url = "http://mockbin.com" + }) + assert(helpers.dao.plugins:insert { + name = "acl", + api_id = api1.id, + config = { + whitelist = "admin" + } + }) + + local api2 = assert(helpers.dao.apis:insert { + request_host = "acl2.com", + upstream_url = "http://mockbin.com" + }) + assert(helpers.dao.plugins:insert { + name = "acl", + api_id = api2.id, + config = { + whitelist = "admin" + } + }) + assert(helpers.dao.plugins:insert { + name = "key-auth", + api_id = api2.id, + config = {} + }) + + local api3 = assert(helpers.dao.apis:insert { + request_host = "acl3.com", + upstream_url = "http://mockbin.com" + }) + assert(helpers.dao.plugins:insert { + name = "acl", + api_id = api3.id, + config = { + blacklist = {"admin"} + } + }) + assert(helpers.dao.plugins:insert { + name = "key-auth", + api_id = api3.id, + config = {} + }) + + local api4 = assert(helpers.dao.apis:insert { + request_host = "acl4.com", + upstream_url = "http://mockbin.com" + }) + assert(helpers.dao.plugins:insert { + name = "acl", + api_id = api4.id, + config = { + whitelist = {"admin", "pro"} + } + }) + assert(helpers.dao.plugins:insert { + name = "key-auth", + api_id = api4.id, + config = {} + }) + + local api5 = assert(helpers.dao.apis:insert { + request_host = "acl5.com", + upstream_url = "http://mockbin.com" + }) + assert(helpers.dao.plugins:insert { + name = "acl", + api_id = api5.id, + config = { + blacklist = {"admin", "pro"} + } + }) + assert(helpers.dao.plugins:insert { + name = "key-auth", + api_id = api5.id, + config = {} + }) + + local api6 = assert(helpers.dao.apis:insert { + request_host = "acl6.com", + upstream_url = "http://mockbin.com" + }) + assert(helpers.dao.plugins:insert { + name = "acl", + api_id = api6.id, + config = { + blacklist = {"admin", "pro", "hello"} + } + }) + assert(helpers.dao.plugins:insert { + name = "key-auth", + api_id = api6.id, + config = {} + }) + + local api7 = assert(helpers.dao.apis:insert { + request_host = "acl7.com", + upstream_url = "http://mockbin.com" + }) + assert(helpers.dao.plugins:insert { + name = "acl", + api_id = api7.id, + config = { + whitelist = {"admin", "pro", "hello"} + } + }) + assert(helpers.dao.plugins:insert { + name = "key-auth", + api_id = api7.id, + config = {} + }) + + assert(helpers.start_kong()) + client = assert(helpers.http_client("127.0.0.1", helpers.test_conf.proxy_port)) + api_client = assert(helpers.http_client("127.0.0.1", helpers.test_conf.admin_port)) + end) + teardown(function() + if client then + client:close() + end + helpers.stop_kong() + --helpers.clean_prefix() + end) + + describe("Simple lists", function() + it("should fail when an authentication plugin is missing", function() + local res = assert(client:send { + method = "GET", + path = "/status/200", + headers = { + ["Host"] = "acl1.com" + } + }) + local body = assert.res_status(403, res) + assert.equal([[{"message":"Cannot identify the consumer, add an authentication plugin to use the ACL plugin"}]], body) + end) + + it("should fail when not in whitelist", function() + local res = assert(client:send { + method = "GET", + path = "/status/200?apikey=apikey123", + headers = { + ["Host"] = "acl2.com" + } + }) + local body = assert.res_status(403, res) + assert.equal([[{"message":"You cannot consume this service"}]], body) + end) + + it("should work when in whitelist", function() + local res = assert(client:send { + method = "GET", + path = "/request?apikey=apikey124", + headers = { + ["Host"] = "acl2.com" + } + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.equal("admin", body.headers["x-consumer-groups"]) + end) + + it("should work when not in blacklist", function() + local res = assert(client:send { + method = "GET", + path = "/request?apikey=apikey123", + headers = { + ["Host"] = "acl3.com" + } + }) + assert.res_status(200, res) + end) + + it("should fail when in blacklist", function() + local res = assert(client:send { + method = "GET", + path = "/request?apikey=apikey124", + headers = { + ["Host"] = "acl3.com" + } + }) + local body = assert.res_status(403, res) + assert.equal([[{"message":"You cannot consume this service"}]], body) + end) + end) + + describe("Multi lists", function() + it("should work when in whitelist", function() + local res = assert(client:send { + method = "GET", + path = "/request?apikey=apikey125", + headers = { + ["Host"] = "acl4.com" + } + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.True(body.headers["x-consumer-groups"] == "pro, hello" or body.headers["x-consumer-groups"] == "hello, pro") + end) + + it("should fail when not in whitelist", function() + local res = assert(client:send { + method = "GET", + path = "/request?apikey=apikey126", + headers = { + ["Host"] = "acl4.com" + } + }) + local body = assert.res_status(403, res) + assert.equal([[{"message":"You cannot consume this service"}]], body) + end) + + it("should fail when in blacklist", function() + local res = assert(client:send { + method = "GET", + path = "/request?apikey=apikey125", + headers = { + ["Host"] = "acl5.com" + } + }) + local body = assert.res_status(403, res) + assert.equal([[{"message":"You cannot consume this service"}]], body) + end) + + it("should work when not in blacklist", function() + local res = assert(client:send { + method = "GET", + path = "/request?apikey=apikey126", + headers = { + ["Host"] = "acl5.com" + } + }) + assert.res_status(200, res) + end) + + it("should not work when one of the ACLs in the blacklist", function() + local res = assert(client:send { + method = "GET", + path = "/request?apikey=apikey126", + headers = { + ["Host"] = "acl6.com" + } + }) + local body = assert.res_status(403, res) + assert.equal([[{"message":"You cannot consume this service"}]], body) + end) + + it("should work when one of the ACLs in the whitelist", function() + local res = assert(client:send { + method = "GET", + path = "/request?apikey=apikey126", + headers = { + ["Host"] = "acl7.com" + } + }) + assert.res_status(200, res) + end) + + it("should not work when at least one of the ACLs in the blacklist", function() + local res = assert(client:send { + method = "GET", + path = "/request?apikey=apikey125", + headers = { + ["Host"] = "acl6.com" + } + }) + local body = assert.res_status(403, res) + assert.equal([[{"message":"You cannot consume this service"}]], body) + end) + end) + + describe("Real-world usage", function() + it("#only should not fail when multiple rules are set fast", function() + -- Create consumer + local res = assert(api_client:send { + method = "POST", + path = "/consumers/", + headers = { + ["Content-Type"] = "application/json" + }, + body = { + username = "acl_consumer" + } + }) + local body = cjson.decode(assert.res_status(201, res)) + local consumer_id = body.id + + -- Create key + local res = assert(api_client:send { + method = "POST", + path = "/consumers/acl_consumer/key-auth/", + headers = { + ["Content-Type"] = "application/json" + }, + body = { + key = "secret123" + } + }) + assert.res_status(201, res) + + for i=1,10 do + -- Create API + local res = assert(api_client:send { + method = "POST", + path = "/apis/", + headers = { + ["Content-Type"] = "application/json" + }, + body = { + name = "acl_test"..i, + request_host = "acl_test"..i..".com", + upstream_url = "http://mockbin.com" + } + }) + assert.res_status(201, res) + + -- Add the ACL plugin to the new API with the new group + local res = assert(api_client:send { + method = "POST", + path = "/apis/acl_test"..i.."/plugins/", + headers = { + ["Content-Type"] = "application/json" + }, + body = { + name = "acl", + ["config.whitelist"] = "admin"..i + } + }) + assert.res_status(201, res) + + -- Add key-authentication to API + local res = assert(api_client:send { + method = "POST", + path = "/apis/acl_test"..i.."/plugins/", + headers = { + ["Content-Type"] = "application/json" + }, + body = { + name = "key-auth" + } + }) + assert.res_status(201, res) + + -- Add a new group the the consumer + local res = assert(api_client:send { + method = "POST", + path = "/consumers/acl_consumer/acls/", + headers = { + ["Content-Type"] = "application/json" + }, + body = { + group = "admin"..i + } + }) + assert.res_status(201, res) + + -- Wait for cache to be invalidated + helpers.wait_until(function() + local res = assert(api_client:send { + method = "GET", + path = "/cache/"..cache.acls_key(consumer_id), + headers = {} + }) + res:read_body() + return res.status == 404 + end, 5) + + -- Make the request, and it should work + local res = assert(client:send { + method = "GET", + path = "/status/200?apikey=secret123", + headers = { + ["Host"] = "acl_test"..i..".com" + } + }) + assert.res_status(200, res) + end + end) + end) +end) From cd4c8559dba006d1869023936e0babcb0a9653c9 Mon Sep 17 00:00:00 2001 From: thefosk Date: Wed, 15 Jun 2016 22:50:52 -0700 Subject: [PATCH 16/29] ACL --- kong/cmd/cluster.lua | 5 ++--- kong/cmd/utils/serf_signals.lua | 4 ++-- spec/03-plugins/acl/access_spec.lua | 3 +-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/kong/cmd/cluster.lua b/kong/cmd/cluster.lua index fc18a02aad18..3437b9ad4539 100644 --- a/kong/cmd/cluster.lua +++ b/kong/cmd/cluster.lua @@ -1,9 +1,7 @@ -local pl_app = require "pl.lapp" local conf_loader = require "kong.conf_loader" local DAOFactory = require "kong.dao.factory" local Serf = require "kong.serf" local log = require "kong.cmd.utils.log" -local pl_stringx = require "pl.stringx" local fmt = string.format local function execute(args) @@ -27,7 +25,7 @@ local function execute(args) elseif args.command == "force-leave" then local node_name = args[1] if not node_name then - pl_app.quit("You need to specify the node name to leave") + error("you need to specify the node name to leave") end log(fmt("Force-leaving %s", node_name)) assert(serf:force_leave(node_name)) @@ -46,6 +44,7 @@ The available commands are: Options: -c,--conf (optional string) configuration file + --prefix (optional string) Nginx prefix path ]] return { diff --git a/kong/cmd/utils/serf_signals.lua b/kong/cmd/utils/serf_signals.lua index 426e5353b7b3..b732a687dd3a 100644 --- a/kong/cmd/utils/serf_signals.lua +++ b/kong/cmd/utils/serf_signals.lua @@ -17,7 +17,7 @@ local serf_bin_name = "serf" local serf_pid_name = "serf.pid" local serf_node_id = "serf.id" local serf_event_name = "kong" -local start_timeout = 2 +local start_timeout = 5 local function check_serf_bin() local cmd = fmt("%s -v", serf_bin_name) @@ -58,7 +58,7 @@ resty -e "$CMD" ]] local function prepare_identifier(kong_config, nginx_prefix) - local id_path = pl_path.join(nginx_prefix, serf_node_id) + local id_path = pl_path.join(nginx_prefix, "serf", serf_node_id) if not pl_path.exists(id_path) then local id = utils.get_hostname().."_"..kong_config.cluster_listen.."_"..utils.random_string() diff --git a/spec/03-plugins/acl/access_spec.lua b/spec/03-plugins/acl/access_spec.lua index f2854e73c640..790d10828bc0 100644 --- a/spec/03-plugins/acl/access_spec.lua +++ b/spec/03-plugins/acl/access_spec.lua @@ -1,6 +1,5 @@ local helpers = require "spec.helpers" local cjson = require "cjson" -local meta = require "kong.meta" local cache = require "kong.tools.database_cache" describe("Plugin: ACL", function() @@ -334,7 +333,7 @@ describe("Plugin: ACL", function() end) describe("Real-world usage", function() - it("#only should not fail when multiple rules are set fast", function() + it("should not fail when multiple rules are set fast", function() -- Create consumer local res = assert(api_client:send { method = "POST", From 8f60c28b389cfd2699188a754970c2fe0a476b13 Mon Sep 17 00:00:00 2001 From: thefosk Date: Thu, 16 Jun 2016 17:41:19 -0700 Subject: [PATCH 17/29] ACL done --- kong/plugins/acl/api.lua | 10 +- .../01-cmd/02-start_stop_spec.lua | 20 +- spec/03-plugins/acl/access_old.lua | 193 ------------ spec/03-plugins/acl/api_old.lua | 118 -------- spec/03-plugins/acl/api_spec.lua | 220 ++++++++++++++ spec/03-plugins/acl/hooks_spec.lua | 279 ++++++++++++++++++ spec/03-plugins/basic-auth/02-api_spec.lua | 10 +- 7 files changed, 521 insertions(+), 329 deletions(-) delete mode 100644 spec/03-plugins/acl/access_old.lua delete mode 100644 spec/03-plugins/acl/api_old.lua create mode 100644 spec/03-plugins/acl/api_spec.lua create mode 100644 spec/03-plugins/acl/hooks_spec.lua diff --git a/kong/plugins/acl/api.lua b/kong/plugins/acl/api.lua index fb1225c43cc6..fd4f97f8c7ca 100644 --- a/kong/plugins/acl/api.lua +++ b/kong/plugins/acl/api.lua @@ -25,13 +25,17 @@ return { crud.find_consumer_by_username_or_id(self, dao_factory, helpers) self.params.consumer_id = self.consumer.id - local err - self.acl, err = dao_factory.acls:find(self.params) + local acls, err = dao_factory.acls:find_all { + consumer_id = self.params.consumer_id, + id = self.params.id + } if err then return helpers.yield_error(err) - elseif self.acl == nil then + elseif next(acls) == nil then return helpers.responses.send_HTTP_NOT_FOUND() end + + self.acl = acls[1] end, GET = function(self, dao_factory, helpers) diff --git a/spec/02-integration/01-cmd/02-start_stop_spec.lua b/spec/02-integration/01-cmd/02-start_stop_spec.lua index bcbd3691aac9..2048cb0a12a9 100644 --- a/spec/02-integration/01-cmd/02-start_stop_spec.lua +++ b/spec/02-integration/01-cmd/02-start_stop_spec.lua @@ -83,6 +83,16 @@ describe("kong start/stop", function() helpers.execute(KILL_ALL) end) end) + it("should start with an inexistent prefix", function() + local ok, _, stdout, stderr = exec "start --prefix foobar" + assert.True(ok) + assert.not_equal("", stdout) + assert.equal("", stderr) + finally(function() + helpers.execute(KILL_ALL) + helpers.dir.rmtree("foobar") + end) + end) end) describe("Serf", function() @@ -151,16 +161,6 @@ describe("kong start/stop", function() assert.is_string(stderr) assert.matches("Error: no file at: foobar.conf", stderr, nil, true) end) - it("start inexistent prefix", function() - local ok, _, stdout, stderr = exec "start --prefix foobar" - assert.True(ok) - assert.not_equal("", stdout) - assert.equal("", stderr) - finally(function() - helpers.execute(KILL_ALL) - helpers.dir.rmtree("foobar") - end) - end) it("stop inexistent prefix", function() assert(helpers.dir.makepath(helpers.test_conf.prefix)) diff --git a/spec/03-plugins/acl/access_old.lua b/spec/03-plugins/acl/access_old.lua deleted file mode 100644 index d92b1763d4b4..000000000000 --- a/spec/03-plugins/acl/access_old.lua +++ /dev/null @@ -1,193 +0,0 @@ -local spec_helper = require "spec.spec_helpers" -local http_client = require "kong.tools.http_client" -local cjson = require "cjson" -local cache = require "kong.tools.database_cache" - -local STUB_GET_URL = spec_helper.STUB_GET_URL -local API_URL = spec_helper.API_URL - -describe("ACL Plugin", function() - - setup(function() - spec_helper.prepare_db() - spec_helper.insert_fixtures { - api = { - {name = "ACL-1", request_host = "acl1.com", upstream_url = "http://mockbin.com"}, - {name = "ACL-2", request_host = "acl2.com", upstream_url = "http://mockbin.com"}, - {name = "ACL-3", request_host = "acl3.com", upstream_url = "http://mockbin.com"}, - {name = "ACL-4", request_host = "acl4.com", upstream_url = "http://mockbin.com"}, - {name = "ACL-5", request_host = "acl5.com", upstream_url = "http://mockbin.com"}, - {name = "ACL-6", request_host = "acl6.com", upstream_url = "http://mockbin.com"}, - {name = "ACL-7", request_host = "acl7.com", upstream_url = "http://mockbin.com"} - }, - consumer = { - {username = "consumer1"}, - {username = "consumer2"}, - {username = "consumer3"}, - {username = "consumer4"} - }, - plugin = { - {name = "acl", config = { whitelist = {"admin"}}, __api = 1}, - {name = "key-auth", config = {key_names = {"apikey"}}, __api = 2}, - {name = "acl", config = { whitelist = {"admin"}}, __api = 2}, - {name = "key-auth", config = {key_names = {"apikey"}}, __api = 3}, - {name = "acl", config = { blacklist = {"admin"}}, __api = 3}, - {name = "key-auth", config = {key_names = {"apikey"}}, __api = 4}, - {name = "acl", config = { whitelist = {"admin", "pro"}}, __api = 4}, - {name = "key-auth", config = {key_names = {"apikey"}}, __api = 5}, - {name = "acl", config = { blacklist = {"admin", "pro"}}, __api = 5}, - {name = "key-auth", config = {key_names = {"apikey"}}, __api = 6}, - {name = "acl", config = { blacklist = {"admin", "pro", "hello"}}, __api = 6}, - {name = "key-auth", config = {key_names = {"apikey"}}, __api = 7}, - {name = "acl", config = { whitelist = {"admin", "pro", "hello"}}, __api = 7} - }, - keyauth_credential = { - {key = "apikey123", __consumer = 1}, - {key = "apikey124", __consumer = 2}, - {key = "apikey125", __consumer = 3}, - {key = "apikey126", __consumer = 4} - }, - acl = { - {group="admin", __consumer = 2}, - {group="pro", __consumer = 3}, - {group="hello", __consumer = 3}, - {group="free", __consumer = 4}, - {group="hello", __consumer = 4}, - } - } - - spec_helper.start_kong() - end) - - teardown(function() - spec_helper.stop_kong() - end) - - describe("Simple lists", function() - it("should fail when an authentication plugin is missing", function() - local response, status = http_client.get(STUB_GET_URL, {}, {host = "acl1.com"}) - local body = cjson.decode(response) - assert.equal(403, status) - assert.equal("Cannot identify the consumer, add an authentication plugin to use the ACL plugin", body.message) - end) - - it("should fail when not in whitelist", function() - local response, status = http_client.get(STUB_GET_URL, {apikey = "apikey123"}, {host = "acl2.com"}) - local body = cjson.decode(response) - assert.equal(403, status) - assert.equal("You cannot consume this service", body.message) - end) - - it("should work when in whitelist", function() - local response, status = http_client.get(STUB_GET_URL, {apikey = "apikey124"}, {host = "acl2.com"}) - assert.equal(200, status) - local body = cjson.decode(response) - assert.equal("admin", body.headers["x-consumer-groups"]) - end) - - it("should work when not in blacklist", function() - local _, status = http_client.get(STUB_GET_URL, {apikey = "apikey123"}, {host = "acl3.com"}) - assert.equal(200, status) - end) - - it("should fail when in blacklist", function() - local response, status = http_client.get(STUB_GET_URL, {apikey = "apikey124"}, {host = "acl3.com"}) - local body = cjson.decode(response) - assert.equal(403, status) - assert.equal("You cannot consume this service", body.message) - end) - end) - - describe("Multi lists", function() - it("should work when in whitelist", function() - local response, status = http_client.get(STUB_GET_URL, {apikey = "apikey125"}, {host = "acl4.com"}) - assert.equal(200, status) - local body = cjson.decode(response) - assert.truthy(body.headers["x-consumer-groups"] == "pro, hello" or body.headers["x-consumer-groups"] == "hello, pro") - end) - - it("should fail when not in whitelist", function() - local response, status = http_client.get(STUB_GET_URL, {apikey = "apikey126"}, {host = "acl4.com"}) - local body = cjson.decode(response) - assert.equal(403, status) - assert.equal("You cannot consume this service", body.message) - end) - - it("should fail when in blacklist", function() - local response, status = http_client.get(STUB_GET_URL, {apikey = "apikey125"}, {host = "acl5.com"}) - local body = cjson.decode(response) - assert.equal(403, status) - assert.equal("You cannot consume this service", body.message) - end) - - it("should work when not in blacklist", function() - local _, status = http_client.get(STUB_GET_URL, {apikey = "apikey126"}, {host = "acl5.com"}) - assert.equal(200, status) - end) - - it("should not work when one of the ACLs in the blacklist", function() - local response, status = http_client.get(STUB_GET_URL, {apikey = "apikey126"}, {host = "acl6.com"}) - local body = cjson.decode(response) - assert.equal(403, status) - assert.equal("You cannot consume this service", body.message) - end) - - it("should work when one of the ACLs in the whitelist", function() - local _, status = http_client.get(STUB_GET_URL, {apikey = "apikey126"}, {host = "acl7.com"}) - assert.equal(200, status) - end) - - it("should not work when at least one of the ACLs in the blacklist", function() - local response, status = http_client.get(STUB_GET_URL, {apikey = "apikey125"}, {host = "acl6.com"}) - local body = cjson.decode(response) - assert.equal(403, status) - assert.equal("You cannot consume this service", body.message) - end) - end) - - describe("Real-world usage", function() - it("#ci should not fail", function() - -- Create consumer - local response, status = http_client.post(API_URL.."/consumers/", {username="acl_consumer"}) - assert.equals(201, status) - local consumer_id = cjson.decode(response).id - assert.truthy(consumer_id) - - -- Create key for consumer - local _, status = http_client.post(API_URL.."/consumers/acl_consumer/key-auth/", {key="secret123"}) - assert.equals(201, status) - - for i=1,10 do - -- Create API - local _, status = http_client.post(API_URL.."/apis/", {name = "acl_test"..i, request_host="acl_test"..i..".com", upstream_url="http://mockbin.com"}) - assert.equals(201, status) - - -- Add the ACL plugin to the new API with the new group - local _, status = http_client.post(API_URL.."/apis/acl_test"..i.."/plugins/", {name="acl", ["config.whitelist"] = "admin"..i}) - assert.equals(201, status) - - -- Add key-authentication to API - local _, status = http_client.post(API_URL.."/apis/acl_test"..i.."/plugins/", {name="key-auth"}) - assert.equals(201, status) - - -- Add a new group the the consumer - local _, status = http_client.post(API_URL.."/consumers/acl_consumer/acls/", {group="admin"..i}) - assert.equals(201, status) - - -- Wait for cache to be invalidated - local exists = true - while(exists) do - local _, status = http_client.get(API_URL.."/cache/"..cache.acls_key(consumer_id)) - if status ~= 200 then - exists = false - end - end - - -- Make the request, and it should work - local _, status = http_client.get(STUB_GET_URL, {apikey = "secret123"}, {host = "acl_test"..i..".com"}) - assert.equal(200, status) - end - end) - end) - -end) \ No newline at end of file diff --git a/spec/03-plugins/acl/api_old.lua b/spec/03-plugins/acl/api_old.lua deleted file mode 100644 index 108b8e23f07f..000000000000 --- a/spec/03-plugins/acl/api_old.lua +++ /dev/null @@ -1,118 +0,0 @@ -local json = require "cjson" -local http_client = require "kong.tools.http_client" -local spec_helper = require "spec.spec_helpers" - -describe("ACLs API", function() - local BASE_URL, acl, consumer - - setup(function() - spec_helper.prepare_db() - spec_helper.start_kong() - end) - - teardown(function() - spec_helper.stop_kong() - end) - - describe("/consumers/:consumer/acls/", function() - - setup(function() - local fixtures = spec_helper.insert_fixtures { - consumer = {{ username = "bob" }} - } - consumer = fixtures.consumer[1] - BASE_URL = spec_helper.API_URL.."/consumers/bob/acls/" - end) - - describe("POST", function() - - it("[FAILURE] should not create an ACL association without a group name", function() - local response, status = http_client.post(BASE_URL, { }) - assert.equal(400, status) - assert.equal("group is required", json.decode(response).group) - end) - - it("[SUCCESS] should create an ACL association", function() - local response, status = http_client.post(BASE_URL, { group = "admin" }) - assert.equal(201, status) - acl = json.decode(response) - assert.equal(consumer.id, acl.consumer_id) - assert.equal("admin", acl.group) - end) - - end) - - describe("PUT", function() - - it("[SUCCESS] should create and update", function() - local response, status = http_client.put(BASE_URL, { group = "pro" }) - assert.equal(201, status) - acl = json.decode(response) - assert.equal(consumer.id, acl.consumer_id) - assert.equal("pro", acl.group) - end) - - end) - - describe("GET", function() - - it("should retrieve all", function() - local response, status = http_client.get(BASE_URL) - assert.equal(200, status) - local body = json.decode(response) - assert.equal(2, #(body.data)) - end) - - end) - - end) - - describe("/consumers/:consumer/acl/:id", function() - - describe("GET", function() - - it("should retrieve by id", function() - local response, status = http_client.get(BASE_URL..acl.id) - assert.equal(200, status) - local body = json.decode(response) - assert.equals(acl.id, body.id) - end) - - end) - - describe("PATCH", function() - - it("[SUCCESS] should update an ACL association", function() - local response, status = http_client.patch(BASE_URL..acl.id, { group = "basic" }) - assert.equal(200, status) - acl = json.decode(response) - assert.equal("basic", acl.group) - end) - - it("[FAILURE] should return proper errors", function() - local response, status = http_client.patch(BASE_URL..acl.id, { group = "" }) - assert.equal(400, status) - assert.equal('{"group":"group is not a string"}\n', response) - end) - - end) - - describe("DELETE", function() - - it("[FAILURE] should return proper errors", function() - local _, status = http_client.delete(BASE_URL.."blah") - assert.equal(400, status) - - _, status = http_client.delete(BASE_URL.."00000000-0000-0000-0000-000000000000") - assert.equal(404, status) - end) - - it("[SUCCESS] should delete an ACL association", function() - local _, status = http_client.delete(BASE_URL..acl.id) - assert.equal(204, status) - end) - - end) - - end) -end) diff --git a/spec/03-plugins/acl/api_spec.lua b/spec/03-plugins/acl/api_spec.lua new file mode 100644 index 000000000000..7174f37d8f5a --- /dev/null +++ b/spec/03-plugins/acl/api_spec.lua @@ -0,0 +1,220 @@ +local helpers = require "spec.helpers" +local cjson = require "cjson" + +describe("ACL API", function() + local consumer, admin_client + setup(function() + helpers.dao:truncate_tables() + helpers.execute "pkill nginx; pkill serf; pkill dnsmasq" + assert(helpers.prepare_prefix()) + assert(helpers.start_kong()) + admin_client = assert(helpers.http_client("127.0.0.1", helpers.test_conf.admin_port)) + end) + teardown(function() + if admin_client then + admin_client:close() + end + helpers.stop_kong() + end) + + describe("/consumers/:consumer/acls/", function() + setup(function() + consumer = assert(helpers.dao.consumers:insert { + username = "bob" + }) + end) + after_each(function() + helpers.dao:truncate_table("acls") + end) + + describe("POST", function() + it("creates an ACL association", function() + local res = assert(admin_client:send { + method = "POST", + path = "/consumers/bob/acls", + body = { + group = "admin" + }, + headers = { + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(201, res) + local json = cjson.decode(body) + assert.equal(consumer.id, json.consumer_id) + assert.equal("admin", json.group) + end) + describe("errors", function() + it("returns bad request", function() + local res = assert(admin_client:send { + method = "POST", + path = "/consumers/bob/acls", + body = {}, + headers = { + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"group":"group is required"}]], body) + end) + end) + end) + + describe("PUT", function() + it("creates a basic-auth credential", function() + local res = assert(admin_client:send { + method = "PUT", + path = "/consumers/bob/acls", + body = { + group = "pro" + }, + headers = { + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(201, res) + local json = cjson.decode(body) + assert.equal(consumer.id, json.consumer_id) + assert.equal("pro", json.group) + end) + describe("errors", function() + it("returns bad request", function() + local res = assert(admin_client:send { + method = "PUT", + path = "/consumers/bob/acls", + body = {}, + headers = { + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"group":"group is required"}]], body) + end) + end) + end) + + describe("GET", function() + setup(function() + for i = 1, 3 do + assert(helpers.dao.acls:insert { + group = "group"..i, + consumer_id = consumer.id + }) + end + end) + teardown(function() + helpers.dao:truncate_table("acls") + end) + it("retrieves the first page", function() + local res = assert(admin_client:send { + method = "GET", + path = "/consumers/bob/acls" + }) + local body = assert.res_status(200, res) + local json = cjson.decode(body) + assert.is_table(json.data) + assert.equal(3, #json.data) + assert.equal(3, json.total) + end) + end) + end) + + describe("/consumers/:consumer/acls/:id", function() + local acl + before_each(function() + helpers.dao:truncate_table("acls") + acl = assert(helpers.dao.acls:insert { + group = "hello", + consumer_id = consumer.id + }) + end) + describe("GET", function() + it("retrieves by id", function() + local res = assert(admin_client:send { + method = "GET", + path = "/consumers/bob/acls/"..acl.id + }) + local body = assert.res_status(200, res) + local json = cjson.decode(body) + assert.equal(acl.id, json.id) + end) + it("retrieves ACL by id only if the ACL belongs to the specified consumer", function() + assert(helpers.dao.consumers:insert { + username = "alice" + }) + + local res = assert(admin_client:send { + method = "GET", + path = "/consumers/bob/acls/"..acl.id + }) + assert.res_status(200, res) + + res = assert(admin_client:send { + method = "GET", + path = "/consumers/alice/acls/"..acl.id + }) + assert.res_status(404, res) + end) + end) + + describe("PATCH", function() + it("updates an ACL group", function() + local previous_group = acl.group + + local res = assert(admin_client:send { + method = "PATCH", + path = "/consumers/bob/acls/"..acl.id, + body = { + group = "updatedGroup" + }, + headers = { + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(200, res) + local json = cjson.decode(body) + assert.not_equal(previous_group, json.group) + end) + describe("errors", function() + it("handles invalid input", function() + local res = assert(admin_client:send { + method = "PATCH", + path = "/consumers/bob/acls/"..acl.id, + body = {}, + headers = { + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"group":"ACL group already exist for this consumer"}]], body) + end) + end) + end) + + describe("DELETE", function() + it("deletes an ACL group", function() + local res = assert(admin_client:send { + method = "DELETE", + path = "/consumers/bob/acls/"..acl.id, + }) + assert.res_status(204, res) + end) + describe("errors", function() + it("returns 400 on invalid input", function() + local res = assert(admin_client:send { + method = "DELETE", + path = "/consumers/bob/acls/blah" + }) + assert.res_status(400, res) + end) + it("returns 404 if not found", function() + local res = assert(admin_client:send { + method = "DELETE", + path = "/consumers/bob/acls/00000000-0000-0000-0000-000000000000" + }) + assert.res_status(404, res) + end) + end) + end) + end) +end) diff --git a/spec/03-plugins/acl/hooks_spec.lua b/spec/03-plugins/acl/hooks_spec.lua new file mode 100644 index 000000000000..23bde538e5bb --- /dev/null +++ b/spec/03-plugins/acl/hooks_spec.lua @@ -0,0 +1,279 @@ +local helpers = require "spec.helpers" +local cache = require "kong.tools.database_cache" + +describe("Plugin hooks: ACL", function() + local admin_client, proxy_client + setup(function() + assert(helpers.prepare_prefix()) + assert(helpers.start_kong()) + proxy_client = assert(helpers.http_client("127.0.0.1", helpers.test_conf.proxy_port)) + admin_client = assert(helpers.http_client("127.0.0.1", helpers.test_conf.admin_port)) + end) + teardown(function() + if admin_client and proxy_client then + admin_client:close() + proxy_client:close() + end + helpers.stop_kong() + end) + + local consumer1, acl1 + before_each(function() + helpers.dao:truncate_tables() + + consumer1 = assert(helpers.dao.consumers:insert { + username = "consumer1" + }) + assert(helpers.dao.keyauth_credentials:insert { + key = "apikey123", + consumer_id = consumer1.id + }) + acl1 = assert(helpers.dao.acls:insert { + group = "admin", + consumer_id = consumer1.id + }) + assert(helpers.dao.acls:insert { + group = "pro", + consumer_id = consumer1.id + }) + + local consumer2 = assert(helpers.dao.consumers:insert { + username = "consumer2" + }) + assert(helpers.dao.keyauth_credentials:insert { + key = "apikey124", + consumer_id = consumer2.id + }) + assert(helpers.dao.acls:insert { + group = "admin", + consumer_id = consumer2.id + }) + + local api1 = assert(helpers.dao.apis:insert { + request_host = "acl1.com", + upstream_url = "http://mockbin.com" + }) + assert(helpers.dao.plugins:insert { + name = "key-auth", + api_id = api1.id + }) + assert(helpers.dao.plugins:insert { + name = "acl", + api_id = api1.id, + config = { + whitelist = {"admin"} + } + }) + + local api2 = assert(helpers.dao.apis:insert { + request_host = "acl2.com", + upstream_url = "http://mockbin.com" + }) + assert(helpers.dao.plugins:insert { + name = "key-auth", + api_id = api2.id + }) + assert(helpers.dao.plugins:insert { + name = "acl", + api_id = api2.id, + config = { + whitelist = {"ya"} + } + }) + + -- Purge cache on every test + local res = assert(admin_client:send { + method = "DELETE", + path = "/cache/", + headers = {} + }) + assert.res_status(204, res) + end) + + describe("ACL entity invalidation", function() + it("should invalidate when ACL entity is deleted", function() + -- It should work + local res = assert(proxy_client:send { + method = "GET", + path = "/status/200?apikey=apikey123", + headers = { + ["Host"] = "acl1.com" + } + }) + assert.res_status(200, res) + + -- Check that the cache is populated + local res = assert(admin_client:send { + method = "GET", + path = "/cache/"..cache.acls_key(consumer1.id), + headers = {} + }) + assert.res_status(200, res) + + -- Delete ACL group (which triggers invalidation) + local res = assert(admin_client:send { + method = "DELETE", + path = "/consumers/consumer1/acls/"..acl1.id, + headers = {} + }) + assert.res_status(204, res) + + -- Wait for cache to be invalidated + helpers.wait_until(function() + local res = assert(admin_client:send { + method = "GET", + path = "/cache/"..cache.acls_key(consumer1.id), + headers = {} + }) + res:read_body() + return res.status == 404 + end, 3) + + -- It should not work + local res = assert(proxy_client:send { + method = "GET", + path = "/status/200?apikey=apikey123", + headers = { + ["Host"] = "acl1.com" + } + }) + assert.res_status(403, res) + end) + it("should invalidate when ACL entity is updated", function() + -- It should work + local res = assert(proxy_client:send { + method = "GET", + path = "/status/200?apikey=apikey123&prova=scemo", + headers = { + ["Host"] = "acl1.com" + } + }) + assert.res_status(200, res) + + -- It should not work + local res = assert(proxy_client:send { + method = "GET", + path = "/status/200?apikey=apikey123", + headers = { + ["Host"] = "acl2.com" + } + }) + assert.res_status(403, res) + + -- Check that the cache is populated + local res = assert(admin_client:send { + method = "GET", + path = "/cache/"..cache.acls_key(consumer1.id), + headers = {} + }) + assert.res_status(200, res) + + -- Update ACL group (which triggers invalidation) + local res = assert(admin_client:send { + method = "PATCH", + path = "/consumers/consumer1/acls/"..acl1.id, + headers = { + ["Content-Type"] = "application/json" + }, + body = { + group = "ya" + } + }) + assert.res_status(200, res) + + -- Wait for cache to be invalidated + helpers.wait_until(function() + local res = assert(admin_client:send { + method = "GET", + path = "/cache/"..cache.acls_key(consumer1.id), + headers = {} + }) + res:read_body() + return res.status == 404 + end, 3) + + -- It should not work + local res = assert(proxy_client:send { + method = "GET", + path = "/status/200?apikey=apikey123", + headers = { + ["Host"] = "acl1.com" + } + }) + assert.res_status(403, res) + + -- It works now + local res = assert(proxy_client:send { + method = "GET", + path = "/status/200?apikey=apikey123", + headers = { + ["Host"] = "acl2.com" + } + }) + assert.res_status(200, res) + end) + end) + + describe("Consumer entity invalidation", function() + it("should invalidate when Consumer entity is deleted", function() + -- It should work + local res = assert(proxy_client:send { + method = "GET", + path = "/status/200?apikey=apikey123", + headers = { + ["Host"] = "acl1.com" + } + }) + assert.res_status(200, res) + + -- Check that the cache is populated + local res = assert(admin_client:send { + method = "GET", + path = "/cache/"..cache.acls_key(consumer1.id), + headers = {} + }) + assert.res_status(200, res) + + -- Delete Consumer (which triggers invalidation) + local res = assert(admin_client:send { + method = "DELETE", + path = "/consumers/consumer1", + headers = {} + }) + assert.res_status(204, res) + + -- Wait for cache to be invalidated + helpers.wait_until(function() + local res = assert(admin_client:send { + method = "GET", + path = "/cache/"..cache.acls_key(consumer1.id), + headers = {} + }) + res:read_body() + return res.status == 404 + end, 3) + + -- Wait for key to be invalidated + helpers.wait_until(function() + local res = assert(admin_client:send { + method = "GET", + path = "/cache/"..cache.keyauth_credential_key("apikey123"), + headers = {} + }) + res:read_body() + return res.status == 404 + end, 3) + + -- It should not work + local res = assert(proxy_client:send { + method = "GET", + path = "/status/200?apikey=apikey123", + headers = { + ["Host"] = "acl1.com" + } + }) + assert.res_status(403, res) + end) + end) + +end) diff --git a/spec/03-plugins/basic-auth/02-api_spec.lua b/spec/03-plugins/basic-auth/02-api_spec.lua index dfe1ee2dc820..466d24c83e87 100644 --- a/spec/03-plugins/basic-auth/02-api_spec.lua +++ b/spec/03-plugins/basic-auth/02-api_spec.lua @@ -27,7 +27,7 @@ describe("Basic Auth Credentials API", function() helpers.dao:truncate_table("basicauth_credentials") end) - describe("#o POST", function() + describe("POST", function() it("creates a basic-auth credential", function() local res = assert(admin_client:send { method = "POST", @@ -85,7 +85,7 @@ describe("Basic Auth Credentials API", function() end) end) - describe("#o PUT", function() + describe("PUT", function() it("creates a basic-auth credential", function() local res = assert(admin_client:send { method = "PUT", @@ -119,7 +119,7 @@ describe("Basic Auth Credentials API", function() end) end) - describe("#o GET", function() + describe("GET", function() setup(function() for i = 1, 3 do assert(helpers.dao.basicauth_credentials:insert { @@ -156,7 +156,7 @@ describe("Basic Auth Credentials API", function() consumer_id = consumer.id }) end) - describe("#o GET", function() + describe("GET", function() it("retrieves basic-auth credential by id", function() local res = assert(admin_client:send { method = "GET", @@ -185,7 +185,7 @@ describe("Basic Auth Credentials API", function() end) end) - describe("#o PATCH", function() + describe("PATCH", function() it("updates a credential", function() local previous_hash = credential.password From 8cc4aa214bb34650f65b0874ae34503c9b90da2d Mon Sep 17 00:00:00 2001 From: thefosk Date: Fri, 17 Jun 2016 17:07:49 -0700 Subject: [PATCH 18/29] rate-limiting wip --- kong/dao/postgres_db.lua | 2 +- spec/03-plugins/acl/hooks_old.lua | 172 -------- spec/03-plugins/rate-limiting/access_spec.lua | 400 ++++++++++++++++++ spec/03-plugins/rate-limiting/api_old.lua | 43 -- spec/03-plugins/rate-limiting/api_spec.lua | 62 +++ .../{daos_old.lua => daos_spec.lua} | 11 +- .../{schema_old.lua => schema_spec.lua} | 0 7 files changed, 467 insertions(+), 223 deletions(-) delete mode 100644 spec/03-plugins/acl/hooks_old.lua create mode 100644 spec/03-plugins/rate-limiting/access_spec.lua delete mode 100644 spec/03-plugins/rate-limiting/api_old.lua create mode 100644 spec/03-plugins/rate-limiting/api_spec.lua rename spec/03-plugins/rate-limiting/{daos_old.lua => daos_spec.lua} (91%) rename spec/03-plugins/rate-limiting/{schema_old.lua => schema_spec.lua} (100%) diff --git a/kong/dao/postgres_db.lua b/kong/dao/postgres_db.lua index 023003870a69..166984dc65a6 100644 --- a/kong/dao/postgres_db.lua +++ b/kong/dao/postgres_db.lua @@ -451,7 +451,7 @@ function PostgresDB:queries(queries) end function PostgresDB:drop_table(table_name) - return select(2, self:query("DROP TABLE "..table_name)) + return select(2, self:query("DROP TABLE "..table_name.." CASCADE")) end function PostgresDB:truncate_table(table_name) diff --git a/spec/03-plugins/acl/hooks_old.lua b/spec/03-plugins/acl/hooks_old.lua deleted file mode 100644 index 7bc704031aee..000000000000 --- a/spec/03-plugins/acl/hooks_old.lua +++ /dev/null @@ -1,172 +0,0 @@ -local json = require "cjson" -local http_client = require "kong.tools.http_client" -local spec_helper = require "spec.spec_helpers" -local cache = require "kong.tools.database_cache" - -local STUB_GET_URL = spec_helper.STUB_GET_URL -local API_URL = spec_helper.API_URL - -describe("ACL Hooks", function() - - setup(function() - spec_helper.prepare_db() - end) - - teardown(function() - spec_helper.stop_kong() - end) - - before_each(function() - spec_helper.restart_kong() - - spec_helper.drop_db() - spec_helper.insert_fixtures { - api = { - {request_host = "acl1.com", upstream_url = "http://mockbin.com"}, - {request_host = "acl2.com", upstream_url = "http://mockbin.com"} - }, - consumer = { - {username = "consumer1"}, - {username = "consumer2"} - }, - plugin = { - {name = "key-auth", config = {key_names = {"apikey"}}, __api = 1}, - {name = "acl", config = { whitelist = {"admin"}}, __api = 1}, - {name = "key-auth", config = {key_names = {"apikey"}}, __api = 2}, - {name = "acl", config = { whitelist = {"ya"}}, __api = 2} - }, - keyauth_credential = { - {key = "apikey123", __consumer = 1}, - {key = "apikey124", __consumer = 2} - }, - acl = { - {group="admin", __consumer = 1}, - {group="pro", __consumer = 1}, - {group="admin", __consumer = 2} - } - } - - end) - - local function get_consumer_id(username) - local response, status = http_client.get(API_URL.."/consumers/consumer1") - assert.equals(200, status) - local consumer_id = json.decode(response).id - assert.truthy(consumer_id) - return consumer_id - end - - local function get_acl_id(consumer_id_or_name, group_name) - local response, status = http_client.get(API_URL.."/consumers/"..consumer_id_or_name.."/acls/", {group=group_name}) - assert.equals(200, status) - local body = json.decode(response) - if #body.data == 1 then - return body.data[1].id - end - end - - describe("ACL entity invalidation", function() - it("should invalidate when ACL entity is deleted", function() - -- It should work - local _, status = http_client.get(STUB_GET_URL, {apikey = "apikey123"}, {host="acl1.com"}) - assert.equals(200, status) - - -- Check that cache is populated - local cache_key = cache.acls_key(get_consumer_id("consumer1")) - local _, status = http_client.get(API_URL.."/cache/"..cache_key) - assert.equals(200, status) - - -- Delete ACL group (which triggers invalidation) - local _, status = http_client.delete(API_URL.."/consumers/consumer1/acls/"..get_acl_id("consumer1", "admin")) - assert.equals(204, status) - - -- Wait for cache to be invalidated - local exists = true - while(exists) do - local _, status = http_client.get(API_URL.."/cache/"..cache_key) - if status ~= 200 then - exists = false - end - end - - -- It should not work - local _, status = http_client.get(STUB_GET_URL, {apikey = "apikey123"}, {host="acl1.com"}) - assert.equals(403, status) - end) - it("should invalidate when ACL entity is updated", function() - -- It should work - local _, status = http_client.get(STUB_GET_URL, {apikey = "apikey123"}, {host="acl1.com"}) - assert.equals(200, status) - - -- It should not work - local _, status = http_client.get(STUB_GET_URL, {apikey = "apikey123"}, {host="acl2.com"}) - assert.equals(403, status) - - -- Check that cache is populated - local cache_key = cache.acls_key(get_consumer_id("consumer1")) - local _, status = http_client.get(API_URL.."/cache/"..cache_key) - assert.equals(200, status) - - -- Update ACL group (which triggers invalidation) - local _, status = http_client.patch(API_URL.."/consumers/consumer1/acls/"..get_acl_id("consumer1", "admin"), {group="ya"}) - assert.equals(200, status) - - -- Wait for cache to be invalidated - local exists = true - while(exists) do - local _, status = http_client.get(API_URL.."/cache/"..cache_key) - if status ~= 200 then - exists = false - end - end - - -- It should not work - local _, status = http_client.get(STUB_GET_URL, {apikey = "apikey123"}, {host="acl1.com"}) - assert.equals(403, status) - - -- It should work now - local _, status = http_client.get(STUB_GET_URL, {apikey = "apikey123"}, {host="acl2.com"}) - assert.equals(200, status) - end) - end) - - describe("Consumer entity invalidation", function() - it("should invalidate when Consumer entity is deleted", function() - -- It should work - local _, status = http_client.get(STUB_GET_URL, {apikey = "apikey123"}, {host="acl1.com"}) - assert.equals(200, status) - - -- Check that cache is populated - local cache_key = cache.acls_key(get_consumer_id("consumer1")) - local _, status = http_client.get(API_URL.."/cache/"..cache_key) - assert.equals(200, status) - - -- Delete Consumer (which triggers invalidation) - local _, status = http_client.delete(API_URL.."/consumers/consumer1") - assert.equals(204, status) - - -- Wait for consumer to be invalidated - local exists = true - while(exists) do - local _, status = http_client.get(API_URL.."/cache/"..cache_key) - if status ~= 200 then - exists = false - end - end - - -- Wait for key to be invalidated - local exists = true - while(exists) do - local _, status = http_client.get(API_URL.."/cache/"..cache.keyauth_credential_key("apikey123")) - if status ~= 200 then - exists = false - end - end - - -- It should not work - local _, status = http_client.get(STUB_GET_URL, {apikey = "apikey123"}, {host="acl1.com"}) - assert.equals(403, status) - end) - end) - -end) diff --git a/spec/03-plugins/rate-limiting/access_spec.lua b/spec/03-plugins/rate-limiting/access_spec.lua new file mode 100644 index 000000000000..9e9f65cebcfd --- /dev/null +++ b/spec/03-plugins/rate-limiting/access_spec.lua @@ -0,0 +1,400 @@ +local helpers = require "spec.helpers" +local cjson = require "cjson" +local timestamp = require "kong.tools.timestamp" +local meta = require "kong.meta" + +local function wait() + -- If the minute elapses in the middle of the test, then the test will + -- fail. So we give it this test 30 seconds to execute, and if the second + -- of the current minute is > 30, then we wait till the new minute kicks in + local current_second = timestamp.get_timetable().sec + if current_second > 20 then + os.execute("sleep "..tostring(60 - current_second)) + end +end + +describe("Plugin: rate-limiting", function() + local client + + local function prepare() + helpers.execute "pkill nginx; pkill serf; pkill dnsmasq" + + helpers.dao:drop_schema() + assert(helpers.dao:run_migrations()) + + local consumer1 = assert(helpers.dao.consumers:insert { + custom_id = "provider_123" + }) + assert(helpers.dao.keyauth_credentials:insert { + key = "apikey122", + consumer_id = consumer1.id + }) + + local consumer2 = assert(helpers.dao.consumers:insert { + custom_id = "provider_124" + }) + assert(helpers.dao.keyauth_credentials:insert { + key = "apikey123", + consumer_id = consumer2.id + }) + + local api1 = assert(helpers.dao.apis:insert { + request_host = "test3.com", + upstream_url = "http://mockbin.com" + }) + assert(helpers.dao.plugins:insert { + name = "key-auth", + api_id = api1.id + }) + assert(helpers.dao.plugins:insert { + name = "rate-limiting", + api_id = api1.id, + config = { minute = 6 } + }) + assert(helpers.dao.plugins:insert { + name = "rate-limiting", + api_id = api1.id, + consumer_id = consumer1.id, + config = { minute = 8 } + }) + + local api2 = assert(helpers.dao.apis:insert { + request_host = "test4.com", + upstream_url = "http://mockbin.com" + }) + assert(helpers.dao.plugins:insert { + name = "rate-limiting", + api_id = api2.id, + config = { minute = 6 } + }) + + local api3 = assert(helpers.dao.apis:insert { + request_host = "test5.com", + upstream_url = "http://mockbin.com" + }) + assert(helpers.dao.plugins:insert { + name = "rate-limiting", + api_id = api3.id, + config = { minute = 3, hour = 5 } + }) + + local api4 = assert(helpers.dao.apis:insert { + request_host = "test6.com", + upstream_url = "http://mockbin.com" + }) + assert(helpers.dao.plugins:insert { + name = "rate-limiting", + api_id = api4.id, + config = { minute = 33 } + }) + + local api5 = assert(helpers.dao.apis:insert { + request_host = "test7.com", + upstream_url = "http://mockbin.com" + }) + assert(helpers.dao.plugins:insert { + name = "rate-limiting", + api_id = api5.id, + config = { minute = 6, async = true } + }) + + local api6 = assert(helpers.dao.apis:insert { + request_host = "test8.com", + upstream_url = "http://mockbin.com" + }) + assert(helpers.dao.plugins:insert { + name = "rate-limiting", + api_id = api6.id, + config = { minute = 6, continue_on_error = false } + }) + + local api7 = assert(helpers.dao.apis:insert { + request_host = "test9.com", + upstream_url = "http://mockbin.com" + }) + assert(helpers.dao.plugins:insert { + name = "rate-limiting", + api_id = api7.id, + config = { minute = 6, continue_on_error = true } + }) + + local api8 = assert(helpers.dao.apis:insert { + request_host = "test10.com", + upstream_url = "http://mockbin.com" + }) + assert(helpers.dao.plugins:insert { + name = "key-auth", + api_id = api8.id + }) + assert(helpers.dao.plugins:insert { + name = "rate-limiting", + api_id = api8.id, + consumer_id = consumer1.id, + config = { minute = 6, continue_on_error = true } + }) + + assert(helpers.start_kong()) + end + + setup(function() + prepare() + wait() + client = assert(helpers.http_client("127.0.0.1", helpers.test_conf.proxy_port)) + end) + teardown(function() + if client then + client:close() + end + helpers.stop_kong() + --helpers.clean_prefix() + end) + + describe("Without authentication (IP address)", function() + it("should get blocked if exceeding limit", function() + -- Default rate-limiting plugin for this API says 6/minute + local limit = 6 + + for i = 1, limit do + local res = assert(client:send { + method = "GET", + path = "/status/200/", + headers = { + ["Host"] = "test4.com" + } + }) + assert.res_status(200, res) + assert.are.same(tostring(limit), res.headers["x-ratelimit-limit-minute"]) + assert.are.same(tostring(limit - i), res.headers["x-ratelimit-remaining-minute"]) + end + + -- Additonal request, while limit is 6/minute + local res = assert(client:send { + method = "GET", + path = "/status/200/", + headers = { + ["Host"] = "test4.com" + } + }) + local body = assert.res_status(429, res) + assert.are.equal([[{"message":"API rate limit exceeded"}]], body) + end) + + it("should handle multiple limits", function() + local limits = { + minute = 3, + hour = 5 + } + + for i = 1, 3 do + local res = assert(client:send { + method = "GET", + path = "/status/200/", + headers = { + ["Host"] = "test5.com" + } + }) + assert.res_status(200, res) + + assert.are.same(tostring(limits.minute), res.headers["x-ratelimit-limit-minute"]) + assert.are.same(tostring(limits.minute - i), res.headers["x-ratelimit-remaining-minute"]) + assert.are.same(tostring(limits.hour), res.headers["x-ratelimit-limit-hour"]) + assert.are.same(tostring(limits.hour - i), res.headers["x-ratelimit-remaining-hour"]) + end + + local res = assert(client:send { + method = "GET", + path = "/status/200/", + headers = { + ["Host"] = "test5.com" + } + }) + local body = assert.res_status(429, res) + assert.are.equal([[{"message":"API rate limit exceeded"}]], body) + assert.are.equal("2", res.headers["x-ratelimit-remaining-hour"]) + assert.are.equal("0", res.headers["x-ratelimit-remaining-minute"]) + end) + end) + + describe("With authentication", function() + describe("Default plugin", function() + it("should get blocked if exceeding limit", function() + -- Default rate-limiting plugin for this API says 6/minute + local limit = 6 + + for i = 1, limit do + local res = assert(client:send { + method = "GET", + path = "/status/200/?apikey=apikey123", + headers = { + ["Host"] = "test3.com" + } + }) + assert.res_status(200, res) + assert.are.same(tostring(limit), res.headers["x-ratelimit-limit-minute"]) + assert.are.same(tostring(limit - i), res.headers["x-ratelimit-remaining-minute"]) + end + + -- Third query, while limit is 2/minute + local res = assert(client:send { + method = "GET", + path = "/status/200/?apikey=apikey123", + headers = { + ["Host"] = "test3.com" + } + }) + local body = assert.res_status(429, res) + assert.are.equal([[{"message":"API rate limit exceeded"}]], body) + end) + end) + + describe("Plugin customized for specific consumer", function() + it("should get blocked if exceeding limit", function() + -- This plugin says this consumer can make 4 requests/minute, not 6 like the default + local limit = 8 + + for i = 1, limit do + local res = assert(client:send { + method = "GET", + path = "/status/200/?apikey=apikey122", + headers = { + ["Host"] = "test3.com" + } + }) + assert.res_status(200, res) + assert.are.same(tostring(limit), res.headers["x-ratelimit-limit-minute"]) + assert.are.same(tostring(limit - i), res.headers["x-ratelimit-remaining-minute"]) + end + + local res = assert(client:send { + method = "GET", + path = "/status/200/?apikey=apikey122", + headers = { + ["Host"] = "test3.com" + } + }) + local body = assert.res_status(429, res) + assert.are.equal([[{"message":"API rate limit exceeded"}]], body) + end) + it("should get blocked if the only rate-limiting plugin existing is per consumer and not per API", function() + -- This plugin says this consumer can make 4 requests/minute, not 6 like the default + local limit = 6 + + for i = 1, limit do + local res = assert(client:send { + method = "GET", + path = "/status/200/?apikey=apikey122", + headers = { + ["Host"] = "test10.com" + } + }) + assert.res_status(200, res) + assert.are.same(tostring(limit), res.headers["x-ratelimit-limit-minute"]) + assert.are.same(tostring(limit - i), res.headers["x-ratelimit-remaining-minute"]) + end + + local res = assert(client:send { + method = "GET", + path = "/status/200/?apikey=apikey122", + headers = { + ["Host"] = "test10.com" + } + }) + local body = assert.res_status(429, res) + assert.are.equal([[{"message":"API rate limit exceeded"}]], body) + end) + end) + end) + + describe("Async increment", function() + it("should increment asynchronously", function() + -- Default rate-limiting plugin for this API says 6/minute + local limit = 6 + + for i = 1, limit do + local res = assert(client:send { + method = "GET", + path = "/status/200/", + headers = { + ["Host"] = "test7.com" + } + }) + assert.res_status(200, res) + assert.are.same(tostring(limit), res.headers["x-ratelimit-limit-minute"]) + assert.are.same(tostring(limit - i), res.headers["x-ratelimit-remaining-minute"]) + ngx.sleep(3) -- Wait for timers to increment + end + + local res = assert(client:send { + method = "GET", + path = "/status/200/", + headers = { + ["Host"] = "test7.com" + } + }) + local body = assert.res_status(429, res) + assert.are.equal([[{"message":"API rate limit exceeded"}]], body) + end) + end) + + describe("Continue on error", function() + after_each(function() + prepare() + end) + it("should not continue if an error occurs", function() + local res = assert(client:send { + method = "GET", + path = "/status/200/", + headers = { + ["Host"] = "test8.com" + } + }) + assert.res_status(200, res) + assert.are.same(tostring(6), res.headers["x-ratelimit-limit-minute"]) + assert.are.same(tostring(5), res.headers["x-ratelimit-remaining-minute"]) + + -- Simulate an error on the database + local err = helpers.dao.ratelimiting_metrics:drop_table(helpers.dao.ratelimiting_metrics.table) + assert.falsy(err) + + -- Make another request + local res = assert(client:send { + method = "GET", + path = "/status/200/", + headers = { + ["Host"] = "test8.com" + } + }) + local body = assert.res_status(500, res) + assert.are.equal([[{"message":"An unexpected error occurred"}]], body) + end) + it("should continue if an error occurs", function() + local res = assert(client:send { + method = "GET", + path = "/status/200/", + headers = { + ["Host"] = "test9.com" + } + }) + assert.res_status(200, res) + assert.falsy(res.headers["x-ratelimit-limit-minute"]) + assert.falsy(res.headers["x-ratelimit-remaining-minute"]) + + -- Simulate an error on the database + local err = helpers.dao.ratelimiting_metrics:drop_table(helpers.dao.ratelimiting_metrics.table) + assert.falsy(err) + + -- Make another request + local res = assert(client:send { + method = "GET", + path = "/status/200/", + headers = { + ["Host"] = "test9.com" + } + }) + assert.res_status(200, res) + assert.falsy(res.headers["x-ratelimit-limit-minute"]) + assert.falsy(res.headers["x-ratelimit-remaining-minute"]) + end) + end) + +end) diff --git a/spec/03-plugins/rate-limiting/api_old.lua b/spec/03-plugins/rate-limiting/api_old.lua deleted file mode 100644 index 6aa8945062fd..000000000000 --- a/spec/03-plugins/rate-limiting/api_old.lua +++ /dev/null @@ -1,43 +0,0 @@ -local json = require "cjson" -local http_client = require "kong.tools.http_client" -local spec_helper = require "spec.spec_helpers" - -local BASE_URL = spec_helper.API_URL.."/apis/%s/plugins/" - -describe("Rate Limiting API", function() - setup(function() - spec_helper.prepare_db() - spec_helper.insert_fixtures { - api = { - { name = "tests-rate-limiting1", request_host = "test1.com", upstream_url = "http://mockbin.com" } - } - } - spec_helper.start_kong() - - local response = http_client.get(spec_helper.API_URL.."/apis/") - BASE_URL = string.format(BASE_URL, json.decode(response).data[1].id) - end) - - teardown(function() - spec_helper.stop_kong() - end) - - describe("POST", function() - - it("should not save with empty config", function() - local response, status = http_client.post(BASE_URL, { name = "rate-limiting" }) - local body = json.decode(response) - assert.are.equal(400, status) - assert.are.equal("You need to set at least one limit: second, minute, hour, day, month, year", body.config) - end) - - it("should save with proper config", function() - local response, status = http_client.post(BASE_URL, { name = "rate-limiting", ["config.second"] = 10 }) - local body = json.decode(response) - assert.are.equal(201, status) - assert.are.equal(10, body.config.second) - end) - - end) - -end) diff --git a/spec/03-plugins/rate-limiting/api_spec.lua b/spec/03-plugins/rate-limiting/api_spec.lua new file mode 100644 index 000000000000..f0c2250479b6 --- /dev/null +++ b/spec/03-plugins/rate-limiting/api_spec.lua @@ -0,0 +1,62 @@ +local helpers = require "spec.helpers" +local cjson = require "cjson" + +describe("Rate Limiting API", function() + local admin_client + setup(function() + helpers.dao:truncate_tables() + helpers.execute "pkill nginx; pkill serf; pkill dnsmasq" + assert(helpers.prepare_prefix()) + assert(helpers.start_kong()) + admin_client = assert(helpers.http_client("127.0.0.1", helpers.test_conf.admin_port)) + end) + teardown(function() + if admin_client then + admin_client:close() + end + helpers.stop_kong() + end) + + describe("POST", function() + setup(function() + assert(helpers.dao.apis:insert { + name = "test", + request_host = "test1.com", + upstream_url = "http://mockbin.com" + }) + end) + + it("should not save with empty config", function() + local res = assert(admin_client:send { + method = "POST", + path = "/apis/test/plugins/", + body = { + name = "rate-limiting" + }, + headers = { + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"config":"You need to set at least one limit: second, minute, hour, day, month, year"}]], body) + end) + + it("should save with proper config", function() + local res = assert(admin_client:send { + method = "POST", + path = "/apis/test/plugins/", + body = { + name = "rate-limiting", + config = { + second = 10 + } + }, + headers = { + ["Content-Type"] = "application/json" + } + }) + local body = cjson.decode(assert.res_status(201, res)) + assert.equal(10, body.config.second) + end) + end) +end) diff --git a/spec/03-plugins/rate-limiting/daos_old.lua b/spec/03-plugins/rate-limiting/daos_spec.lua similarity index 91% rename from spec/03-plugins/rate-limiting/daos_old.lua rename to spec/03-plugins/rate-limiting/daos_spec.lua index c0057a71803e..a4f15c73c54f 100644 --- a/spec/03-plugins/rate-limiting/daos_old.lua +++ b/spec/03-plugins/rate-limiting/daos_spec.lua @@ -1,22 +1,19 @@ -local spec_helper = require "spec.spec_helpers" +local helpers = require "spec.helpers" local timestamp = require "kong.tools.timestamp" local uuid = require "lua_uuid" -local env = spec_helper.get_env() -local dao_factory = env.dao_factory -local ratelimiting_metrics = dao_factory.ratelimiting_metrics +local ratelimiting_metrics = helpers.dao.ratelimiting_metrics describe("Rate Limiting Metrics", function() local api_id = uuid() local identifier = uuid() setup(function() - dao_factory:drop_schema() - spec_helper.prepare_db() + helpers.dao:truncate_tables() end) after_each(function() - spec_helper.drop_db() + helpers.dao:truncate_tables() end) it("should return nil when rate-limiting metrics are not existing", function() diff --git a/spec/03-plugins/rate-limiting/schema_old.lua b/spec/03-plugins/rate-limiting/schema_spec.lua similarity index 100% rename from spec/03-plugins/rate-limiting/schema_old.lua rename to spec/03-plugins/rate-limiting/schema_spec.lua From 378b851652c4335043067058e31ad9fb1810393c Mon Sep 17 00:00:00 2001 From: thefosk Date: Tue, 21 Jun 2016 14:40:42 -0700 Subject: [PATCH 19/29] rate-limiting --- kong/cmd/utils/serf_signals.lua | 11 ++++++++--- kong/serf.lua | 2 +- spec/03-plugins/rate-limiting/access_spec.lua | 8 ++++---- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/kong/cmd/utils/serf_signals.lua b/kong/cmd/utils/serf_signals.lua index b732a687dd3a..c6bb194ad32d 100644 --- a/kong/cmd/utils/serf_signals.lua +++ b/kong/cmd/utils/serf_signals.lua @@ -8,6 +8,7 @@ local pl_stringx = require "pl.stringx" local pl_utils = require "pl.utils" local pl_path = require "pl.path" local pl_file = require "pl.file" +local pl_dir = require "pl.dir" local kill = require "kong.cmd.utils.kill" local log = require "kong.cmd.utils.log" local utils = require "kong.tools.utils" @@ -58,6 +59,10 @@ resty -e "$CMD" ]] local function prepare_identifier(kong_config, nginx_prefix) + local serf_path = pl_path.join(nginx_prefix, "serf") + local ok, err = pl_dir.makepath(serf_path) + if not ok then return nil, err end + local id_path = pl_path.join(nginx_prefix, "serf", serf_node_id) if not pl_path.exists(id_path) then local id = utils.get_hostname().."_"..kong_config.cluster_listen.."_"..utils.random_string() @@ -94,7 +99,7 @@ local _M = {} function _M.start(kong_config, nginx_prefix, dao) -- is Serf already running in this prefix? - local pid_path = pl_path.join(nginx_prefix, serf_pid_name) + local pid_path = pl_path.join(nginx_prefix, "pids", serf_pid_name) if is_running(pid_path) then log.verbose("Serf agent already running at %s", pid_path) return true @@ -104,7 +109,7 @@ function _M.start(kong_config, nginx_prefix, dao) end -- prepare shell script - local script_path = pl_path.join(nginx_prefix, "serf_event.sh") + local script_path = pl_path.join(nginx_prefix, "serf", "serf_event.sh") local ok, err = prepare_prefix(kong_config, nginx_prefix, script_path) if not ok then return nil, err end @@ -115,7 +120,7 @@ function _M.start(kong_config, nginx_prefix, dao) local serf = Serf.new(kong_config, nginx_prefix, dao) local node_name = serf.node_name - local log_path = pl_path.join(nginx_prefix, "serf.log") + local log_path = pl_path.join(nginx_prefix, "logs", "serf.log") local args = setmetatable({ ["-bind"] = kong_config.cluster_listen, diff --git a/kong/serf.lua b/kong/serf.lua index 4483adbebc4b..987df736b21b 100644 --- a/kong/serf.lua +++ b/kong/serf.lua @@ -24,7 +24,7 @@ Serf.args_mt = { function Serf.new(kong_config, nginx_prefix, dao) return setmetatable({ - node_name = assert(pl_file.read(pl_path.join(nginx_prefix, serf_node_id))), + node_name = assert(pl_file.read(pl_path.join(nginx_prefix, "serf", serf_node_id))), config = kong_config, dao = dao }, Serf) diff --git a/spec/03-plugins/rate-limiting/access_spec.lua b/spec/03-plugins/rate-limiting/access_spec.lua index 9e9f65cebcfd..8ed28f1f6596 100644 --- a/spec/03-plugins/rate-limiting/access_spec.lua +++ b/spec/03-plugins/rate-limiting/access_spec.lua @@ -134,12 +134,12 @@ describe("Plugin: rate-limiting", function() }) assert(helpers.start_kong()) + client = assert(helpers.http_client("127.0.0.1", helpers.test_conf.proxy_port)) end setup(function() prepare() wait() - client = assert(helpers.http_client("127.0.0.1", helpers.test_conf.proxy_port)) end) teardown(function() if client then @@ -336,7 +336,7 @@ describe("Plugin: rate-limiting", function() end) end) - describe("Continue on error", function() + describe("#only Continue on error", function() after_each(function() prepare() end) @@ -376,8 +376,8 @@ describe("Plugin: rate-limiting", function() } }) assert.res_status(200, res) - assert.falsy(res.headers["x-ratelimit-limit-minute"]) - assert.falsy(res.headers["x-ratelimit-remaining-minute"]) + assert.are.same(tostring(6), res.headers["x-ratelimit-limit-minute"]) + assert.are.same(tostring(5), res.headers["x-ratelimit-remaining-minute"]) -- Simulate an error on the database local err = helpers.dao.ratelimiting_metrics:drop_table(helpers.dao.ratelimiting_metrics.table) From 0547c092a49c96e6fa6cfd1bd33c84cc7e68612f Mon Sep 17 00:00:00 2001 From: thefosk Date: Wed, 22 Jun 2016 12:08:04 -0700 Subject: [PATCH 20/29] response ratelimiting tests --- spec/03-plugins/rate-limiting/access_old.lua | 234 --------- spec/03-plugins/rate-limiting/access_spec.lua | 4 +- .../response-ratelimiting/access_old.lua | 249 ---------- .../response-ratelimiting/access_spec.lua | 444 ++++++++++++++++++ .../response-ratelimiting/api_old.lua | 43 -- .../response-ratelimiting/api_spec.lua | 64 +++ .../{daos_old.lua => daos_spec.lua} | 11 +- .../{schema_old.lua => schema_spec.lua} | 0 8 files changed, 513 insertions(+), 536 deletions(-) delete mode 100644 spec/03-plugins/rate-limiting/access_old.lua delete mode 100644 spec/03-plugins/response-ratelimiting/access_old.lua create mode 100644 spec/03-plugins/response-ratelimiting/access_spec.lua delete mode 100644 spec/03-plugins/response-ratelimiting/api_old.lua create mode 100644 spec/03-plugins/response-ratelimiting/api_spec.lua rename spec/03-plugins/response-ratelimiting/{daos_old.lua => daos_spec.lua} (91%) rename spec/03-plugins/response-ratelimiting/{schema_old.lua => schema_spec.lua} (100%) diff --git a/spec/03-plugins/rate-limiting/access_old.lua b/spec/03-plugins/rate-limiting/access_old.lua deleted file mode 100644 index 7b2920465c4e..000000000000 --- a/spec/03-plugins/rate-limiting/access_old.lua +++ /dev/null @@ -1,234 +0,0 @@ -local spec_helper = require "spec.spec_helpers" -local http_client = require "kong.tools.http_client" -local timestamp = require "kong.tools.timestamp" -local cjson = require "cjson" - -local env = spec_helper.get_env() -local dao_factory = env.dao_factory - -local STUB_GET_URL = spec_helper.STUB_GET_URL - -local function wait() - -- If the minute elapses in the middle of the test, then the test will - -- fail. So we give it this test 30 seconds to execute, and if the second - -- of the current minute is > 30, then we wait till the new minute kicks in - local current_second = timestamp.get_timetable().sec - if current_second > 20 then - os.execute("sleep "..tostring(60 - current_second)) - end -end - -describe("RateLimiting Plugin", function() - - local function prepare_db() - spec_helper.prepare_db() - spec_helper.insert_fixtures { - api = { - { request_host = "test3.com", upstream_url = "http://mockbin.com" }, - { request_host = "test4.com", upstream_url = "http://mockbin.com" }, - { request_host = "test5.com", upstream_url = "http://mockbin.com" }, - { request_host = "test6.com", upstream_url = "http://mockbin.com" }, - { request_host = "test7.com", upstream_url = "http://mockbin.com" }, - { request_host = "test8.com", upstream_url = "http://mockbin.com" }, - { request_host = "test9.com", upstream_url = "http://mockbin.com" }, - { request_host = "test10.com", upstream_url = "http://mockbin.com" } - }, - consumer = { - { custom_id = "provider_123" }, - { custom_id = "provider_124" } - }, - plugin = { - { name = "key-auth", config = {key_names = {"apikey"}, hide_credentials = true}, __api = 1 }, - { name = "rate-limiting", config = { minute = 6 }, __api = 1 }, - { name = "rate-limiting", config = { minute = 8 }, __api = 1, __consumer = 1 }, - { name = "rate-limiting", config = { minute = 6 }, __api = 2 }, - { name = "rate-limiting", config = { minute = 3, hour = 5 }, __api = 3 }, - { name = "rate-limiting", config = { minute = 33 }, __api = 4 }, - { name = "rate-limiting", config = { minute = 6, async = true }, __api = 5 }, - { name = "rate-limiting", config = { minute = 6, continue_on_error = false }, __api = 6 }, - { name = "rate-limiting", config = { minute = 6, continue_on_error = true }, __api = 7 }, - { name = "key-auth", config = {}, __api = 8 }, - { name = "rate-limiting", config = { minute = 6, continue_on_error = true }, __api = 8, __consumer = 1 } - }, - keyauth_credential = { - { key = "apikey122", __consumer = 1 }, - { key = "apikey123", __consumer = 2 } - } - } - end - - setup(function() - dao_factory:drop_schema() - prepare_db() - spec_helper.start_kong() - wait() - end) - - teardown(function() - spec_helper.stop_kong() - end) - - describe("Without authentication (IP address)", function() - - it("should get blocked if exceeding limit", function() - -- Default rate-limiting plugin for this API says 6/minute - local limit = 6 - - for i = 1, limit do - local _, status, headers = http_client.get(STUB_GET_URL, {}, {host = "test4.com"}) - assert.are.equal(200, status) - assert.are.same(tostring(limit), headers["x-ratelimit-limit-minute"]) - assert.are.same(tostring(limit - i), headers["x-ratelimit-remaining-minute"]) - end - - -- Additonal request, while limit is 6/minute - local response, status = http_client.get(STUB_GET_URL, {}, {host = "test4.com"}) - local body = cjson.decode(response) - assert.are.equal(429, status) - assert.are.equal("API rate limit exceeded", body.message) - end) - - it("should handle multiple limits", function() - local limits = { - minute = 3, - hour = 5 - } - - for i = 1, 3 do - local _, status, headers = http_client.get(STUB_GET_URL, {}, {host = "test5.com"}) - assert.are.equal(200, status) - - assert.are.same(tostring(limits.minute), headers["x-ratelimit-limit-minute"]) - assert.are.same(tostring(limits.minute - i), headers["x-ratelimit-remaining-minute"]) - assert.are.same(tostring(limits.hour), headers["x-ratelimit-limit-hour"]) - assert.are.same(tostring(limits.hour - i), headers["x-ratelimit-remaining-hour"]) - end - - local response, status, headers = http_client.get(STUB_GET_URL, {}, {host = "test5.com"}) - assert.are.equal("2", headers["x-ratelimit-remaining-hour"]) - assert.are.equal("0", headers["x-ratelimit-remaining-minute"]) - local body = cjson.decode(response) - assert.are.equal(429, status) - assert.are.equal("API rate limit exceeded", body.message) - end) - - end) - - describe("With authentication", function() - describe("Default plugin", function() - it("should get blocked if exceeding limit", function() - -- Default rate-limiting plugin for this API says 6/minute - local limit = 6 - - for i = 1, limit do - local _, status, headers = http_client.get(STUB_GET_URL, {apikey = "apikey123"}, {host = "test3.com"}) - assert.are.equal(200, status) - assert.are.same(tostring(limit), headers["x-ratelimit-limit-minute"]) - assert.are.same(tostring(limit - i), headers["x-ratelimit-remaining-minute"]) - end - - -- Third query, while limit is 2/minute - local response, status = http_client.get(STUB_GET_URL, {apikey = "apikey123"}, {host = "test3.com"}) - local body = cjson.decode(response) - assert.are.equal(429, status) - assert.are.equal("API rate limit exceeded", body.message) - end) - end) - - describe("Plugin customized for specific consumer", function() - it("should get blocked if exceeding limit", function() - -- This plugin says this consumer can make 4 requests/minute, not 6 like the default - local limit = 8 - - for i = 1, limit do - local _, status, headers = http_client.get(STUB_GET_URL, {apikey = "apikey122"}, {host = "test3.com"}) - assert.are.equal(200, status) - assert.are.same(tostring(limit), headers["x-ratelimit-limit-minute"]) - assert.are.same(tostring(limit - i), headers["x-ratelimit-remaining-minute"]) - end - - local response, status = http_client.get(STUB_GET_URL, {apikey = "apikey122"}, {host = "test3.com"}) - local body = cjson.decode(response) - assert.are.equal(429, status) - assert.are.equal("API rate limit exceeded", body.message) - end) - it("should get blocked if the only rate-limiting plugin existing is per consumer and not per API", function() - -- This plugin says this consumer can make 4 requests/minute, not 6 like the default - local limit = 6 - - for i = 1, limit do - local _, status, headers = http_client.get(STUB_GET_URL, {apikey = "apikey122"}, {host = "test10.com"}) - assert.are.equal(200, status) - assert.are.same(tostring(limit), headers["x-ratelimit-limit-minute"]) - assert.are.same(tostring(limit - i), headers["x-ratelimit-remaining-minute"]) - end - - local response, status = http_client.get(STUB_GET_URL, {apikey = "apikey122"}, {host = "test10.com"}) - local body = cjson.decode(response) - assert.are.equal(429, status) - assert.are.equal("API rate limit exceeded", body.message) - end) - end) - end) - - describe("Async increment", function() - it("should increment asynchronously", function() - -- Default rate-limiting plugin for this API says 6/minute - local limit = 6 - - for i = 1, limit do - local _, status, headers = http_client.get(STUB_GET_URL, {}, {host = "test7.com"}) - assert.are.equal(200, status) - assert.are.same(tostring(limit), headers["x-ratelimit-limit-minute"]) - assert.are.same(tostring(limit - i), headers["x-ratelimit-remaining-minute"]) - os.execute("sleep 3") -- Wait for timers to increment - end - - local response, status = http_client.get(STUB_GET_URL, {}, {host = "test7.com"}) - local body = cjson.decode(response) - assert.are.equal(429, status) - assert.are.equal("API rate limit exceeded", body.message) - end) - end) - - describe("Continue on error", function() - after_each(function() - dao_factory:drop_schema() - prepare_db() - end) - - it("should not continue if an error occurs", function() - local _, status, headers = http_client.get(STUB_GET_URL, {}, {host = "test8.com"}) - assert.are.equal(200, status) - assert.are.same(tostring(6), headers["x-ratelimit-limit-minute"]) - assert.are.same(tostring(5), headers["x-ratelimit-remaining-minute"]) - - -- Simulate an error on the database - local err = dao_factory.ratelimiting_metrics:drop_table(dao_factory.ratelimiting_metrics.table) - assert.falsy(err) - - -- Make another request - local res, status, _ = http_client.get(STUB_GET_URL, {}, {host = "test8.com"}) - assert.equal("An unexpected error occurred", cjson.decode(res).message) - assert.are.equal(500, status) - end) - - it("should continue if an error occurs", function() - local _, status, headers = http_client.get(STUB_GET_URL, {}, {host = "test9.com"}) - assert.are.equal(200, status) - assert.falsy(headers["x-ratelimit-limit-minute"]) - assert.falsy(headers["x-ratelimit-remaining-minute"]) - - -- Simulate an error on the database - local err = dao_factory.ratelimiting_metrics:drop_table(dao_factory.ratelimiting_metrics.table) - assert.falsy(err) - - -- Make another request - local _, status, headers = http_client.get(STUB_GET_URL, {}, {host = "test9.com"}) - assert.are.equal(200, status) - assert.falsy(headers["x-ratelimit-limit-minute"]) - assert.falsy(headers["x-ratelimit-remaining-minute"]) - end) - end) - -end) diff --git a/spec/03-plugins/rate-limiting/access_spec.lua b/spec/03-plugins/rate-limiting/access_spec.lua index 8ed28f1f6596..2d81e922bac7 100644 --- a/spec/03-plugins/rate-limiting/access_spec.lua +++ b/spec/03-plugins/rate-limiting/access_spec.lua @@ -1,7 +1,5 @@ local helpers = require "spec.helpers" -local cjson = require "cjson" local timestamp = require "kong.tools.timestamp" -local meta = require "kong.meta" local function wait() -- If the minute elapses in the middle of the test, then the test will @@ -336,7 +334,7 @@ describe("Plugin: rate-limiting", function() end) end) - describe("#only Continue on error", function() + describe("Continue on error", function() after_each(function() prepare() end) diff --git a/spec/03-plugins/response-ratelimiting/access_old.lua b/spec/03-plugins/response-ratelimiting/access_old.lua deleted file mode 100644 index 4c35ab504fa6..000000000000 --- a/spec/03-plugins/response-ratelimiting/access_old.lua +++ /dev/null @@ -1,249 +0,0 @@ -local spec_helper = require "spec.spec_helpers" -local http_client = require "kong.tools.http_client" -local timestamp = require "kong.tools.timestamp" -local cjson = require "cjson" - -local env = spec_helper.get_env() -local dao_factory = env.dao_factory - -local PROXY_URL = spec_helper.PROXY_URL -local SLEEP_VALUE = "0.5" - -local function wait() - -- If the minute elapses in the middle of the test, then the test will - -- fail. So we give it this test 30 seconds to execute, and if the second - -- of the current minute is > 30, then we wait till the new minute kicks in - local current_second = timestamp.get_timetable().sec - if current_second > 20 then - os.execute("sleep "..tostring(60 - current_second)) - end -end - -describe("RateLimiting Plugin", function() - - local function prepare_db() - spec_helper.prepare_db() - spec_helper.insert_fixtures { - api = { - { name = "tests-response-ratelimiting1", request_host = "test1.com", upstream_url = "http://httpbin.org/" }, - { name = "tests-response-ratelimiting2", request_host = "test2.com", upstream_url = "http://httpbin.org/" }, - { name = "tests-response-ratelimiting3", request_host = "test3.com", upstream_url = "http://httpbin.org/" }, - { name = "tests-response-ratelimiting4", request_host = "test4.com", upstream_url = "http://httpbin.org/" }, - { name = "tests-response-ratelimiting5", request_host = "test5.com", upstream_url = "http://httpbin.org/" }, - { name = "tests-response-ratelimiting6", request_host = "test6.com", upstream_url = "http://httpbin.org/" }, - { name = "tests-response-ratelimiting7", request_host = "test7.com", upstream_url = "http://httpbin.org/" }, - { name = "tests-response-ratelimiting8", request_host = "test8.com", upstream_url = "http://httpbin.org/" } - }, - consumer = { - { custom_id = "consumer_123" }, - { custom_id = "consumer_124" }, - { custom_id = "consumer_125" } - }, - plugin = { - { name = "response-ratelimiting", config = { limits = { video = { minute = 6 } } }, __api = 1 }, - { name = "response-ratelimiting", config = { limits = { video = { minute = 6, hour = 10 }, image = { minute = 4 } } }, __api = 2 }, - { name = "key-auth", config = {key_names = {"apikey"}, hide_credentials = true}, __api = 3 }, - { name = "response-ratelimiting", config = { limits = { video = { minute = 6 } } }, __api = 3 }, - { name = "response-ratelimiting", config = { limits = { video = { minute = 2 } } }, __api = 3, __consumer = 1 }, - { name = "response-ratelimiting", config = { continue_on_error = false, limits = { video = { minute = 6 } } }, __api = 4 }, - { name = "response-ratelimiting", config = { continue_on_error = true, limits = { video = { minute = 6 } } }, __api = 5 }, - { name = "response-ratelimiting", config = { continue_on_error = true, limits = { video = { minute = 2 } } }, __api = 6 }, - { name = "response-ratelimiting", config = { continue_on_error = false, block_on_first_violation = true, limits = { video = { minute = 6, hour = 10 }, image = { minute = 4 } } }, __api = 7 }, - { name = "response-ratelimiting", config = { limits = { video = { minute = 6, hour = 10 }, image = { minute = 4 } } }, __api = 8 } - }, - keyauth_credential = { - { key = "apikey123", __consumer = 1 }, - { key = "apikey124", __consumer = 2 }, - { key = "apikey125", __consumer = 3 } - } - } - end - - setup(function() - dao_factory:drop_schema() - prepare_db() - spec_helper.start_kong() - wait() - end) - - teardown(function() - spec_helper.stop_kong() - end) - - describe("Without authentication (IP address)", function() - it("should get blocked if exceeding limit", function() - -- Default ratelimiting plugin for this API says 6/minute - local limit = 6 - - for i = 1, limit do - local _, status, headers = http_client.get(PROXY_URL.."/response-headers", {["x-kong-limit"] = "video=1, test=5"}, {host = "test1.com"}) - assert.are.equal(200, status) - assert.are.same(tostring(limit), headers["x-ratelimit-limit-video-minute"]) - assert.are.same(tostring(limit - i), headers["x-ratelimit-remaining-video-minute"]) - os.execute("sleep "..SLEEP_VALUE) -- The increment happens in log_by_lua, give it some time - end - - -- Additonal request, while limit is 6/minute - local _, status = http_client.get(PROXY_URL.."/response-headers", {["x-kong-limit"] = "video=1"}, {host = "test1.com"}) - assert.are.equal(429, status) - end) - - it("should handle multiple limits", function() - for i = 1, 3 do - local _, status, headers = http_client.get(PROXY_URL.."/response-headers", {["x-kong-limit"] = "video=2, image = 1"}, {host = "test2.com"}) - assert.are.equal(200, status) - - assert.are.same(tostring(6), headers["x-ratelimit-limit-video-minute"]) - assert.are.same(tostring(6 - (i * 2)), headers["x-ratelimit-remaining-video-minute"]) - assert.are.same(tostring(10), headers["x-ratelimit-limit-video-hour"]) - assert.are.same(tostring(10 - (i * 2)), headers["x-ratelimit-remaining-video-hour"]) - assert.are.same(tostring(4), headers["x-ratelimit-limit-image-minute"]) - assert.are.same(tostring(4 - i), headers["x-ratelimit-remaining-image-minute"]) - os.execute("sleep "..SLEEP_VALUE) -- The increment happens in log_by_lua, give it some time - end - - local _, status, headers = http_client.get(PROXY_URL.."/response-headers", {["x-kong-limit"] = "video=2, image = 1"}, {host = "test2.com"}) - - assert.are.equal(429, status) - assert.are.equal("0", headers["x-ratelimit-remaining-video-minute"]) - assert.are.equal("4", headers["x-ratelimit-remaining-video-hour"]) - assert.are.equal("1", headers["x-ratelimit-remaining-image-minute"]) - end) - end) - - describe("With authentication", function() - describe("Default plugin", function() - it("should get blocked if exceeding limit and a per consumer setting", function() - -- Default ratelimiting plugin for this API says 6/minute - local limit = 2 - - for i = 1, limit do - local _, status, headers = http_client.get(PROXY_URL.."/response-headers", {apikey = "apikey123", ["x-kong-limit"] = "video=1"}, {host = "test3.com"}) - assert.are.equal(200, status) - assert.are.same(tostring(limit), headers["x-ratelimit-limit-video-minute"]) - assert.are.same(tostring(limit - i), headers["x-ratelimit-remaining-video-minute"]) - os.execute("sleep "..SLEEP_VALUE) -- The increment happens in log_by_lua, give it some time - end - - -- Third query, while limit is 2/minute - local _, status, headers = http_client.get(PROXY_URL.."/response-headers", {apikey = "apikey123", ["x-kong-limit"] = "video=1"}, {host = "test3.com"}) - assert.are.equal(429, status) - assert.are.equal("0", headers["x-ratelimit-remaining-video-minute"]) - assert.are.equal("2", headers["x-ratelimit-limit-video-minute"]) - end) - - it("should not get blocked if the last request doesn't increment", function() - -- Default ratelimiting plugin for this API says 6/minute - local limit = 6 - - for i = 1, limit do - local _, status, headers = http_client.get(PROXY_URL.."/response-headers", {apikey = "apikey124", ["x-kong-limit"] = "video=1"}, {host = "test3.com"}) - assert.are.equal(200, status) - assert.are.same(tostring(limit), headers["x-ratelimit-limit-video-minute"]) - assert.are.same(tostring(limit - i), headers["x-ratelimit-remaining-video-minute"]) - os.execute("sleep "..SLEEP_VALUE) -- The increment happens in log_by_lua, give it some time - end - - -- Third query, while limit is 2/minute - local _, status, headers = http_client.get(PROXY_URL.."/response-headers", {apikey = "apikey124"}, {host = "test3.com"}) - assert.are.equal(200, status) - assert.are.equal("0", headers["x-ratelimit-remaining-video-minute"]) - assert.are.equal("6", headers["x-ratelimit-limit-video-minute"]) - end) - - it("should get blocked if exceeding limit", function() - -- Default ratelimiting plugin for this API says 6/minute - local limit = 6 - - for i = 1, limit do - local _, status, headers = http_client.get(PROXY_URL.."/response-headers", {apikey = "apikey125", ["x-kong-limit"] = "video=1"}, {host = "test3.com"}) - assert.are.equal(200, status) - assert.are.same(tostring(limit), headers["x-ratelimit-limit-video-minute"]) - assert.are.same(tostring(limit - i), headers["x-ratelimit-remaining-video-minute"]) - os.execute("sleep "..SLEEP_VALUE) -- The increment happens in log_by_lua, give it some time - end - - -- Third query, while limit is 2/minute - local _, status, headers = http_client.get(PROXY_URL.."/response-headers", {apikey = "apikey125", ["x-kong-limit"] = "video=1"}, {host = "test3.com"}) - assert.are.equal(429, status) - assert.are.equal("0", headers["x-ratelimit-remaining-video-minute"]) - assert.are.equal("6", headers["x-ratelimit-limit-video-minute"]) - end) - end) - end) - - describe("Upstream usage headers", function() - it("should append the headers with multiple limits", function() - local response, status = http_client.get(PROXY_URL.."/get", {}, {host = "test8.com"}) - assert.are.equal(200, status) - local body = cjson.decode(response) - assert.are.equal("4", body.headers["X-Ratelimit-Remaining-Image"]) - assert.are.equal("6", body.headers["X-Ratelimit-Remaining-Video"]) - - -- Actually consume the limits - local _, status = http_client.get(PROXY_URL.."/response-headers", {["x-kong-limit"] = "video=2, image=1"}, {host = "test8.com"}) - assert.are.equal(200, status) - - os.execute("sleep "..SLEEP_VALUE) -- The increment happens in log_by_lua, give it some time - - local response, status = http_client.get(PROXY_URL.."/get", {}, {host = "test8.com"}) - assert.are.equal(200, status) - local body = cjson.decode(response) - - assert.are.equal("3", body.headers["X-Ratelimit-Remaining-Image"]) - assert.are.equal("4", body.headers["X-Ratelimit-Remaining-Video"]) - end) - end) - - it("should block on first violation", function() - local _, status = http_client.get(PROXY_URL.."/response-headers", {["x-kong-limit"] = "image=4, video=2"}, {host = "test7.com"}) - assert.are.equal(200, status) - - os.execute("sleep "..SLEEP_VALUE) -- The increment happens in log_by_lua, give it some time - - local response, status = http_client.get(PROXY_URL.."/response-headers", {["x-kong-limit"] = "video=2"}, {host = "test7.com"}) - assert.are.equal(429, status) - assert.are.equal("API rate limit exceeded for 'image'", cjson.decode(response).message) - end) - - describe("Continue on error", function() - after_each(function() - dao_factory:drop_schema() - prepare_db() - end) - - it("should not continue if an error occurs", function() - local _, status, headers = http_client.get(PROXY_URL.."/response-headers", {["x-kong-limit"] = "video=1"}, {host = "test4.com"}) - assert.are.equal(200, status) - assert.are.same('6', headers["x-ratelimit-limit-video-minute"]) - assert.are.same('5', headers["x-ratelimit-remaining-video-minute"]) - - -- Simulate an error on the database - local err = dao_factory.response_ratelimiting_metrics:drop_table(dao_factory.response_ratelimiting_metrics.table) - assert.falsy(err) - - -- Make another request - local res, status, _ = http_client.get(PROXY_URL.."/response-headers", {["x-kong-limit"] = "video=1"}, {host = "test4.com"}) - assert.equal("An unexpected error occurred", cjson.decode(res).message) - assert.are.equal(500, status) - end) - - it("should continue if an error occurs", function() - local _, status, headers = http_client.get(PROXY_URL.."/response-headers", {["x-kong-limit"] = "video=1"}, {host = "test5.com"}) - assert.are.equal(200, status) - assert.falsy(headers["x-ratelimit-limit-video-minute"]) - assert.falsy(headers["x-ratelimit-remaining-video-minute"]) - - -- Simulate an error on the database - local err = dao_factory.response_ratelimiting_metrics:drop_table(dao_factory.response_ratelimiting_metrics.table) - assert.falsy(err) - - -- Make another request - local _, status, headers = http_client.get(PROXY_URL.."/response-headers", {["x-kong-limit"] = "video=1"}, {host = "test5.com"}) - assert.are.equal(200, status) - assert.falsy(headers["x-ratelimit-limit-video-minute"]) - assert.falsy(headers["x-ratelimit-remaining-video-minute"]) - end) - end) - -end) diff --git a/spec/03-plugins/response-ratelimiting/access_spec.lua b/spec/03-plugins/response-ratelimiting/access_spec.lua new file mode 100644 index 000000000000..043296f2ab7a --- /dev/null +++ b/spec/03-plugins/response-ratelimiting/access_spec.lua @@ -0,0 +1,444 @@ +local helpers = require "spec.helpers" +local cjson = require "cjson" +local timestamp = require "kong.tools.timestamp" + +local SLEEP_VALUE = "0.5" + +local function wait() + -- If the minute elapses in the middle of the test, then the test will + -- fail. So we give it this test 30 seconds to execute, and if the second + -- of the current minute is > 30, then we wait till the new minute kicks in + local current_second = timestamp.get_timetable().sec + if current_second > 20 then + os.execute("sleep "..tostring(60 - current_second)) + end +end + +describe("Plugin: response-ratelimiting", function() + local client + + local function prepare() + helpers.execute "pkill nginx; pkill serf; pkill dnsmasq" + + helpers.dao:drop_schema() + assert(helpers.dao:run_migrations()) + + local consumer1 = assert(helpers.dao.consumers:insert { + custom_id = "provider_123" + }) + assert(helpers.dao.keyauth_credentials:insert { + key = "apikey123", + consumer_id = consumer1.id + }) + + local consumer2 = assert(helpers.dao.consumers:insert { + custom_id = "provider_124" + }) + assert(helpers.dao.keyauth_credentials:insert { + key = "apikey124", + consumer_id = consumer2.id + }) + + local consumer3 = assert(helpers.dao.consumers:insert { + custom_id = "provider_125" + }) + assert(helpers.dao.keyauth_credentials:insert { + key = "apikey125", + consumer_id = consumer3.id + }) + + local api1 = assert(helpers.dao.apis:insert { + request_host = "test1.com", + upstream_url = "http://httpbin.org" + }) + assert(helpers.dao.plugins:insert { + name = "response-ratelimiting", + api_id = api1.id, + config = { limits = { video = { minute = 6 } } } + }) + + local api2 = assert(helpers.dao.apis:insert { + request_host = "test2.com", + upstream_url = "http://httpbin.org" + }) + assert(helpers.dao.plugins:insert { + name = "response-ratelimiting", + api_id = api2.id, + config = { limits = { video = { minute = 6, hour = 10 }, image = { minute = 4 } } } + }) + + local api3 = assert(helpers.dao.apis:insert { + request_host = "test3.com", + upstream_url = "http://httpbin.org" + }) + assert(helpers.dao.plugins:insert { + name = "key-auth", + api_id = api3.id + }) + assert(helpers.dao.plugins:insert { + name = "response-ratelimiting", + api_id = api3.id, + config = { limits = { video = { minute = 6 } } } + }) + assert(helpers.dao.plugins:insert { + name = "response-ratelimiting", + api_id = api3.id, + consumer_id = consumer1.id, + config = { limits = { video = { minute = 2 } } } + }) + + local api4 = assert(helpers.dao.apis:insert { + request_host = "test4.com", + upstream_url = "http://httpbin.org" + }) + assert(helpers.dao.plugins:insert { + name = "response-ratelimiting", + api_id = api4.id, + config = { continue_on_error = false, limits = { video = { minute = 6 } } } + }) + + local api5 = assert(helpers.dao.apis:insert { + request_host = "test5.com", + upstream_url = "http://httpbin.org" + }) + assert(helpers.dao.plugins:insert { + name = "response-ratelimiting", + api_id = api5.id, + config = { continue_on_error = true, limits = { video = { minute = 6 } } } + }) + + local api6 = assert(helpers.dao.apis:insert { + request_host = "test6.com", + upstream_url = "http://httpbin.org" + }) + assert(helpers.dao.plugins:insert { + name = "response-ratelimiting", + api_id = api6.id, + config = { continue_on_error = true, limits = { video = { minute = 2 } } } + }) + + local api7 = assert(helpers.dao.apis:insert { + request_host = "test7.com", + upstream_url = "http://httpbin.org" + }) + assert(helpers.dao.plugins:insert { + name = "response-ratelimiting", + api_id = api7.id, + config = { continue_on_error = false, block_on_first_violation = true, limits = { video = { minute = 6, hour = 10 }, image = { minute = 4 } } } + }) + + local api8 = assert(helpers.dao.apis:insert { + request_host = "test8.com", + upstream_url = "http://httpbin.org" + }) + assert(helpers.dao.plugins:insert { + name = "response-ratelimiting", + api_id = api8.id, + config = { limits = { video = { minute = 6, hour = 10 }, image = { minute = 4 } } } + }) + + assert(helpers.start_kong()) + end + + setup(function() + prepare() + wait() + end) + teardown(function() + if client then + client:close() + end + helpers.stop_kong() + --helpers.clean_prefix() + end) + before_each(function() + client = assert(helpers.http_client("127.0.0.1", helpers.test_conf.proxy_port)) + end) + + describe("Without authentication (IP address)", function() + it("should get blocked if exceeding limit", function() + -- Default ratelimiting plugin for this API says 6/minute + local limit = 6 + + for i = 1, limit do + local res = assert(client:send { + method = "GET", + path = "/response-headers?x-kong-limit=video=1, test=5", + headers = { + ["Host"] = "test1.com" + } + }) + assert.res_status(200, res) + assert.are.same(tostring(limit), res.headers["x-ratelimit-limit-video-minute"]) + assert.are.same(tostring(limit - i), res.headers["x-ratelimit-remaining-video-minute"]) + + ngx.sleep(SLEEP_VALUE) -- The increment happens in log_by_lua, give it some time + end + + local res = assert(client:send { + method = "GET", + path = "/response-headers?x-kong-limit=video=1", + headers = { + ["Host"] = "test1.com" + } + }) + local body = assert.res_status(429, res) + assert.are.equal([[]], body) + end) + + it("should handle multiple limits", function() + for i = 1, 3 do + local res = assert(client:send { + method = "GET", + path = "/response-headers?x-kong-limit=video=2, image=1", + headers = { + ["Host"] = "test2.com" + } + }) + assert.res_status(200, res) + assert.are.same(tostring(6), res.headers["x-ratelimit-limit-video-minute"]) + assert.are.same(tostring(6 - (i * 2)), res.headers["x-ratelimit-remaining-video-minute"]) + assert.are.same(tostring(10), res.headers["x-ratelimit-limit-video-hour"]) + assert.are.same(tostring(10 - (i * 2)), res.headers["x-ratelimit-remaining-video-hour"]) + assert.are.same(tostring(4), res.headers["x-ratelimit-limit-image-minute"]) + assert.are.same(tostring(4 - i), res.headers["x-ratelimit-remaining-image-minute"]) + + ngx.sleep(SLEEP_VALUE) -- The increment happens in log_by_lua, give it some time + end + + local res = assert(client:send { + method = "GET", + path = "/response-headers?x-kong-limit=video=2, image=1", + headers = { + ["Host"] = "test2.com" + } + }) + local body = assert.res_status(429, res) + assert.are.equal([[]], body) + assert.are.equal("0", res.headers["x-ratelimit-remaining-video-minute"]) + assert.are.equal("4", res.headers["x-ratelimit-remaining-video-hour"]) + assert.are.equal("1", res.headers["x-ratelimit-remaining-image-minute"]) + end) + end) + + describe("With authentication", function() + describe("Default plugin", function() + it("should get blocked if exceeding limit and a per consumer setting", function() + -- Default ratelimiting plugin for this API says 6/minute + local limit = 2 + + for i = 1, limit do + local res = assert(client:send { + method = "GET", + path = "/response-headers?apikey=apikey123&x-kong-limit=video=1", + headers = { + ["Host"] = "test3.com" + } + }) + assert.res_status(200, res) + assert.are.same(tostring(limit), res.headers["x-ratelimit-limit-video-minute"]) + assert.are.same(tostring(limit - i), res.headers["x-ratelimit-remaining-video-minute"]) + ngx.sleep(SLEEP_VALUE) -- The increment happens in log_by_lua, give it some time + end + + -- Third query, while limit is 2/minute + local res = assert(client:send { + method = "GET", + path = "/response-headers?apikey=apikey123&x-kong-limit=video=1", + headers = { + ["Host"] = "test3.com" + } + }) + local body = assert.res_status(429, res) + assert.are.equal([[]], body) + assert.are.equal("0", res.headers["x-ratelimit-remaining-video-minute"]) + assert.are.equal("2", res.headers["x-ratelimit-limit-video-minute"]) + end) + + it("should get blocked if exceeding limit and a per consumer setting", function() + -- Default ratelimiting plugin for this API says 6/minute + local limit = 6 + + for i = 1, limit do + local res = assert(client:send { + method = "GET", + path = "/response-headers?apikey=apikey124&x-kong-limit=video=1", + headers = { + ["Host"] = "test3.com" + } + }) + assert.res_status(200, res) + assert.are.same(tostring(limit), res.headers["x-ratelimit-limit-video-minute"]) + assert.are.same(tostring(limit - i), res.headers["x-ratelimit-remaining-video-minute"]) + ngx.sleep(SLEEP_VALUE) -- The increment happens in log_by_lua, give it some time + end + + local res = assert(client:send { + method = "GET", + path = "/response-headers?apikey=apikey124", + headers = { + ["Host"] = "test3.com" + } + }) + assert.res_status(200, res) + assert.are.equal("0", res.headers["x-ratelimit-remaining-video-minute"]) + assert.are.equal("6", res.headers["x-ratelimit-limit-video-minute"]) + end) + + it("should get blocked if exceeding limit", function() + -- Default ratelimiting plugin for this API says 6/minute + local limit = 6 + + for i = 1, limit do + local res = assert(client:send { + method = "GET", + path = "/response-headers?apikey=apikey125&x-kong-limit=video=1", + headers = { + ["Host"] = "test3.com" + } + }) + assert.res_status(200, res) + assert.are.same(tostring(limit), res.headers["x-ratelimit-limit-video-minute"]) + assert.are.same(tostring(limit - i), res.headers["x-ratelimit-remaining-video-minute"]) + ngx.sleep(SLEEP_VALUE) -- The increment happens in log_by_lua, give it some time + end + + -- Third query, while limit is 2/minute + local res = assert(client:send { + method = "GET", + path = "/response-headers?apikey=apikey125&x-kong-limit=video=1", + headers = { + ["Host"] = "test3.com" + } + }) + local body = assert.res_status(429, res) + assert.are.equal([[]], body) + assert.are.equal("0", res.headers["x-ratelimit-remaining-video-minute"]) + assert.are.equal("6", res.headers["x-ratelimit-limit-video-minute"]) + end) + end) + end) + + describe("Upstream usage headers", function() + it("should append the headers with multiple limits", function() + local res = assert(client:send { + method = "GET", + path = "/get", + headers = { + ["Host"] = "test8.com" + } + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.are.equal("4", body.headers["X-Ratelimit-Remaining-Image"]) + assert.are.equal("6", body.headers["X-Ratelimit-Remaining-Video"]) + + -- Actually consume the limits + local res = assert(client:send { + method = "GET", + path = "/response-headers?x-kong-limit=video=2, image=1", + headers = { + ["Host"] = "test8.com" + } + }) + assert.res_status(200, res) + ngx.sleep(SLEEP_VALUE) -- The increment happens in log_by_lua, give it some time + + local res = assert(client:send { + method = "GET", + path = "/get", + headers = { + ["Host"] = "test8.com" + } + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.are.equal("3", body.headers["X-Ratelimit-Remaining-Image"]) + assert.are.equal("4", body.headers["X-Ratelimit-Remaining-Video"]) + end) + end) + + it("should block on first violation", function() + local res = assert(client:send { + method = "GET", + path = "/response-headers?x-kong-limit=video=2, image=4", + headers = { + ["Host"] = "test7.com" + } + }) + assert.res_status(200, res) + ngx.sleep(SLEEP_VALUE) -- The increment happens in log_by_lua, give it some time + + local res = assert(client:send { + method = "GET", + path = "/response-headers?x-kong-limit=video=2", + headers = { + ["Host"] = "test7.com" + } + }) + local body = assert.res_status(429, res) + assert.are.equal([[{"message":"API rate limit exceeded for 'image'"}]], body) + end) + + describe("Continue on error", function() + after_each(function() + prepare() + end) + + it("should not continue if an error occurs", function() + local res = assert(client:send { + method = "GET", + path = "/response-headers?x-kong-limit=video=1", + headers = { + ["Host"] = "test4.com" + } + }) + assert.res_status(200, res) + assert.are.same("6", res.headers["x-ratelimit-limit-video-minute"]) + assert.are.same("5", res.headers["x-ratelimit-remaining-video-minute"]) + + -- Simulate an error on the database + local err = helpers.dao.response_ratelimiting_metrics:drop_table(helpers.dao.response_ratelimiting_metrics.table) + assert.falsy(err) + + -- Make another request + local res = assert(client:send { + method = "GET", + path = "/response-headers?x-kong-limit=video=1", + headers = { + ["Host"] = "test4.com" + } + }) + local body = assert.res_status(500, res) + assert.are.equal([[{"message":"An unexpected error occurred"}]], body) + end) + + it("should continue if an error occurs", function() + local res = assert(client:send { + method = "GET", + path = "/response-headers?x-kong-limit=video=1", + headers = { + ["Host"] = "test5.com" + } + }) + assert.res_status(200, res) + assert.are.same("6", res.headers["x-ratelimit-limit-video-minute"]) + assert.are.same("5", res.headers["x-ratelimit-remaining-video-minute"]) + + -- Simulate an error on the database + local err = helpers.dao.response_ratelimiting_metrics:drop_table(helpers.dao.response_ratelimiting_metrics.table) + assert.falsy(err) + + -- Make another request + local res = assert(client:send { + method = "GET", + path = "/response-headers?x-kong-limit=video=1", + headers = { + ["Host"] = "test5.com" + } + }) + assert.res_status(200, res) + assert.is_nil(res.headers["x-ratelimit-limit-video-minute"]) + assert.is_nil(res.headers["x-ratelimit-remaining-video-minute"]) + end) + end) + +end) diff --git a/spec/03-plugins/response-ratelimiting/api_old.lua b/spec/03-plugins/response-ratelimiting/api_old.lua deleted file mode 100644 index 057583a3f56e..000000000000 --- a/spec/03-plugins/response-ratelimiting/api_old.lua +++ /dev/null @@ -1,43 +0,0 @@ -local json = require "cjson" -local http_client = require "kong.tools.http_client" -local spec_helper = require "spec.spec_helpers" - -local BASE_URL = spec_helper.API_URL.."/apis/%s/plugins/" - -describe("Response Rate Limiting API", function() - setup(function() - spec_helper.prepare_db() - spec_helper.insert_fixtures { - api = { - { name = "tests-response-ratelimiting1", request_host = "test1.com", upstream_url = "http://mockbin.com" } - } - } - spec_helper.start_kong() - - local response = http_client.get(spec_helper.API_URL.."/apis/") - BASE_URL = string.format(BASE_URL, json.decode(response).data[1].id) - end) - - teardown(function() - spec_helper.stop_kong() - end) - - describe("POST", function() - - it("should not save with empty config", function() - local response, status = http_client.post(BASE_URL, { name = "response-ratelimiting" }) - local body = json.decode(response) - assert.are.equal(400, status) - assert.are.equal("You need to set at least one limit name", body.config) - end) - - it("should save with proper config", function() - local response, status = http_client.post(BASE_URL, { name = "response-ratelimiting", ["config.limits.video.second"] = 10 }) - local body = json.decode(response) - assert.are.equal(201, status) - assert.are.equal(10, body.config.limits.video.second) - end) - - end) - -end) diff --git a/spec/03-plugins/response-ratelimiting/api_spec.lua b/spec/03-plugins/response-ratelimiting/api_spec.lua new file mode 100644 index 000000000000..640c72f0237c --- /dev/null +++ b/spec/03-plugins/response-ratelimiting/api_spec.lua @@ -0,0 +1,64 @@ +local helpers = require "spec.helpers" +local cjson = require "cjson" + +describe("Response Rate Limiting API", function() + local admin_client + setup(function() + helpers.dao:truncate_tables() + helpers.execute "pkill nginx; pkill serf; pkill dnsmasq" + assert(helpers.prepare_prefix()) + assert(helpers.start_kong()) + admin_client = assert(helpers.http_client("127.0.0.1", helpers.test_conf.admin_port)) + end) + teardown(function() + if admin_client then + admin_client:close() + end + helpers.stop_kong() + end) + + describe("POST", function() + setup(function() + assert(helpers.dao.apis:insert { + name = "test", + request_host = "test1.com", + upstream_url = "http://mockbin.com" + }) + end) + + it("should not save with empty config", function() + local res = assert(admin_client:send { + method = "POST", + path = "/apis/test/plugins/", + body = { + name = "response-ratelimiting" + }, + headers = { + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"config":"You need to set at least one limit name"}]], body) + end) + + it("should save with proper config", function() + local res = assert(admin_client:send { + method = "POST", + path = "/apis/test/plugins/", + body = { + name = "response-ratelimiting", + config = { + limits = { + video = {second = 10} + } + } + }, + headers = { + ["Content-Type"] = "application/json" + } + }) + local body = cjson.decode(assert.res_status(201, res)) + assert.equal(10, body.config.limits.video.second) + end) + end) +end) diff --git a/spec/03-plugins/response-ratelimiting/daos_old.lua b/spec/03-plugins/response-ratelimiting/daos_spec.lua similarity index 91% rename from spec/03-plugins/response-ratelimiting/daos_old.lua rename to spec/03-plugins/response-ratelimiting/daos_spec.lua index c62aecffcb24..8e33804354d8 100644 --- a/spec/03-plugins/response-ratelimiting/daos_old.lua +++ b/spec/03-plugins/response-ratelimiting/daos_spec.lua @@ -1,22 +1,19 @@ -local spec_helper = require "spec.spec_helpers" +local helpers = require "spec.helpers" local timestamp = require "kong.tools.timestamp" local uuid = require "lua_uuid" -local env = spec_helper.get_env() -local dao_factory = env.dao_factory -local response_ratelimiting_metrics = dao_factory.response_ratelimiting_metrics +local response_ratelimiting_metrics = helpers.dao.response_ratelimiting_metrics describe("Rate Limiting Metrics", function() local api_id = uuid() local identifier = uuid() setup(function() - dao_factory:drop_schema() - spec_helper.prepare_db() + helpers.dao:truncate_tables() end) after_each(function() - spec_helper.drop_db() + helpers.dao:truncate_tables() end) it("should return nil when ratelimiting metrics are not existing", function() diff --git a/spec/03-plugins/response-ratelimiting/schema_old.lua b/spec/03-plugins/response-ratelimiting/schema_spec.lua similarity index 100% rename from spec/03-plugins/response-ratelimiting/schema_old.lua rename to spec/03-plugins/response-ratelimiting/schema_spec.lua From 1f4f7587a095d5c96614c2ba1c1914168a625472 Mon Sep 17 00:00:00 2001 From: thefosk Date: Wed, 22 Jun 2016 12:33:10 -0700 Subject: [PATCH 21/29] fixing some specs --- kong/cmd/utils/serf_signals.lua | 2 +- .../02-integration/01-cmd/07-cluster_spec.lua | 3 +- .../02-integration/06-cluster/cluster_old.lua | 466 ------------------ 3 files changed, 2 insertions(+), 469 deletions(-) delete mode 100644 spec/02-integration/06-cluster/cluster_old.lua diff --git a/kong/cmd/utils/serf_signals.lua b/kong/cmd/utils/serf_signals.lua index c6bb194ad32d..2192deb7ef8e 100644 --- a/kong/cmd/utils/serf_signals.lua +++ b/kong/cmd/utils/serf_signals.lua @@ -183,7 +183,7 @@ function _M.stop(kong_config, nginx_prefix, dao) local ok, err = serf:leave() if not ok then return nil, err end - local pid_path = pl_path.join(nginx_prefix, serf_pid_name) + local pid_path = pl_path.join(nginx_prefix, "pids", serf_pid_name) log.verbose("stopping Serf agent at %s", pid_path) return kill(pid_path, "-9") end diff --git a/spec/02-integration/01-cmd/07-cluster_spec.lua b/spec/02-integration/01-cmd/07-cluster_spec.lua index 33dcd1bd5d24..8e6f03978c4d 100644 --- a/spec/02-integration/01-cmd/07-cluster_spec.lua +++ b/spec/02-integration/01-cmd/07-cluster_spec.lua @@ -10,7 +10,6 @@ describe("kong cluster", function() local ok, _, stdout, stderr = exec "cluster keygen" assert.True(ok) assert.equal("", stderr) - print(stdout) - assert.matches("init_by_lua_block", stdout) + assert.equal(26, stdout:len()) -- 24 + \r\n end) end) diff --git a/spec/02-integration/06-cluster/cluster_old.lua b/spec/02-integration/06-cluster/cluster_old.lua deleted file mode 100644 index 1bd39f5c4007..000000000000 --- a/spec/02-integration/06-cluster/cluster_old.lua +++ /dev/null @@ -1,466 +0,0 @@ -local spec_helper = require "spec.spec_helpers" -local yaml = require "yaml" -local IO = require "kong.tools.io" -local http_client = require "kong.tools.http_client" -local cjson = require "cjson" -local stringy = require "stringy" -local cache = require "kong.tools.database_cache" -local utils = require "kong.tools.utils" - -local TEST_CONF = spec_helper.get_env().conf_file - -local SERVERS = { - ["kong_CLUSTER_1.yml"] = { - nginx_working_dir = "nginx_tmp_1", - proxy_listen = "0.0.0.0:9000", - proxy_listen_ssl = "0.0.0.0:9443", - admin_api_listen = "0.0.0.0:9001", - cluster_listen = "0.0.0.0:9946", - cluster_listen_rpc = "0.0.0.0:9373", - dns_resolvers_available = { - dnsmasq = {port = 8054} - } - }, - ["kong_CLUSTER_2.yml"] = { - nginx_working_dir = "nginx_tmp_2", - proxy_listen = "0.0.0.0:10000", - proxy_listen_ssl = "0.0.0.0:10443", - admin_api_listen = "0.0.0.0:10001", - cluster_listen = "0.0.0.0:10946", - cluster_listen_rpc = "0.0.0.0:10373", - dns_resolvers_available = { - dnsmasq = {port = 10054} - } - }, - ["kong_CLUSTER_3.yml"] = { - nginx_working_dir = "nginx_tmp_3", - proxy_listen = "0.0.0.0:20000", - proxy_listen_ssl = "0.0.0.0:20443", - admin_api_listen = "0.0.0.0:20001", - cluster_listen = "0.0.0.0:20946", - cluster_listen_rpc = "0.0.0.0:20373", - dns_resolvers_available = { - dnsmasq = {port = 20054} - } - } -} - -local SERVER_CONFS = {} -for k, _ in pairs(SERVERS) do - table.insert(SERVER_CONFS, k) -end - -local function replace_properties(t, output_file) - local yaml_value = yaml.load(IO.read_file(output_file)) - for k, v in pairs(t) do - if type(v) == "table" then - if not yaml_value[k] then yaml_value[k] = {} end - for sub_k, sub_v in pairs(v) do - yaml_value[k][sub_k] = sub_v - end - else - yaml_value[k] = v - end - end - local new_config_content = yaml.dump(yaml_value) - - -- Workaround for https://github.com/lubyk/yaml/issues/2 - -- This workaround is in two places. To remove it "Find and replace" in the code - new_config_content = string.gsub(new_config_content, "(%w+:%s*)([%w%.]+:%d+)", "%1\"%2\"") - - local ok = IO.write_to_file(output_file, new_config_content) - assert.truthy(ok) -end - -describe("Cluster", function() - - before_each(function() - spec_helper.prepare_db() - - for k, v in pairs(SERVERS) do - os.execute("cp "..TEST_CONF.." "..k.." && mkdir -p "..v.nginx_working_dir) - replace_properties(v, k) - spec_helper.add_env(k) - end - end) - - after_each(function() - for k, v in pairs(SERVERS) do - pcall(spec_helper.stop_kong, k) - os.execute("rm "..k.." && rm -rf "..v.nginx_working_dir) - spec_helper.remove_env(k) - end - end) - - describe("Nodes", function() - it("should register the node on startup", function() - local _, exit_code = spec_helper.start_kong(SERVER_CONFS[1]) - assert.are.same(0, exit_code) - - local api_url = "http://127.0.0.1:"..stringy.split(SERVERS[SERVER_CONFS[1]].admin_api_listen, ":")[2] - - local _, status = http_client.get(api_url) - assert.equal(200, status) -- is running - - while(#spec_helper.envs[SERVER_CONFS[1]].dao_factory.nodes:find_all() ~= 1) do - -- Wait - end - - local res, err = spec_helper.envs[SERVER_CONFS[1]].dao_factory.nodes:find_all() - assert.falsy(err) - assert.equal(1, #res) - assert.truthy(res[1].created_at) - assert.truthy(res[1].name) - assert.truthy(res[1].cluster_listening_address) - - local res, status = http_client.get(api_url.."/cluster") - assert.equal(200, status) - assert.equal(1, cjson.decode(res).total) - end) - it("should register the node on startup with the advertised address", function() - -- Changing advertise property - local properties = utils.deep_copy(SERVERS[SERVER_CONFS[1]]) - properties.cluster = { advertise = "5.5.5.5:1234" } - replace_properties(properties, SERVER_CONFS[1]) - - local _, exit_code = spec_helper.start_kong(SERVER_CONFS[1]) - assert.are.same(0, exit_code) - - local api_url = "http://127.0.0.1:"..stringy.split(SERVERS[SERVER_CONFS[1]].admin_api_listen, ":")[2] - local _, status = http_client.get(api_url) - assert.equal(200, status) -- is running - - while(#spec_helper.envs[SERVER_CONFS[1]].dao_factory.nodes:find_all() ~= 1) do - -- Wait - end - - local res, err = spec_helper.envs[SERVER_CONFS[1]].dao_factory.nodes:find_all() - assert.falsy(err) - assert.equal(1, #res) - assert.truthy(res[1].created_at) - assert.truthy(res[1].name) - assert.truthy(res[1].cluster_listening_address) - assert.equal("5.5.5.5:1234", res[1].cluster_listening_address) - - local res, status = http_client.get(api_url.."/cluster") - assert.equal(200, status) - assert.equal(1, cjson.decode(res).total) - assert.equal("5.5.5.5:1234", cjson.decode(res).data[1].address) - end) - end) - - describe("Auto-join", function() - it("should register the second node on startup and auto-join sequentially", function() - local _, exit_code = spec_helper.start_kong(SERVER_CONFS[1]) - assert.are.same(0, exit_code) - - local api_url1 = "http://127.0.0.1:"..stringy.split(SERVERS[SERVER_CONFS[1]].admin_api_listen, ":")[2] - local _, status = http_client.get(api_url1) - assert.equal(200, status) -- is running - - while(#spec_helper.envs[SERVER_CONFS[1]].dao_factory.nodes:find_all() ~= 1) do - -- Wait - end - - local _, exit_code = spec_helper.start_kong(SERVER_CONFS[2]) - assert.are.same(0, exit_code) - - local api_url2 = "http://127.0.0.1:"..stringy.split(SERVERS[SERVER_CONFS[2]].admin_api_listen, ":")[2] - local _, status = http_client.get(api_url2) - assert.equal(200, status) -- is running - - while(#spec_helper.envs[SERVER_CONFS[1]].dao_factory.nodes:find_all() ~= 2) do - -- Wait - end - - local res, err = spec_helper.envs[SERVER_CONFS[1]].dao_factory.nodes:find_all() - assert.falsy(err) - assert.equal(2, #res) - assert.truthy(res[1].created_at) - assert.truthy(res[1].name) - assert.truthy(res[1].cluster_listening_address) - assert.truthy(res[2].created_at) - assert.truthy(res[2].name) - assert.truthy(res[2].cluster_listening_address) - - local total - repeat - local res, status = http_client.get(api_url1.."/cluster") - assert.equal(200, status) - total = cjson.decode(res).total - until(total == 2) - - local res, status = http_client.get(api_url1.."/cluster") - assert.equal(200, status) - assert.equal(2, cjson.decode(res).total) - - local res, status = http_client.get(api_url2.."/cluster") - assert.equal(200, status) - assert.equal(2, cjson.decode(res).total) - end) - - it("should register the second node on startup and auto-join asyncronously", function() - local _, exit_code = spec_helper.start_kong(SERVER_CONFS[1]) - assert.are.same(0, exit_code) - - local _, exit_code = spec_helper.start_kong(SERVER_CONFS[2]) - assert.are.same(0, exit_code) - - while(#spec_helper.envs[SERVER_CONFS[1]].dao_factory.nodes:find_all() ~= 2) do - -- Wait - end - - -- We need to wait a few seconds for the async job kick in and join the nodes together - os.execute("sleep 5") - - local res, err = spec_helper.envs[SERVER_CONFS[1]].dao_factory.nodes:find_all() - assert.falsy(err) - assert.equal(2, #res) - assert.truthy(res[1].created_at) - assert.truthy(res[1].name) - assert.truthy(res[1].cluster_listening_address) - assert.truthy(res[2].created_at) - assert.truthy(res[2].name) - assert.truthy(res[2].cluster_listening_address) - - local api_url1 = "http://127.0.0.1:"..stringy.split(SERVERS[SERVER_CONFS[1]].admin_api_listen, ":")[2] - local api_url2 = "http://127.0.0.1:"..stringy.split(SERVERS[SERVER_CONFS[2]].admin_api_listen, ":")[2] - local total - repeat - local res, status = http_client.get(api_url1.."/cluster") - assert.equal(200, status) - total = cjson.decode(res).total - until(total == 2) - - local res, status = http_client.get(api_url1.."/cluster") - assert.equal(200, status) - assert.equal(2, cjson.decode(res).total) - - local res, status = http_client.get(api_url2.."/cluster") - assert.equal(200, status) - assert.equal(2, cjson.decode(res).total) - end) - - it("should not join the second node on startup when auto-join is false", function() - -- Changing auto-join property - local properties = utils.deep_copy(SERVERS[SERVER_CONFS[1]]) - properties.cluster = { ["auto-join"] = false } - replace_properties(properties, SERVER_CONFS[1]) - properties = utils.deep_copy(SERVERS[SERVER_CONFS[2]]) - properties.cluster = { ["auto-join"] = false } - replace_properties(properties, SERVER_CONFS[2]) - - local _, exit_code = spec_helper.start_kong(SERVER_CONFS[1]) - assert.are.same(0, exit_code) - - while(#spec_helper.envs[SERVER_CONFS[1]].dao_factory.nodes:find_all() ~= 1) do - -- Wait - end - - local _, exit_code = spec_helper.start_kong(SERVER_CONFS[2]) - assert.are.same(0, exit_code) - - while(#spec_helper.envs[SERVER_CONFS[1]].dao_factory.nodes:find_all() ~= 2) do - -- Wait - end - - local res, err = spec_helper.envs[SERVER_CONFS[1]].dao_factory.nodes:find_all() - assert.falsy(err) - assert.equal(2, #res) - assert.truthy(res[1].created_at) - assert.truthy(res[1].name) - assert.truthy(res[1].cluster_listening_address) - assert.truthy(res[2].created_at) - assert.truthy(res[2].name) - assert.truthy(res[2].cluster_listening_address) - - local api_url1 = "http://127.0.0.1:"..stringy.split(SERVERS[SERVER_CONFS[1]].admin_api_listen, ":")[2] - local api_url2 = "http://127.0.0.1:"..stringy.split(SERVERS[SERVER_CONFS[2]].admin_api_listen, ":")[2] - local total - repeat - local res, status = http_client.get(api_url1.."/cluster") - assert.equal(200, status) - total = cjson.decode(res).total - until(total == 1) - - local res, status = http_client.get(api_url1.."/cluster") - assert.equal(200, status) - assert.equal(1, cjson.decode(res).total) - - local res, status = http_client.get(api_url2.."/cluster") - assert.equal(200, status) - assert.equal(1, cjson.decode(res).total) - end) - end) - - describe("Cache purges", function() - it("must purge cache on all nodes on member-join", function() - local _, exit_code = spec_helper.start_kong(SERVER_CONFS[1]) - assert.are.equal(0, exit_code) - - while(#spec_helper.envs[SERVER_CONFS[1]].dao_factory.nodes:find_all() ~= 1) do - -- Wait - end - - local proxy_url1 = "http://127.0.0.1:"..stringy.split(SERVERS[SERVER_CONFS[1]].proxy_listen, ":")[2] - local api_url1 = "http://127.0.0.1:"..stringy.split(SERVERS[SERVER_CONFS[1]].admin_api_listen, ":")[2] - local api_url2 = "http://127.0.0.1:"..stringy.split(SERVERS[SERVER_CONFS[2]].admin_api_listen, ":")[2] - - -- Adding an API - local _, status = http_client.post(api_url1.."/apis", {request_host="test.com", upstream_url="http://mockbin.org"}) - assert.equal(201, status) - - -- Wait for invalidation of API creation to propagate - os.execute("sleep 5") - - -- Populating the cache - local _, status = http_client.get(proxy_url1.."/request", {}, {host = "test.com"}) - assert.equal(200, status) - - -- Checking the element in the cache - local _, status = http_client.get(api_url1.."/cache/"..cache.all_apis_by_dict_key()) - assert.equal(200, status) - - -- Starting second node - local _, exit_code = spec_helper.start_kong(SERVER_CONFS[2]) - assert.are.equal(0, exit_code) - - while(#spec_helper.envs[SERVER_CONFS[2]].dao_factory.nodes:find_all() ~= 2) do - -- Wait - end - - -- Wait for event to propagate - local status - repeat - _, status = http_client.get(api_url1.."/cache/"..cache.all_apis_by_dict_key()) - until(status ~= 200) - - -- The cache on the first node should be invalidated - local _, status = http_client.get(api_url1.."/cache/"..cache.all_apis_by_dict_key()) - assert.equal(404, status) - - -- And the second node has no cache either because it was never invoked - local _, status = http_client.get(api_url2.."/cache/"..cache.all_apis_by_dict_key()) - assert.equal(404, status) - end) - - it("must purge cache on all nodes when a failed serf starts again (member-join event - simulation of a crash in a 3-node setup)", function() - local _, exit_code = spec_helper.start_kong(SERVER_CONFS[1]) - assert.are.same(0, exit_code) - - local _, exit_code = spec_helper.start_kong(SERVER_CONFS[2]) - assert.are.same(0, exit_code) - - local _, exit_code = spec_helper.start_kong(SERVER_CONFS[3]) - assert.are.same(0, exit_code) - - while(#spec_helper.envs[SERVER_CONFS[1]].dao_factory.nodes:find_all() ~= 3) do - -- Wait - end - - local api_url1 = "http://127.0.0.1:"..stringy.split(SERVERS[SERVER_CONFS[1]].admin_api_listen, ":")[2] - local api_url2 = "http://127.0.0.1:"..stringy.split(SERVERS[SERVER_CONFS[2]].admin_api_listen, ":")[2] - local api_url3 = "http://127.0.0.1:"..stringy.split(SERVERS[SERVER_CONFS[3]].admin_api_listen, ":")[2] - local total - repeat - local res, status = http_client.get(api_url1.."/cluster") - assert.equals(200, status) - total = cjson.decode(res).total - until(total == 3) - - -- Now we have three nodes connected to each other, let's create and consume an API - -- Adding an API - local _, status = http_client.post(api_url1.."/apis", {request_host="test.com", upstream_url="http://mockbin.org"}) - assert.equal(201, status) - - os.execute("sleep 5") - - local proxy_url1 = "http://127.0.0.1:"..stringy.split(SERVERS[SERVER_CONFS[1]].proxy_listen, ":")[2] - local proxy_url2 = "http://127.0.0.1:"..stringy.split(SERVERS[SERVER_CONFS[2]].proxy_listen, ":")[2] - local proxy_url3 = "http://127.0.0.1:"..stringy.split(SERVERS[SERVER_CONFS[3]].proxy_listen, ":")[2] - - -- Populate the cache - local _, status = http_client.get(proxy_url1.."/request", {}, {host = "test.com"}) - assert.equal(200, status) - local _, status = http_client.get(proxy_url2.."/request", {}, {host = "test.com"}) - assert.equal(200, status) - local _, status = http_client.get(proxy_url3.."/request", {}, {host = "test.com"}) - assert.equal(200, status) - - -- Check the cache - local _, status = http_client.get(api_url1.."/cache/"..cache.all_apis_by_dict_key(), {}) - assert.equal(200, status) - local _, status = http_client.get(api_url2.."/cache/"..cache.all_apis_by_dict_key(), {}) - assert.equal(200, status) - local _, status = http_client.get(api_url3.."/cache/"..cache.all_apis_by_dict_key(), {}) - assert.equal(200, status) - - -- The status is active for all three - local res = http_client.get(api_url1.."/cluster") - for _, v in ipairs(cjson.decode(res).data) do - assert.equals("alive", v.status) - end - - -- Kill one serf - local serf_pid = IO.read_file(SERVERS[SERVER_CONFS[2]].nginx_working_dir.."/serf.pid") - assert.truthy(serf_pid) - os.execute("kill -9 "..serf_pid) - - -- Now wait until the node becomes failed - local has_failed - local node_name - repeat - local res, status = http_client.get(api_url1.."/cluster") - assert.equals(200, status) - local body = cjson.decode(res) - for _, v in ipairs(body.data) do - if v.status == "failed" then - has_failed = true - node_name = v.name - break - end - end - os.execute("sleep 1") - until(has_failed) - - -- The member has now failed, let's bring him up again - local current_dir = IO.os_execute("pwd") - os.execute("serf agent -profile=local -node="..node_name.." -rpc-addr="..SERVERS[SERVER_CONFS[2]].cluster_listen_rpc.." -bind="..SERVERS[SERVER_CONFS[2]].cluster_listen.." -event-handler=member-join,member-leave,member-failed,member-update,member-reap,user:kong="..current_dir.."/"..SERVERS[SERVER_CONFS[2]].nginx_working_dir.."/serf_event.sh > /dev/null &") - -- Now wait until the node becomes active again - repeat - local res, status = http_client.get(api_url1.."/cluster") - assert.equals(200, status) - local body = cjson.decode(res) - local all_alive = true - for _, v in ipairs(body.data) do - if v.status == "failed" then - all_alive = false - break - end - end - os.execute("sleep 1") - until(not all_alive) - - -- The cache should have been deleted on every node - -- Wait for event to propagate - local all_invalidated - repeat - local _, status1 = http_client.get(api_url1.."/cache/"..cache.all_apis_by_dict_key()) - local _, status2 = http_client.get(api_url2.."/cache/"..cache.all_apis_by_dict_key()) - local _, status3 = http_client.get(api_url3.."/cache/"..cache.all_apis_by_dict_key()) - all_invalidated = (status1 == 404) and (status2 == 404) and (status3 == 404) - os.execute("sleep 1") - until(all_invalidated) - - -- The cache on every node should be invalidated - local _, status = http_client.get(api_url1.."/cache/"..cache.all_apis_by_dict_key()) - assert.equal(404, status) - local _, status = http_client.get(api_url2.."/cache/"..cache.all_apis_by_dict_key()) - assert.equal(404, status) - local _, status = http_client.get(api_url3.."/cache/"..cache.all_apis_by_dict_key()) - assert.equal(404, status) - - os.execute("pkill -9 serf") -- Kill the serf we just started - end) - end) -end) \ No newline at end of file From 9702ede636e5fab52df6789391fea3cdaf474819 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Thu, 16 Jun 2016 12:55:42 -0700 Subject: [PATCH 22/29] feat(conf) now dump compiled conf in prefix to avoid giving `stop` and other commands a configuration file argument, we now store the configuration in Kong's working directory (or Nginx prefix as it is now refered to). --- kong/cmd/start.lua | 6 +- kong/cmd/stop.lua | 7 +- kong/cmd/utils/dnsmasq_signals.lua | 35 ++++--- kong/cmd/utils/nginx_conf_compiler.lua | 65 ++++--------- kong/cmd/utils/serf_signals.lua | 24 ++--- kong/kong.lua | 14 ++- kong/serf.lua | 2 +- kong/templates/nginx_kong.lua | 7 +- spec/01-unit/01-conf/01-conf_loader_spec.lua | 32 +++--- .../01-conf/02-conf_compilation_spec.lua | 36 ++++--- .../01-cmd/02-start_stop_spec.lua | 97 ++++++++++++------- .../02-integration/01-cmd/03-compile_spec.lua | 14 ++- spec/02-integration/01-cmd/04-reload_spec.lua | 4 +- 13 files changed, 174 insertions(+), 169 deletions(-) diff --git a/kong/cmd/start.lua b/kong/cmd/start.lua index c2b0485894bd..29cb749844c0 100644 --- a/kong/cmd/start.lua +++ b/kong/cmd/start.lua @@ -1,7 +1,7 @@ local nginx_conf_compiler = require "kong.cmd.utils.nginx_conf_compiler" +local dnsmasq_signals = require "kong.cmd.utils.dnsmasq_signals" local nginx_signals = require "kong.cmd.utils.nginx_signals" local serf_signals = require "kong.cmd.utils.serf_signals" -local dnsmasq_signals = require "kong.cmd.utils.dnsmasq_signals" local conf_loader = require "kong.conf_loader" local DAOFactory = require "kong.dao.factory" local log = require "kong.cmd.utils.log" @@ -21,7 +21,9 @@ local function execute(args) local dao = DAOFactory(conf) assert(dao:run_migrations()) assert(nginx_conf_compiler.prepare_prefix(conf, conf.prefix)) - assert(dnsmasq_signals.start(conf, conf.prefix)) + if conf.dnsmasq then + assert(dnsmasq_signals.start(conf, conf.prefix)) + end assert(serf_signals.start(conf, conf.prefix, dao)) assert(nginx_signals.start(conf.prefix)) log("Started") diff --git a/kong/cmd/stop.lua b/kong/cmd/stop.lua index 743a760cc632..7ef64df4c9b9 100644 --- a/kong/cmd/stop.lua +++ b/kong/cmd/stop.lua @@ -1,6 +1,6 @@ +local dnsmasq_signals = require "kong.cmd.utils.dnsmasq_signals" local nginx_signals = require "kong.cmd.utils.nginx_signals" local serf_signals = require "kong.cmd.utils.serf_signals" -local dnsmasq_signals = require "kong.cmd.utils.dnsmasq_signals" local conf_loader = require "kong.conf_loader" local DAOFactory = require "kong.dao.factory" local log = require "kong.cmd.utils.log" @@ -11,10 +11,11 @@ local function execute(args) })) local dao = DAOFactory(conf) - assert(nginx_signals.stop(conf.prefix)) assert(serf_signals.stop(conf, conf.prefix, dao)) - assert(dnsmasq_signals.stop(conf.prefix)) + if conf.dnsmasq then + assert(dnsmasq_signals.stop(conf.prefix)) + end log("Stopped") end diff --git a/kong/cmd/utils/dnsmasq_signals.lua b/kong/cmd/utils/dnsmasq_signals.lua index 4d50c965ffc6..857c7e6151e1 100644 --- a/kong/cmd/utils/dnsmasq_signals.lua +++ b/kong/cmd/utils/dnsmasq_signals.lua @@ -48,27 +48,26 @@ local function is_running(pid_path) end function _M.start(kong_config, nginx_prefix) - if kong_config.dnsmasq then - -- is dnsmasq already running in this prefix? - local pid_path = pl_path.join(nginx_prefix, "pids", dnsmasq_pid_name) - if is_running(pid_path) then - log.verbose("dnsmasq already running at %s", pid_path) - return true - else - log.verbose("dnsmasq not running, deleting %s", pid_path) - pl_file.delete(pid_path) - end + -- is dnsmasq already running in this prefix? + local pid_path = pl_path.join(nginx_prefix, "pids", dnsmasq_pid_name) + if is_running(pid_path) then + log.verbose("dnsmasq already running at %s", pid_path) + return true + else + log.verbose("dnsmasq not running, deleting %s", pid_path) + pl_file.delete(pid_path) + end - local dnsmasq_bin, err = _M.find_bin() - if not dnsmasq_bin then return nil, err end + local dnsmasq_bin, err = _M.find_bin() + if not dnsmasq_bin then return nil, err end - local cmd = fmt("%s -p %d --pid-file=%s -N -o --listen-address=127.0.0.1", dnsmasq_bin, kong_config.dnsmasq_port, pid_path) + local cmd = fmt("%s -p %d --pid-file=%s -N -o --listen-address=127.0.0.1", + dnsmasq_bin, kong_config.dnsmasq_port, pid_path) - log.debug("starting dnsmasq: %s", cmd) + log.debug("starting dnsmasq: %s", cmd) - local ok, _, _, stderr = pl_utils.executeex(cmd) - if not ok then return nil, stderr end - end + local ok, _, _, stderr = pl_utils.executeex(cmd) + if not ok then return nil, stderr end return true end @@ -82,4 +81,4 @@ function _M.stop(nginx_prefix) return true end -return _M \ No newline at end of file +return _M diff --git a/kong/cmd/utils/nginx_conf_compiler.lua b/kong/cmd/utils/nginx_conf_compiler.lua index a1b14f5c5477..641c8a7186b7 100644 --- a/kong/cmd/utils/nginx_conf_compiler.lua +++ b/kong/cmd/utils/nginx_conf_compiler.lua @@ -1,30 +1,7 @@ -local NGINX_VARS = { - prefix = true, - plugins = true, - cluster_listen = true, - cluster_listen_rpc = true, - database = true, - pg_host = true, - pg_port = true, - pg_user = true, - pg_password = true, - pg_database = true, - cassandra_contact_points = true, - cassandra_keyspace = true, - cassandra_timeout = true, - cassandra_consistency = true, - cassandra_ssl = true, - cassandra_ssl_verify = true, - cassandra_username = true, - cassandra_password = true, - anonymous_reports = true -} - local kong_nginx_template = require "kong.templates.nginx_kong" local nginx_template = require "kong.templates.nginx" local pl_template = require "pl.template" local pl_stringx = require "pl.stringx" -local pl_pretty = require "pl.pretty" local pl_tablex = require "pl.tablex" local pl_utils = require "pl.utils" local pl_file = require "pl.file" @@ -32,7 +9,6 @@ local pl_path = require "pl.path" local pl_dir = require "pl.dir" local ssl = require "kong.cmd.utils.ssl" local log = require "kong.cmd.utils.log" -local fmt = string.format local function gather_system_infos(compile_env) local infos = {} @@ -53,19 +29,6 @@ local function compile_conf(kong_config, conf_template) nginx_vars = {} } - -- variables needed in Nginx - for k in pairs(NGINX_VARS) do - local v = kong_config[k] - local typ = type(v) - if typ == "table" then - v = pl_pretty.write(v, string.rep(" ", 6), true) - elseif typ == "string" then - v = string.format("%q", v) - end - - compile_env.nginx_vars[k] = v - end - local ssl_data, err = ssl.get_ssl_cert_and_key(kong_config, kong_config.prefix) if not ssl_data then return nil, err end @@ -110,10 +73,10 @@ local function touch(file_path) end local function prepare_prefix(kong_config, nginx_prefix) - log.verbose("preparing prefix directory at %s", nginx_prefix) + log.verbose("preparing nginx prefix directory at %s", nginx_prefix) if not pl_path.exists(nginx_prefix) then - log.verbose(fmt("prefix directory %s not found, trying to create it", nginx_prefix)) + log("prefix directory %s not found, trying to create it", nginx_prefix) local ok, err = pl_dir.makepath(nginx_prefix) if not ok then return nil, err end elseif not pl_path.isdir(nginx_prefix) then @@ -133,7 +96,7 @@ local function prepare_prefix(kong_config, nginx_prefix) if not ok then return nil, stderr end local ok, _, _, stderr = touch(acc_logs_path) if not ok then return nil, stderr end - + -- pids folder local pids_path = pl_path.join(nginx_prefix, "pids") local ok, err = pl_dir.makepath(pids_path) @@ -143,20 +106,32 @@ local function prepare_prefix(kong_config, nginx_prefix) local ok, err = ssl.prepare_ssl_cert_and_key(nginx_prefix) if not ok then return nil, err end - local nginx_config_path = pl_path.join(nginx_prefix, "nginx.conf") + local kong_conf_path = pl_path.join(nginx_prefix, "kong.conf") + local nginx_conf_path = pl_path.join(nginx_prefix, "nginx.conf") local kong_nginx_conf_path = pl_path.join(nginx_prefix, "nginx-kong.conf") -- write NGINX conf local nginx_conf, err = compile_nginx_conf(kong_config) if not nginx_conf then return nil, err end - local ok, err = pl_file.write(nginx_config_path, nginx_conf) - if not ok then return nil, err end + + pl_file.write(nginx_conf_path, nginx_conf) -- write Kong's NGINX conf local kong_nginx_conf, err = compile_kong_conf(kong_config) if not kong_nginx_conf then return nil, err end - local ok, err = pl_file.write(kong_nginx_conf_path, kong_nginx_conf) - if not ok then return nil, err end + pl_file.write(kong_nginx_conf_path, kong_nginx_conf) + + -- write kong.conf in prefix (for workers and CLI) + local buf = {} + for k, v in pairs(kong_config) do + if type(v) == "table" then + v = table.concat(v, ",") + end + if v ~= "" then + buf[#buf+1] = k.." = "..tostring(v) + end + end + pl_file.write(kong_conf_path, table.concat(buf, "\n")) return true end diff --git a/kong/cmd/utils/serf_signals.lua b/kong/cmd/utils/serf_signals.lua index 2192deb7ef8e..0f97e20d6afb 100644 --- a/kong/cmd/utils/serf_signals.lua +++ b/kong/cmd/utils/serf_signals.lua @@ -58,31 +58,19 @@ client:request { \ resty -e "$CMD" ]] -local function prepare_identifier(kong_config, nginx_prefix) - local serf_path = pl_path.join(nginx_prefix, "serf") - local ok, err = pl_dir.makepath(serf_path) - if not ok then return nil, err end - +local function prepare_prefix(kong_config, nginx_prefix, script_path) + log.verbose("saving Serf identifier in %s", id_path) local id_path = pl_path.join(nginx_prefix, "serf", serf_node_id) if not pl_path.exists(id_path) then local id = utils.get_hostname().."_"..kong_config.cluster_listen.."_"..utils.random_string() - - log.verbose("saving Serf identifier in %s", id_path) - local ok, err = pl_file.write(id_path, id) - if not ok then return nil, err end + pl_file.write(id_path, id) end - return true -end -local function prepare_prefix(kong_config, nginx_prefix, script_path) - local ok, err = prepare_identifier(kong_config, nginx_prefix) - if not ok then return nil, err end - - log.verbose("dumping Serf shell script handler in %s", script_path) + log.verbose("saving Serf shell script handler in %s", script_path) local script = fmt(script_template, "127.0.0.1", kong_config.admin_port) - local ok, err = pl_file.write(script_path, script) - if not ok then return nil, err end + pl_file.write(script_path, script) + local ok, _, _, stderr = pl_utils.executeex("chmod +x "..script_path) if not ok then return nil, stderr end diff --git a/kong/kong.lua b/kong/kong.lua index 7ef5c2097595..95cd4529d1bd 100644 --- a/kong/kong.lua +++ b/kong/kong.lua @@ -116,12 +116,16 @@ end local Kong = {} -function Kong.init(config) - -- retrieve node plugins - local events = Events() +function Kong.init() + local pl_path = require "pl.path" + local conf_loader = require "kong.conf_loader" - -- instanciate long-lived DAO - local dao = DAOFactory(config, events) + -- retrieve kong_config + local conf_path = pl_path.join(ngx.config.prefix(), "kong.conf") + local config = assert(conf_loader(conf_path)) + + local events = Events() -- retrieve node plugins + local dao = DAOFactory(config, events) -- instanciate long-lived DAO assert(dao:run_migrations()) -- migrating in case embedded in custom nginx -- populate singletons diff --git a/kong/serf.lua b/kong/serf.lua index 987df736b21b..625b4ed4d074 100644 --- a/kong/serf.lua +++ b/kong/serf.lua @@ -24,7 +24,7 @@ Serf.args_mt = { function Serf.new(kong_config, nginx_prefix, dao) return setmetatable({ - node_name = assert(pl_file.read(pl_path.join(nginx_prefix, "serf", serf_node_id))), + node_name = pl_file.read(pl_path.join(nginx_prefix, "serf", serf_node_id)), config = kong_config, dao = dao }, Serf) diff --git a/kong/templates/nginx_kong.lua b/kong/templates/nginx_kong.lua index bdd65ecdd00a..45d55db49971 100644 --- a/kong/templates/nginx_kong.lua +++ b/kong/templates/nginx_kong.lua @@ -41,13 +41,8 @@ lua_ssl_trusted_certificate '${{lua_ssl_trusted_certificate}}'; > end init_by_lua_block { - local config = {} -> for k, v in pairs(nginx_vars) do - config["$(k)"] = $(tostring(v)) -> end - kong = require 'kong' - kong.init(config) + kong.init() } init_worker_by_lua_block { diff --git a/spec/01-unit/01-conf/01-conf_loader_spec.lua b/spec/01-unit/01-conf/01-conf_loader_spec.lua index e8c378fba2de..64d4db110ef7 100644 --- a/spec/01-unit/01-conf/01-conf_loader_spec.lua +++ b/spec/01-unit/01-conf/01-conf_loader_spec.lua @@ -183,15 +183,7 @@ describe("Configuration loader", function() assert.is_nil(conf) assert.equal("proxy_listen_ssl must be of form 'address:port'", err) end) - end) - - describe("errors", function() - it("returns inexistent file", function() - local conf, err = conf_loader "inexistent" - assert.equal("no file at: inexistent", err) - assert.is_nil(conf) - end) - it("returns a DNS error when both a resolver and dnsmasq are enabled", function() + it("errors when both a resolver and dnsmasq are enabled", function() local conf, err = conf_loader(nil, { dnsmasq = true, dns_resolver = "8.8.8.8:53" @@ -213,6 +205,14 @@ describe("Configuration loader", function() assert.equal("cluster_ttl_on_failure must be at least 60 seconds", err) assert.is_nil(conf) end) + it("does not check SSL cert and key if SSL is off", function() + local conf, err = conf_loader(nil, { + ssl = false, + ssl_cert = "/path/cert.pem" + }) + assert.is_nil(err) + assert.is_table(conf) + end) it("requires both SSL cert and key", function() local conf, err = conf_loader(nil, { ssl_cert = "/path/cert.pem" @@ -233,13 +233,13 @@ describe("Configuration loader", function() assert.is_nil(err) assert.is_table(conf) end) - it("does not check SSL cert and key if SSL is off", function() - local conf, err = conf_loader(nil, { - ssl = false, - ssl_cert = "/path/cert.pem" - }) - assert.is_nil(err) - assert.is_table(conf) + end) + + describe("errors", function() + it("returns inexistent file", function() + local conf, err = conf_loader "inexistent" + assert.equal("no file at: inexistent", err) + assert.is_nil(conf) end) it("returns all errors in ret value #3", function() local conf, _, errors = conf_loader(nil, { diff --git a/spec/01-unit/01-conf/02-conf_compilation_spec.lua b/spec/01-unit/01-conf/02-conf_compilation_spec.lua index 62c62b06626f..6991e9f85263 100644 --- a/spec/01-unit/01-conf/02-conf_compilation_spec.lua +++ b/spec/01-unit/01-conf/02-conf_compilation_spec.lua @@ -82,21 +82,17 @@ describe("NGINX conf compiler", function() describe("prepare_prefix()", function() local prefix = "servroot_tmp" - local pl_dir = require "pl.dir" - local pl_path = require "pl.path" - local exists, join = pl_path.exists, pl_path.join + local exists, join = helpers.path.exists, helpers.path.join before_each(function() - pcall(pl_dir.rmtree, prefix) - pl_dir.makepath(prefix) + pcall(helpers.dir.rmtree, prefix) + helpers.dir.makepath(prefix) end) - after_each(function() - pl_dir.rmtree(prefix) - end) - it("auto-creates inexistent prefix", function() - assert(nginx_conf_compiler.prepare_prefix(helpers.test_conf, "./inexistent")) + it("creates inexistent prefix", function() finally(function() - helpers.dir.rmtree("inexistent") + pcall(helpers.dir.rmtree, "inexistent") end) + assert(nginx_conf_compiler.prepare_prefix(helpers.test_conf, "./inexistent")) + assert.truthy(exists("inexistent")) end) it("checks prefix is a directory", function() local tmp = os.tmpname() @@ -114,5 +110,23 @@ describe("NGINX conf compiler", function() assert.truthy(exists(join(prefix, "logs", "error.log"))) assert.truthy(exists(join(prefix, "logs", "access.log"))) end) + it("dumps Kong conf", function() + assert(nginx_conf_compiler.prepare_prefix(helpers.test_conf, prefix)) + local path = assert.truthy(exists(join(prefix, "kong.conf"))) + + local in_prefix_kong_conf = assert(conf_loader(path)) + assert.same(helpers.test_conf, in_prefix_kong_conf) + end) + it("dump Kong conf (custom conf)", function() + local conf = assert(conf_loader(nil, { + pg_database = "foobar" + })) + assert.equal("foobar", conf.pg_database) + assert(nginx_conf_compiler.prepare_prefix(conf, prefix)) + local path = assert.truthy(exists(join(prefix, "kong.conf"))) + + local in_prefix_kong_conf = assert(conf_loader(path)) + assert.same(conf, in_prefix_kong_conf) + end) end) end) diff --git a/spec/02-integration/01-cmd/02-start_stop_spec.lua b/spec/02-integration/01-cmd/02-start_stop_spec.lua index 2048cb0a12a9..c78bf1f30fde 100644 --- a/spec/02-integration/01-cmd/02-start_stop_spec.lua +++ b/spec/02-integration/01-cmd/02-start_stop_spec.lua @@ -38,10 +38,9 @@ describe("kong start/stop", function() it("start/stop default conf/prefix", function() -- don't want to force migrations to be run on default -- keyspace/database - local ok, _, stdout, stderr = exec "start" - assert.not_equal("", stdout) + local _, _, stdout, stderr = exec "start" assert.equal("", stderr) - assert.True(ok) + assert.not_equal("", stdout) ok, _, stdout, stderr = exec "stop" assert.not_equal("", stdout) @@ -49,39 +48,57 @@ describe("kong start/stop", function() assert.True(ok) end) it("start/stop custom Kong conf/prefix", function() - local ok, _, stdout, stderr = exec("start --conf "..helpers.test_conf_path) - assert.True(ok) + local _, _, stdout, stderr = exec("start --conf "..helpers.test_conf_path) + assert.equal("", stderr) assert.not_equal("", stdout) + + _, _, stdout, stderr = exec("stop --conf "..helpers.test_conf_path) assert.equal("", stderr) + assert.not_equal("", stdout) + end) + it("start with inexistent prefix", function() + finally(function() + helpers.execute(KILL_ALL) + pcall(helpers.dir.rmtree, "foobar") + end) - ok, _, stdout, stderr = exec("stop --conf "..helpers.test_conf_path) - assert.True(ok) + local _, _, stdout, stderr = exec "start --prefix foobar" + assert.equal("", stderr) assert.not_equal("", stdout) + end) + it("start dump config in prefix", function() + local _, _, stdout, stderr = exec("start --conf "..helpers.test_conf_path) assert.equal("", stderr) + assert.not_equal("", stdout) + + local conf_path = helpers.path.join(helpers.test_conf.prefix, "kong.conf") + assert.truthy(helpers.path.exists(conf_path)) + + _, _, stdout, stderr = exec("stop --conf "..helpers.test_conf_path) + assert.equal("", stderr) + assert.not_equal("", stdout) end) describe("verbose args", function() - it("accepts verbose", function() - local ok, _, stdout, stderr = exec("start --v --conf "..helpers.test_conf_path) - assert.True(ok) - assert.matches("[verbose] prefix in use: ", stdout, nil, true) + it("accepts verbose --v", function() + local _, _, stdout, stderr = exec("start --v --conf "..helpers.test_conf_path) assert.equal("", stderr) + assert.matches("[verbose] prefix in use: ", stdout, nil, true) finally(function() helpers.execute(KILL_ALL) end) end) - it("accepts debug", function() - local ok, _, stdout, stderr = exec("start --vv --conf "..helpers.test_conf_path) - assert.True(ok) + it("accepts debug --vv", function() + finally(function() + helpers.execute(KILL_ALL) + end) + + local _, _, stdout, stderr = exec("start --vv --conf "..helpers.test_conf_path) assert.matches("[verbose] prefix in use: ", stdout, nil, true) assert.matches("[debug] prefix = ", stdout, nil, true) assert.matches("[debug] database = ", stdout, nil, true) assert.equal("", stderr) - - finally(function() - helpers.execute(KILL_ALL) - end) end) it("should start with an inexistent prefix", function() local ok, _, stdout, stderr = exec "start --prefix foobar" @@ -97,8 +114,8 @@ describe("kong start/stop", function() describe("Serf", function() it("starts Serf agent daemon", function() - local ok = exec("start --conf "..helpers.test_conf_path) - assert.True(ok) + local _, _, _, stderr = exec("start --conf "..helpers.test_conf_path) + assert.equal("", stderr) local serf_pid_path = helpers.path.join(helpers.test_conf.prefix, "pids", "serf.pid") local cmd = string.format("kill -0 `cat %s` >/dev/null 2>&1", serf_pid_path) @@ -110,10 +127,11 @@ describe("kong start/stop", function() end) it("recovers from expired serf.pid file", function() local serf_pid_path = helpers.path.join(helpers.test_conf.prefix, "pids", "serf.pid") - local ok = helpers.execute("touch "..serf_pid_path) -- dumb pid - assert.True(ok) + local _, _, _, stderr = helpers.execute("touch "..serf_pid_path) -- dumb pid + assert.equal("", stderr) - assert.True(exec("start --conf "..helpers.test_conf_path)) + local _, _, _, stderr = exec("start --conf "..helpers.test_conf_path) + assert.equal("", stderr) local cmd = string.format("kill -0 `cat %s` >/dev/null 2>&1", serf_pid_path) local ok, code = helpers.execute(cmd) @@ -126,7 +144,10 @@ describe("kong start/stop", function() describe("dnsmasq", function() it("starts dnsmasq daemon", function() - local ok = exec("start --conf "..helpers.test_conf_path, {dnsmasq=true, dns_resolver = ""}) + local ok = exec("start --conf "..helpers.test_conf_path, { + dnsmasq = true, + dns_resolver = "" + }) assert.True(ok) local dnsmasq_pid_path = helpers.path.join(helpers.test_conf.prefix, "pids", "dnsmasq.pid") @@ -139,11 +160,13 @@ describe("kong start/stop", function() end) it("recovers from expired dnsmasq.pid file", function() local dnsmasq_pid_path = helpers.path.join(helpers.test_conf.prefix, "pids", "dnsmasq.pid") - local ok = helpers.execute("touch "..dnsmasq_pid_path) -- dumb pid - assert.True(ok) - - assert.True(exec("start --conf "..helpers.test_conf_path, {dnsmasq=true, dns_resolver = ""})) + local _, _, _, stderr = helpers.execute("touch "..dnsmasq_pid_path) -- dumb pid + assert.equal("", stderr) + assert.True(exec("start --conf "..helpers.test_conf_path, { + dnsmasq = true, + dns_resolver = "" + })) local cmd = string.format("kill -0 `cat %s` >/dev/null 2>&1", dnsmasq_pid_path) local ok, code = helpers.execute(cmd) assert.True(ok) @@ -155,13 +178,17 @@ describe("kong start/stop", function() describe("errors", function() it("start inexistent Kong conf file", function() - local ok, _, stdout, stderr = exec "start --conf foobar.conf" - assert.False(ok) + local _, _, stdout, stderr = exec "start --conf foobar.conf" assert.equal("", stdout) assert.is_string(stderr) assert.matches("Error: no file at: foobar.conf", stderr, nil, true) end) it("stop inexistent prefix", function() + finally(function() + helpers.execute(KILL_ALL) + helpers.dir.rmtree(helpers.test_conf.prefix) + end) + assert(helpers.dir.makepath(helpers.test_conf.prefix)) local ok, _, stdout, stderr = exec("start --prefix "..helpers.test_conf.prefix) @@ -173,13 +200,13 @@ describe("kong start/stop", function() assert.False(ok) assert.equal("", stdout) assert.matches("Error: could not get Nginx pid", stderr, nil, true) - + end) + it("notifies when Nginx is already running", function() finally(function() helpers.execute(KILL_ALL) helpers.dir.rmtree(helpers.test_conf.prefix) end) - end) - it("notifies when Nginx is already running", function() + assert(helpers.dir.makepath(helpers.test_conf.prefix)) local ok, _, stdout, stderr = exec("start --prefix "..helpers.test_conf.prefix) @@ -191,10 +218,6 @@ describe("kong start/stop", function() assert.False(ok) assert.equal("", stdout) assert.matches("Nginx is already running in", stderr) - finally(function() - helpers.execute(KILL_ALL) - helpers.dir.rmtree(helpers.test_conf.prefix) - end) end) end) end) diff --git a/spec/02-integration/01-cmd/03-compile_spec.lua b/spec/02-integration/01-cmd/03-compile_spec.lua index b5414ee25e07..9ce8adb649fb 100644 --- a/spec/02-integration/01-cmd/03-compile_spec.lua +++ b/spec/02-integration/01-cmd/03-compile_spec.lua @@ -8,19 +8,23 @@ end describe("kong compile", function() it("compiles a Kong NGINX config", function() local ok, _, stdout, stderr = exec "compile" - assert.True(ok) assert.equal("", stderr) + assert.True(ok) assert.matches("init_by_lua_block", stdout) assert.matches("init_worker_by_lua_block", stdout) - assert.matches("lua_code_cache", stdout) + assert.matches("lua_code_cache on", stdout) assert.matches("server_name kong", stdout) assert.matches("server_name kong_admin", stdout) - assert.matches('config["pg_database"] = "kong"', stdout, nil, true) + assert.matches("listen 0.0.0.0:8000", stdout, nil, true) + assert.matches("listen 0.0.0.0:8001", stdout, nil, true) + assert.matches("listen 0.0.0.0:8443 ssl", stdout, nil, true) end) it("accepts a custom Kong conf", function() local ok, _, stdout, stderr = exec("compile --conf "..helpers.test_conf_path) - assert.True(ok) assert.equal("", stderr) - assert.matches('config["pg_database"] = "kong_tests"', stdout, nil, true) + assert.True(ok) + assert.matches("listen 0.0.0.0:9000", stdout, nil, true) + assert.matches("listen 0.0.0.0:9001", stdout, nil, true) + assert.matches("listen 0.0.0.0:9443 ssl", stdout, nil, true) end) end) diff --git a/spec/02-integration/01-cmd/04-reload_spec.lua b/spec/02-integration/01-cmd/04-reload_spec.lua index b5d6c9e408b9..f3c06aaf385b 100644 --- a/spec/02-integration/01-cmd/04-reload_spec.lua +++ b/spec/02-integration/01-cmd/04-reload_spec.lua @@ -12,7 +12,7 @@ describe("kong reload", function() helpers.clean_prefix() end) - it("send a HUP signal to a running nginx master process", function() + it("send a HUP signal to a running Nginx master process", function() finally(function() helpers.execute(KILL_ALL) end) @@ -30,7 +30,7 @@ describe("kong reload", function() describe("errors", function() it("complains about missing PID if not already running", function() local ok, err = helpers.kong_exec("reload") - assert.falsy(ok) + assert.False(ok) assert.matches("Error: could not get Nginx pid (is Nginx running in this prefix?)", err, nil, true) end) end) From 12e91ad6c56b9d974067806ed129730d68799c85 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Wed, 22 Jun 2016 16:24:30 -0700 Subject: [PATCH 23/29] fix(cli) correct support for prefix/conf in CLI and fix tests --- kong-0.8.2-0.rockspec | 22 +-- kong.conf.default | 62 ++++---- kong/cmd/cluster.lua | 14 +- kong/cmd/compile.lua | 4 +- kong/cmd/init.lua | 3 +- kong/cmd/reload.lua | 17 +- kong/cmd/start.lua | 4 +- kong/cmd/stop.lua | 11 +- ...x_conf_compiler.lua => prefix_handler.lua} | 49 ++++++ kong/cmd/utils/serf_signals.lua | 72 ++------- kong/serf.lua | 11 +- ...on_spec.lua => 02-prefix_handler_spec.lua} | 85 +++++++--- spec/02-integration/01-cmd/01-cmds_spec.lua | 17 +- .../01-cmd/02-start_stop_spec.lua | 148 ++++++------------ .../02-integration/01-cmd/03-compile_spec.lua | 11 +- spec/02-integration/01-cmd/04-reload_spec.lua | 13 +- .../02-integration/01-cmd/05-version_spec.lua | 6 +- spec/02-integration/01-cmd/06-check_spec.lua | 18 +-- .../02-integration/01-cmd/07-cluster_spec.lua | 8 +- .../03-admin_api/06-cluster_routes_spec.lua | 7 +- .../05-proxy/01-resolver_spec.lua | 1 + .../05-proxy/02-real_ip_spec.lua | 1 + .../05-proxy/03-plugins_triggering_spec.lua | 1 + .../06-cluster/cluster_spec.lua | 25 +-- spec/helpers.lua | 36 +++-- spec/kong_tests.conf | 2 +- 26 files changed, 306 insertions(+), 342 deletions(-) rename kong/cmd/utils/{nginx_conf_compiler.lua => prefix_handler.lua} (75%) rename spec/01-unit/01-conf/{02-conf_compilation_spec.lua => 02-prefix_handler_spec.lua} (58%) diff --git a/kong-0.8.2-0.rockspec b/kong-0.8.2-0.rockspec index d4d464fc0370..75f3361d34c8 100644 --- a/kong-0.8.2-0.rockspec +++ b/kong-0.8.2-0.rockspec @@ -48,29 +48,29 @@ build = { ["kong.singletons"] = "kong/singletons.lua", ["kong.conf_loader"] = "kong/conf_loader.lua", - ["kong.templates.kong_defaults"] = "kong/templates/kong_defaults.lua", ["kong.templates.nginx"] = "kong/templates/nginx.lua", ["kong.templates.nginx_kong"] = "kong/templates/nginx_kong.lua", + ["kong.templates.kong_defaults"] = "kong/templates/kong_defaults.lua", ["kong.vendor.classic"] = "kong/vendor/classic.lua", + ["kong.cmd.roar"] = "kong/cmd/roar.lua", + ["kong.cmd.init"] = "kong/cmd/init.lua", + ["kong.cmd.stop"] = "kong/cmd/stop.lua", + ["kong.cmd.start"] = "kong/cmd/start.lua", ["kong.cmd.check"] = "kong/cmd/check.lua", + ["kong.cmd.reload"] = "kong/cmd/reload.lua", + ["kong.cmd.cluster"] = "kong/cmd/cluster.lua", ["kong.cmd.compile"] = "kong/cmd/compile.lua", - ["kong.cmd.init"] = "kong/cmd/init.lua", ["kong.cmd.migrations"] = "kong/cmd/migrations.lua", - ["kong.cmd.cluster"] = "kong/cmd/cluster.lua", - ["kong.cmd.reload"] = "kong/cmd/reload.lua", - ["kong.cmd.roar"] = "kong/cmd/roar.lua", - ["kong.cmd.start"] = "kong/cmd/start.lua", - ["kong.cmd.stop"] = "kong/cmd/stop.lua", ["kong.cmd.version"] = "kong/cmd/version.lua", - ["kong.cmd.utils.kill"] = "kong/cmd/utils/kill.lua", ["kong.cmd.utils.log"] = "kong/cmd/utils/log.lua", - ["kong.cmd.utils.nginx_conf_compiler"] = "kong/cmd/utils/nginx_conf_compiler.lua", - ["kong.cmd.utils.nginx_signals"] = "kong/cmd/utils/nginx_signals.lua", + ["kong.cmd.utils.ssl"] = "kong/cmd/utils/ssl.lua", + ["kong.cmd.utils.kill"] = "kong/cmd/utils/kill.lua", ["kong.cmd.utils.serf_signals"] = "kong/cmd/utils/serf_signals.lua", + ["kong.cmd.utils.nginx_signals"] = "kong/cmd/utils/nginx_signals.lua", + ["kong.cmd.utils.prefix_handler"] = "kong/cmd/utils/prefix_handler.lua", ["kong.cmd.utils.dnsmasq_signals"] = "kong/cmd/utils/dnsmasq_signals.lua", - ["kong.cmd.utils.ssl"] = "kong/cmd/utils/ssl.lua", ["kong.api.init"] = "kong/api/init.lua", ["kong.api.api_helpers"] = "kong/api/api_helpers.lua", diff --git a/kong.conf.default b/kong.conf.default index bc9d61a4f33f..5016d104d7e4 100644 --- a/kong.conf.default +++ b/kong.conf.default @@ -1,6 +1,6 @@ # Kong configuration file. # -# All commented values are default values. Uncomment and update a property to +# All commented values are default values. Uncomment and update a property to # configure it. # The Kong working directory. The directory will contain Kong process files and @@ -12,26 +12,26 @@ # This section determines the network settings for Kong. By default Kong listens # for connections from all the network interfaces available on the server. -# Address and port on which the server will accept HTTP requests, consumers will +# Address and port on which the server will accept HTTP requests, consumers will # make requests on this port. # proxy_listen = 0.0.0.0:8000 # Same as proxy_listen, but for HTTPS requests. # proxy_listen_ssl = 0.0.0.0:8443 -# Address and port on which the admin API will listen to. The admin API is a -# private API which lets you manage your Kong infrastructure. It needs to be +# Address and port on which the admin API will listen to. The admin API is a +# private API which lets you manage your Kong infrastructure. It needs to be # secured appropriately. # admin_listen = 0.0.0.0:8001 -# Address and port used by the node to communicate with other Kong nodes in the -# cluster with both UDP and TCP messages. All the nodes in the cluster must be -# able to communicate with this node on this address. Only IPv4 addresses are +# Address and port used by the node to communicate with other Kong nodes in the +# cluster with both UDP and TCP messages. All the nodes in the cluster must be +# able to communicate with this node on this address. Only IPv4 addresses are # allowed (no hostnames). # cluster_listen = 0.0.0.0:7946 -# Address and port used by the node to communicate with the local clustering -# agent (TCP only, and local only). Used internally by this Kong node. Only +# Address and port used by the node to communicate with the local clustering +# agent (TCP only, and local only). Used internally by this Kong node. Only # IPv4 addresses are allowed (no hostnames). # cluster_listen_rpc = 127.0.0.1:7373 @@ -78,35 +78,35 @@ # cassandra_ssl_verify = off # cassandra_ssl_trusted_cert = NONE -# Cluster authentication options. Provide a user and a password here if your +# Cluster authentication options. Provide a user and a password here if your # cluster uses the "PasswordAuthenticator" scheme. # cassandra_username = kong # cassandra_password = kong ################################## CLUSTER ##################################### -# Cluster settings for Kong nodes. Every Kong node that points to the same +# Cluster settings for Kong nodes. Every Kong node that points to the same # database MUST join together to form a Kong Cluster, in both single or multi-DC -# setups. Kong works on the IP layer (hostnames are not supported, only IPs are -# allowed) and it expects a flat network topology without any NAT between the -# datacenters. A common setup is having a VPN between the two datacenters such +# setups. Kong works on the IP layer (hostnames are not supported, only IPs are +# allowed) and it expects a flat network topology without any NAT between the +# datacenters. A common setup is having a VPN between the two datacenters such # that the "flat" network assumption of Kong is not violated. -# By default, the cluster_listen address is advertised. If the cluster_listen -# host is "0.0.0.0", then the first local, non-loopback, IPv4 address will be -# advertised to the other nodes. However, in some cases (specifically NAT +# By default, the cluster_listen address is advertised. If the cluster_listen +# host is "0.0.0.0", then the first local, non-loopback, IPv4 address will be +# advertised to the other nodes. However, in some cases (specifically NAT # traversal), there may be a routable address that cannot be bound to. This flag # enables gossiping a different address to support this. # cluster_advertise = NONE -# Key for encrypting network traffic within Kong. Must be a base64-encoded +# Key for encrypting network traffic within Kong. Must be a base64-encoded # 16-byte key. # cluster_encrypt = NONE -# The TTL (time to live), in seconds, of a node in the cluster when it stops -# sending healthcheck pings, possibly caused by a node or network failure. If -# the node is not able to send a new healthcheck ping before the expiration, -# then new nodes in the cluster will stop attempting to connect to it on +# The TTL (time to live), in seconds, of a node in the cluster when it stops +# sending healthcheck pings, possibly caused by a node or network failure. If +# the node is not able to send a new healthcheck ping before the expiration, +# then new nodes in the cluster will stop attempting to connect to it on # startup. Should be at least 60. # cluster_ttl_on_failure = 3600 @@ -114,7 +114,7 @@ # By default Kong leverages on dnsmasq to resolve DNS addresses to the upstream # services by using the system settings in /etc/hosts and /etc/resolv.conf. -# dnsmasq = on +dnsmasq = off # The port used by dnsmasq, only used locally by Kong. # dnsmasq_port = 8053 @@ -130,21 +130,21 @@ # proxy_listen and proxy_listen_ssl properties. You can optionally enable or # disable SSL support (note that this may break plugins that are leveraging it). -ssl = on -ssl_cert = NONE -ssl_cert_key = NONE +#ssl = on +#ssl_cert = NONE +#ssl_cert_key = NONE ################################## GENERAL ##################################### # The log level for the events returned by Kong and its services. # log_level = error -# Comma separated list of additional plugins names to load on this node, used to +# Comma separated list of additional plugins names to load on this node, used to # load custom plugins that are not already bundled with Kong. # Plugins will be loaded from the kong.plugins.{name}.* namespace. # custom_plugins = NONE -# The path to the SSL certificate and key that Kong will use when listening on +# The path to the SSL certificate and key that Kong will use when listening on # the proxy_listen_ssl port. # ssl_cert = NONE # ssl_cert_key = NONE @@ -160,7 +160,7 @@ ssl_cert_key = NONE # Allows Kong to set specific connection and proxying settings in Nginx. # nginx_optimizations = on -# The size in MB of the internal preallocated in-memory cache for database -# entities. The default value is `128`, and the potential maximum value is the +# The size in MB of the internal preallocated in-memory cache for database +# entities. The default value is `128`, and the potential maximum value is the # total size of the datastore. -# mem_cache_size = 128m \ No newline at end of file +# mem_cache_size = 128m diff --git a/kong/cmd/cluster.lua b/kong/cmd/cluster.lua index 3437b9ad4539..affa6a5e9d2e 100644 --- a/kong/cmd/cluster.lua +++ b/kong/cmd/cluster.lua @@ -2,34 +2,29 @@ local conf_loader = require "kong.conf_loader" local DAOFactory = require "kong.dao.factory" local Serf = require "kong.serf" local log = require "kong.cmd.utils.log" -local fmt = string.format local function execute(args) - local conf = assert(conf_loader(args.conf, { - prefix = args.prefix - })) - + local conf = assert(conf_loader(args.conf)) local dao = DAOFactory(conf) local serf = Serf.new(conf, conf.prefix, dao) if args.command == "members" then local members = assert(serf:members(true)) for _, v in ipairs(members) do - print(fmt("%s\t%s\t%s", v.name, v.addr, v.status)) + print(string.format("%s\t%s\t%s", v.name, v.addr, v.status)) end elseif args.command == "keygen" then print(assert(serf:keygen())) elseif args.command == "reachability" then - log("Please wait..") print(assert(serf:reachability())) elseif args.command == "force-leave" then local node_name = args[1] if not node_name then error("you need to specify the node name to leave") end - log(fmt("Force-leaving %s", node_name)) + log("force-leaving %s", node_name) assert(serf:force_leave(node_name)) - log("Done") + log("left node %s", node_name) end end @@ -44,7 +39,6 @@ The available commands are: Options: -c,--conf (optional string) configuration file - --prefix (optional string) Nginx prefix path ]] return { diff --git a/kong/cmd/compile.lua b/kong/cmd/compile.lua index afb92adc44ba..813fa6de6428 100644 --- a/kong/cmd/compile.lua +++ b/kong/cmd/compile.lua @@ -1,9 +1,9 @@ -local nginx_conf_compiler = require "kong.cmd.utils.nginx_conf_compiler" +local prefix_handler = require "kong.cmd.utils.prefix_handler" local conf_loader = require "kong.conf_loader" local function execute(args) local conf = assert(conf_loader(args.conf)) - local kong_nginx_conf = assert(nginx_conf_compiler.compile_kong_conf(conf)) + local kong_nginx_conf = assert(prefix_handler.compile_kong_conf(conf)) print(kong_nginx_conf) end diff --git a/kong/cmd/init.lua b/kong/cmd/init.lua index 45f4ebf90051..bf8204d5ad40 100644 --- a/kong/cmd/init.lua +++ b/kong/cmd/init.lua @@ -15,8 +15,9 @@ The available commands are: stop reload check - migrations compile + migrations + cluster version Options: diff --git a/kong/cmd/reload.lua b/kong/cmd/reload.lua index 1a48438dabe6..41ae6f51a7c5 100644 --- a/kong/cmd/reload.lua +++ b/kong/cmd/reload.lua @@ -1,17 +1,23 @@ -local nginx_conf_compiler = require "kong.cmd.utils.nginx_conf_compiler" +local dnsmasq_signals = require "kong.cmd.utils.dnsmasq_signals" +local prefix_handler = require "kong.cmd.utils.prefix_handler" local nginx_signals = require "kong.cmd.utils.nginx_signals" local serf_signals = require "kong.cmd.utils.serf_signals" -local dnsmasq_signals = require "kong.cmd.utils.dnsmasq_signals" local conf_loader = require "kong.conf_loader" local DAOFactory = require "kong.dao.factory" +local pl_path = require "pl.path" local log = require "kong.cmd.utils.log" local function execute(args) - local conf = assert(conf_loader(args.conf, { - prefix = args.prefix + local default_conf = assert(conf_loader()) -- just retrieve default prefix + local prefix = args.prefix or default_conf.prefix + assert(pl_path.exists(prefix), "no such prefix: "..prefix) + + local conf_path = pl_path.join(prefix, "kong.conf") + local conf = assert(conf_loader(conf_path, { + prefix = prefix })) - assert(nginx_conf_compiler.prepare_prefix(conf, conf.prefix)) + assert(prefix_handler.prepare_prefix(conf, conf.prefix)) assert(dnsmasq_signals.start(conf, conf.prefix)) assert(serf_signals.start(conf, conf.prefix, DAOFactory(conf))) assert(nginx_signals.reload(conf.prefix)) @@ -22,7 +28,6 @@ local lapp = [[ Usage: kong reload [OPTIONS] Options: - -c,--conf (optional string) configuration file --prefix (optional string) Nginx prefix path ]] diff --git a/kong/cmd/start.lua b/kong/cmd/start.lua index 29cb749844c0..af91e47c9011 100644 --- a/kong/cmd/start.lua +++ b/kong/cmd/start.lua @@ -1,5 +1,5 @@ -local nginx_conf_compiler = require "kong.cmd.utils.nginx_conf_compiler" local dnsmasq_signals = require "kong.cmd.utils.dnsmasq_signals" +local prefix_handler = require "kong.cmd.utils.prefix_handler" local nginx_signals = require "kong.cmd.utils.nginx_signals" local serf_signals = require "kong.cmd.utils.serf_signals" local conf_loader = require "kong.conf_loader" @@ -20,7 +20,7 @@ local function execute(args) local dao = DAOFactory(conf) assert(dao:run_migrations()) - assert(nginx_conf_compiler.prepare_prefix(conf, conf.prefix)) + assert(prefix_handler.prepare_prefix(conf, conf.prefix)) if conf.dnsmasq then assert(dnsmasq_signals.start(conf, conf.prefix)) end diff --git a/kong/cmd/stop.lua b/kong/cmd/stop.lua index 7ef64df4c9b9..42caa5b6317f 100644 --- a/kong/cmd/stop.lua +++ b/kong/cmd/stop.lua @@ -3,11 +3,17 @@ local nginx_signals = require "kong.cmd.utils.nginx_signals" local serf_signals = require "kong.cmd.utils.serf_signals" local conf_loader = require "kong.conf_loader" local DAOFactory = require "kong.dao.factory" +local pl_path = require "pl.path" local log = require "kong.cmd.utils.log" local function execute(args) - local conf = assert(conf_loader(args.conf, { - prefix = args.prefix + local default_conf = assert(conf_loader()) -- just retrieve default prefix + local prefix = args.prefix or default_conf.prefix + assert(pl_path.exists(prefix), "no such prefix: "..prefix) + + local conf_path = pl_path.join(prefix, "kong.conf") + local conf = assert(conf_loader(conf_path, { + prefix = prefix })) local dao = DAOFactory(conf) @@ -23,7 +29,6 @@ local lapp = [[ Usage: kong stop [OPTIONS] Options: - -c,--conf (optional string) configuration file --prefix (optional string) Nginx prefix path ]] diff --git a/kong/cmd/utils/nginx_conf_compiler.lua b/kong/cmd/utils/prefix_handler.lua similarity index 75% rename from kong/cmd/utils/nginx_conf_compiler.lua rename to kong/cmd/utils/prefix_handler.lua index 641c8a7186b7..6eeaeb163084 100644 --- a/kong/cmd/utils/nginx_conf_compiler.lua +++ b/kong/cmd/utils/prefix_handler.lua @@ -7,9 +7,37 @@ local pl_utils = require "pl.utils" local pl_file = require "pl.file" local pl_path = require "pl.path" local pl_dir = require "pl.dir" +local utils = require "kong.tools.utils" local ssl = require "kong.cmd.utils.ssl" local log = require "kong.cmd.utils.log" +local serf_node_id = "serf.id" + +-- script from old services.serf module +local script_template = [[ +#!/bin/sh + +PAYLOAD=`cat` # Read from stdin +if [ "$SERF_EVENT" != "user" ]; then + PAYLOAD="{\"type\":\"${SERF_EVENT}\",\"entity\": \"${PAYLOAD}\"}" +fi + +CMD="\ +local http = require 'resty.http' \ +local client = http.new() \ +client:connect('%s', %d) \ +client:request { \ + method = 'POST', \ + path = '/cluster/events/', \ + body = [=[${PAYLOAD}]=], \ + headers = { \ + ['content-type'] = 'application/json' \ + } \ +}" + +resty -e "$CMD" +]] + local function gather_system_infos(compile_env) local infos = {} @@ -102,6 +130,27 @@ local function prepare_prefix(kong_config, nginx_prefix) local ok, err = pl_dir.makepath(pids_path) if not ok then return nil, err end + -- serf folder (node identifier + shell script) + local serf_path = pl_path.join(nginx_prefix, "serf") + local ok, err = pl_dir.makepath(serf_path) + if not ok then return nil, err end + + local id_path = pl_path.join(nginx_prefix, "serf", serf_node_id) + log.verbose("saving Serf identifier in %s", id_path) + if not pl_path.exists(id_path) then + local id = utils.get_hostname().."_"..kong_config.cluster_listen.."_"..utils.random_string() + pl_file.write(id_path, id) + end + + local script_path = pl_path.join(nginx_prefix, "serf", "serf_event.sh") + log.verbose("saving Serf shell script handler in %s", script_path) + local script = string.format(script_template, "127.0.0.1", kong_config.admin_port) + + pl_file.write(script_path, script) + + local ok, _, _, stderr = pl_utils.executeex("chmod +x "..script_path) + if not ok then return nil, stderr end + -- auto-generate default SSL certificate local ok, err = ssl.prepare_ssl_cert_and_key(nginx_prefix) if not ok then return nil, err end diff --git a/kong/cmd/utils/serf_signals.lua b/kong/cmd/utils/serf_signals.lua index 0f97e20d6afb..37aa501cec15 100644 --- a/kong/cmd/utils/serf_signals.lua +++ b/kong/cmd/utils/serf_signals.lua @@ -8,20 +8,16 @@ local pl_stringx = require "pl.stringx" local pl_utils = require "pl.utils" local pl_path = require "pl.path" local pl_file = require "pl.file" -local pl_dir = require "pl.dir" local kill = require "kong.cmd.utils.kill" local log = require "kong.cmd.utils.log" -local utils = require "kong.tools.utils" -local fmt = string.format local serf_bin_name = "serf" local serf_pid_name = "serf.pid" -local serf_node_id = "serf.id" local serf_event_name = "kong" local start_timeout = 5 local function check_serf_bin() - local cmd = fmt("%s -v", serf_bin_name) + local cmd = string.format("%s -v", serf_bin_name) local ok, _, stdout = pl_utils.executeex(cmd) if ok and stdout then if not stdout:match "^Serf v0%.7%.0" then @@ -33,50 +29,6 @@ local function check_serf_bin() return nil, "could not find Serf executable (is it in your $PATH?)" end --- script from old services.serf module -local script_template = [[ -#!/bin/sh - -PAYLOAD=`cat` # Read from stdin -if [ "$SERF_EVENT" != "user" ]; then - PAYLOAD="{\"type\":\"${SERF_EVENT}\",\"entity\": \"${PAYLOAD}\"}" -fi - -CMD="\ -local http = require 'resty.http' \ -local client = http.new() \ -client:connect('%s', %d) \ -client:request { \ - method = 'POST', \ - path = '/cluster/events/', \ - body = [=[${PAYLOAD}]=], \ - headers = { \ - ['content-type'] = 'application/json' \ - } \ -}" - -resty -e "$CMD" -]] - -local function prepare_prefix(kong_config, nginx_prefix, script_path) - log.verbose("saving Serf identifier in %s", id_path) - local id_path = pl_path.join(nginx_prefix, "serf", serf_node_id) - if not pl_path.exists(id_path) then - local id = utils.get_hostname().."_"..kong_config.cluster_listen.."_"..utils.random_string() - pl_file.write(id_path, id) - end - - log.verbose("saving Serf shell script handler in %s", script_path) - local script = fmt(script_template, "127.0.0.1", kong_config.admin_port) - - pl_file.write(script_path, script) - - local ok, _, _, stderr = pl_utils.executeex("chmod +x "..script_path) - if not ok then return nil, stderr end - - return true -end - local function is_running(pid_path) if not pl_path.exists(pid_path) then return nil end local code = kill(pid_path, "-0") @@ -96,19 +48,14 @@ function _M.start(kong_config, nginx_prefix, dao) pl_file.delete(pid_path) end - -- prepare shell script - local script_path = pl_path.join(nginx_prefix, "serf", "serf_event.sh") - local ok, err = prepare_prefix(kong_config, nginx_prefix, script_path) - if not ok then return nil, err end - -- make sure Serf is in PATH local ok, err = check_serf_bin() if not ok then return nil, err end local serf = Serf.new(kong_config, nginx_prefix, dao) - local node_name = serf.node_name local log_path = pl_path.join(nginx_prefix, "logs", "serf.log") + local script_path = pl_path.join(nginx_prefix, "serf", "serf_event.sh") local args = setmetatable({ ["-bind"] = kong_config.cluster_listen, @@ -117,15 +64,15 @@ function _M.start(kong_config, nginx_prefix, dao) ["-encrypt"] = kong_config.cluster_encrypt, ["-log-level"] = "err", ["-profile"] = "wan", - ["-node"] = node_name, + ["-node"] = serf.node_name, ["-event-handler"] = "member-join,member-leave,member-failed," .."member-update,member-reap,user:" ..serf_event_name.."="..script_path }, Serf.args_mt) - local cmd = fmt("nohup %s agent %s > %s 2>&1 & echo $! > %s", - serf_bin_name, tostring(args), - log_path, pid_path) + local cmd = string.format("nohup %s agent %s > %s 2>&1 & echo $! > %s", + serf_bin_name, tostring(args), + log_path, pid_path) log.debug("starting Serf agent: %s", cmd) @@ -152,11 +99,12 @@ function _M.start(kong_config, nginx_prefix, dao) return nil, "could not start Serf:\n "..err end - log.verbose("auto-joining Serf cluster...") + log.debug("Serf agent running") + log.verbose("auto-joining cluster...") local ok, err = serf:autojoin() if not ok then return nil, err end - log.verbose("adding node to Serf cluster (in datastore)...") + log.verbose("adding node to cluster (in datastore)...") local ok, err = serf:add_node() if not ok then return nil, err end @@ -164,7 +112,7 @@ function _M.start(kong_config, nginx_prefix, dao) end function _M.stop(kong_config, nginx_prefix, dao) - log.info("Leaving cluster") + log.info("leaving cluster...") local serf = Serf.new(kong_config, nginx_prefix, dao) diff --git a/kong/serf.lua b/kong/serf.lua index 625b4ed4d074..792206fde249 100644 --- a/kong/serf.lua +++ b/kong/serf.lua @@ -7,7 +7,6 @@ local pl_path = require "pl.path" local pl_file = require "pl.file" local cjson = require "cjson.safe" local log = require "kong.cmd.utils.log" -local fmt = string.format local serf_node_id = "serf.id" @@ -38,7 +37,7 @@ function Serf:invoke_signal(signal, args, no_rpc) setmetatable(args, Serf.args_mt) end local rpc = no_rpc and "" or "-rpc-addr="..self.config.cluster_listen_rpc - local cmd = fmt("serf %s %s %s", signal, rpc, tostring(args)) + local cmd = string.format("serf %s %s %s", signal, rpc, tostring(args)) local ok, code, stdout = pl_utils.executeex(cmd) if not ok or code ~= 0 then return nil, pl_stringx.splitlines(stdout)[1] end -- always print the first error line of serf @@ -97,7 +96,7 @@ function Serf:autojoin() local nodes, err = self.dao.nodes:find_all() if err then return nil, tostring(err) elseif #nodes == 0 then - log.info("No other Kong nodes were found in the cluster") + log.info("no other Kong nodes were found in the cluster") else -- Sort by newest to oldest (although by TTL would be a better sort) table.sort(nodes, function(a, b) return a.created_at > b.created_at end) @@ -105,7 +104,7 @@ function Serf:autojoin() local joined for _, v in ipairs(nodes) do if self:join_node(v.cluster_listening_address) then - log("Successfully auto-joined %s", v.cluster_listening_address) + log("successfully auto-joined %s", v.cluster_listening_address) joined = true break else @@ -113,7 +112,7 @@ function Serf:autojoin() end end if not joined then - log.warn("could not join the existing cluster") + log.warn("could not join existing cluster") end end @@ -151,7 +150,7 @@ function Serf:event(t_payload) if #payload > 512 then -- Serf can't send a payload greater than 512 bytes - return nil, "Encoded payload is "..#payload.." and exceeds the limit of 512 bytes!" + return nil, "encoded payload is "..#payload.." and exceeds the limit of 512 bytes!" end return self:invoke_signal("event -coalesce=false", " kong '"..payload.."'") diff --git a/spec/01-unit/01-conf/02-conf_compilation_spec.lua b/spec/01-unit/01-conf/02-prefix_handler_spec.lua similarity index 58% rename from spec/01-unit/01-conf/02-conf_compilation_spec.lua rename to spec/01-unit/01-conf/02-prefix_handler_spec.lua index 6991e9f85263..1ee7d7de3686 100644 --- a/spec/01-unit/01-conf/02-conf_compilation_spec.lua +++ b/spec/01-unit/01-conf/02-prefix_handler_spec.lua @@ -1,6 +1,6 @@ -local nginx_conf_compiler = require "kong.cmd.utils.nginx_conf_compiler" -local conf_loader = require "kong.conf_loader" local helpers = require "spec.helpers" +local conf_loader = require "kong.conf_loader" +local prefix_handler = require "kong.cmd.utils.prefix_handler" describe("NGINX conf compiler", function() local custom_conf @@ -18,7 +18,7 @@ describe("NGINX conf compiler", function() describe("compile_kong_conf()", function() it("compiles the Kong NGINX conf chunk", function() - local kong_nginx_conf = nginx_conf_compiler.compile_kong_conf(helpers.test_conf) + local kong_nginx_conf = prefix_handler.compile_kong_conf(helpers.test_conf) assert.matches("lua_package_path '?/init.lua;./kong/?.lua;;';", kong_nginx_conf, nil, true) assert.matches("lua_code_cache on;", kong_nginx_conf, nil, true) assert.matches("listen 0.0.0.0:9000;", kong_nginx_conf, nil, true) @@ -28,14 +28,14 @@ describe("NGINX conf compiler", function() assert.not_matches("lua_ssl_trusted_certificate", kong_nginx_conf, nil, true) end) it("compiles with custom conf", function() - local kong_nginx_conf = nginx_conf_compiler.compile_kong_conf(custom_conf) + local kong_nginx_conf = prefix_handler.compile_kong_conf(custom_conf) assert.matches("lua_code_cache off;", kong_nginx_conf, nil, true) assert.matches("lua_shared_dict cache 128k;", kong_nginx_conf, nil, true) assert.matches("listen 0.0.0.0:80;", kong_nginx_conf, nil, true) assert.matches("listen 127.0.0.1:8001;", kong_nginx_conf, nil, true) end) it("disables SSL", function() - local kong_nginx_conf = nginx_conf_compiler.compile_kong_conf(custom_conf) + local kong_nginx_conf = prefix_handler.compile_kong_conf(custom_conf) assert.not_matches("listen %d+%.%d+%.%d+%.%d+:%d+ ssl;", kong_nginx_conf) assert.not_matches("ssl_certificate", kong_nginx_conf) assert.not_matches("ssl_certificate_key", kong_nginx_conf) @@ -47,26 +47,26 @@ describe("NGINX conf compiler", function() cassandra_ssl = true, cassandra_ssl_trusted_cert = "/path/to/ca.cert" })) - local kong_nginx_conf = nginx_conf_compiler.compile_kong_conf(conf) + local kong_nginx_conf = prefix_handler.compile_kong_conf(conf) assert.matches("lua_ssl_trusted_certificate '/path/to/ca.cert';", kong_nginx_conf, nil, true) end) end) describe("compile_nginx_conf()", function() it("compiles a main NGINX conf", function() - local nginx_conf = nginx_conf_compiler.compile_nginx_conf(helpers.test_conf) + local nginx_conf = prefix_handler.compile_nginx_conf(helpers.test_conf) assert.matches("worker_processes 1;", nginx_conf, nil, true) assert.matches("daemon on;", nginx_conf, nil, true) end) it("compiles with custom conf", function() - local nginx_conf = nginx_conf_compiler.compile_nginx_conf(custom_conf) + local nginx_conf = prefix_handler.compile_nginx_conf(custom_conf) assert.matches("daemon off;", nginx_conf, nil, true) end) it("compiles without opinionated nginx optimizations", function() local conf = assert(conf_loader(nil, { nginx_optimizations = false, })) - local nginx_conf = nginx_conf_compiler.compile_nginx_conf(conf) + local nginx_conf = prefix_handler.compile_nginx_conf(conf) assert.not_matches("worker_connections %d+;", nginx_conf) assert.not_matches("multi_accept on;", nginx_conf) end) @@ -74,24 +74,27 @@ describe("NGINX conf compiler", function() local conf = assert(conf_loader(nil, { nginx_optimizations = true, })) - local nginx_conf = nginx_conf_compiler.compile_nginx_conf(conf) + local nginx_conf = prefix_handler.compile_nginx_conf(conf) assert.matches("worker_connections %d+;", nginx_conf) assert.matches("multi_accept on;", nginx_conf) end) end) describe("prepare_prefix()", function() - local prefix = "servroot_tmp" + local tmp_path = "servroot_tmp" local exists, join = helpers.path.exists, helpers.path.join before_each(function() - pcall(helpers.dir.rmtree, prefix) - helpers.dir.makepath(prefix) + pcall(helpers.dir.rmtree, tmp_path) + helpers.dir.makepath(tmp_path) + end) + after_each(function() + helpers.dir.rmtree(tmp_path) end) it("creates inexistent prefix", function() finally(function() pcall(helpers.dir.rmtree, "inexistent") end) - assert(nginx_conf_compiler.prepare_prefix(helpers.test_conf, "./inexistent")) + assert(prefix_handler.prepare_prefix(helpers.test_conf, "./inexistent")) assert.truthy(exists("inexistent")) end) it("checks prefix is a directory", function() @@ -99,21 +102,28 @@ describe("NGINX conf compiler", function() finally(function() assert(os.remove(tmp)) end) - local ok, err = nginx_conf_compiler.prepare_prefix(helpers.test_conf, tmp) + local ok, err = prefix_handler.prepare_prefix(helpers.test_conf, tmp) assert.equal(tmp.." is not a directory", err) assert.is_nil(ok) end) + it("creates pids folder", function() + assert(prefix_handler.prepare_prefix(helpers.test_conf, tmp_path)) + assert.truthy(exists(join(tmp_path, "pids"))) + end) + it("creates serf folder", function() + assert(prefix_handler.prepare_prefix(helpers.test_conf, tmp_path)) + assert.truthy(exists(join(tmp_path, "serf"))) + end) it("creates NGINX conf and log files", function() - assert(nginx_conf_compiler.prepare_prefix(helpers.test_conf, prefix)) - assert.truthy(exists(join(prefix, "nginx.conf"))) - assert.truthy(exists(join(prefix, "nginx-kong.conf"))) - assert.truthy(exists(join(prefix, "logs", "error.log"))) - assert.truthy(exists(join(prefix, "logs", "access.log"))) + assert(prefix_handler.prepare_prefix(helpers.test_conf, tmp_path)) + assert.truthy(exists(join(tmp_path, "nginx.conf"))) + assert.truthy(exists(join(tmp_path, "nginx-kong.conf"))) + assert.truthy(exists(join(tmp_path, "logs", "error.log"))) + assert.truthy(exists(join(tmp_path, "logs", "access.log"))) end) it("dumps Kong conf", function() - assert(nginx_conf_compiler.prepare_prefix(helpers.test_conf, prefix)) - local path = assert.truthy(exists(join(prefix, "kong.conf"))) - + assert(prefix_handler.prepare_prefix(helpers.test_conf, tmp_path)) + local path = assert.truthy(exists(join(tmp_path, "kong.conf"))) local in_prefix_kong_conf = assert(conf_loader(path)) assert.same(helpers.test_conf, in_prefix_kong_conf) end) @@ -122,11 +132,34 @@ describe("NGINX conf compiler", function() pg_database = "foobar" })) assert.equal("foobar", conf.pg_database) - assert(nginx_conf_compiler.prepare_prefix(conf, prefix)) - local path = assert.truthy(exists(join(prefix, "kong.conf"))) - + assert(prefix_handler.prepare_prefix(conf, tmp_path)) + local path = assert.truthy(exists(join(tmp_path, "kong.conf"))) local in_prefix_kong_conf = assert(conf_loader(path)) assert.same(conf, in_prefix_kong_conf) end) + it("dumps Serf script", function() + assert(prefix_handler.prepare_prefix(helpers.test_conf, tmp_path)) + local path = assert.truthy(exists(join(tmp_path, "serf", "serf_event.sh"))) + + local identifier = helpers.file.read(path) + assert.is_string(identifier) + end) + it("dumps Serf identifier", function() + assert(prefix_handler.prepare_prefix(helpers.test_conf, tmp_path)) + local path = assert.truthy(exists(join(tmp_path, "serf", "serf.id"))) + + local identifier = helpers.file.read(path) + assert.is_string(identifier) + end) + it("preserves Serf identifier if already exists", function() + -- prepare twice + assert(prefix_handler.prepare_prefix(helpers.test_conf, tmp_path)) + local identifier_1 = helpers.file.read(join(tmp_path, "serf", "serf.id")) + + assert(prefix_handler.prepare_prefix(helpers.test_conf, tmp_path)) + local identifier_2 = helpers.file.read(join(tmp_path, "serf", "serf.id")) + + assert.equal(identifier_1, identifier_2) + end) end) end) diff --git a/spec/02-integration/01-cmd/01-cmds_spec.lua b/spec/02-integration/01-cmd/01-cmds_spec.lua index 2d9ccfb61744..4d3713eb6a17 100644 --- a/spec/02-integration/01-cmd/01-cmds_spec.lua +++ b/spec/02-integration/01-cmd/01-cmds_spec.lua @@ -1,26 +1,17 @@ local helpers = require "spec.helpers" -local function exec(...) - local args = {...} - table.insert(args, 1, helpers.bin_path) - local cmd = table.concat(args, " ") - return helpers.execute(cmd) -end - describe("CLI commands", function() describe("'kong'", function() it("outputs usage by default", function() - local ok, _, stdout, stderr = exec() -- 'kong' - assert.False(ok) - assert.equal("", stdout) + local _, stderr, stdout = helpers.kong_exec() -- 'kong' + assert.is_nil(stdout) assert.matches("kong COMMAND [OPTIONS]", stderr, nil, true) end) describe("errors", function() it("errors on invalid command", function() - local ok, _, stdout, stderr = exec("foobar") - assert.False(ok) - assert.equal("", stdout) + local _, stderr, stdout = helpers.kong_exec "foobar" + assert.is_nil(stdout) assert.matches("No such command: foobar", stderr, nil, true) end) end) diff --git a/spec/02-integration/01-cmd/02-start_stop_spec.lua b/spec/02-integration/01-cmd/02-start_stop_spec.lua index c78bf1f30fde..113096a94bf9 100644 --- a/spec/02-integration/01-cmd/02-start_stop_spec.lua +++ b/spec/02-integration/01-cmd/02-start_stop_spec.lua @@ -1,222 +1,172 @@ local helpers = require "spec.helpers" -local KILL_ALL = "pkill nginx; pkill serf; pkill dnsmasq" - -local function exec(args, env) - args = args or "" - env = env or {} - - local env_vars = "" - for k, v in pairs(env) do - env_vars = string.format("%s KONG_%s=%s", env_vars, k:upper(), v) - end - return helpers.execute(env_vars.." "..helpers.bin_path.." "..args) -end - describe("kong start/stop", function() setup(function() - helpers.execute(KILL_ALL) helpers.prepare_prefix() end) teardown(function() - helpers.execute(KILL_ALL) + helpers.kill_all() helpers.clean_prefix() end) + before_each(function() + helpers.kill_all() + end) it("start help", function() - local _, _, stdout, stderr = exec "start --help" - assert.equal("", stdout) - assert.is_string(stderr) + local _, stderr, stdout = helpers.kong_exec "start --help" + assert.is_nil(stdout) assert.not_equal("", stderr) end) it("stop help", function() - local _, _, stdout, stderr = exec "stop --help" - assert.equal("", stdout) - assert.is_string(stderr) + local _, stderr, stdout = helpers.kong_exec "stop --help" + assert.is_nil(stdout) assert.not_equal("", stderr) end) it("start/stop default conf/prefix", function() -- don't want to force migrations to be run on default -- keyspace/database - local _, _, stdout, stderr = exec "start" + local _, stderr, stdout = helpers.kong_exec "start" assert.equal("", stderr) assert.not_equal("", stdout) - ok, _, stdout, stderr = exec "stop" + _, stderr, stdout = helpers.kong_exec "stop" assert.not_equal("", stdout) assert.equal("", stderr) - assert.True(ok) end) it("start/stop custom Kong conf/prefix", function() - local _, _, stdout, stderr = exec("start --conf "..helpers.test_conf_path) + local _, stderr, stdout = helpers.kong_exec("start --conf "..helpers.test_conf_path) assert.equal("", stderr) assert.not_equal("", stdout) - _, _, stdout, stderr = exec("stop --conf "..helpers.test_conf_path) + _, stderr, stdout = helpers.kong_exec("stop --prefix "..helpers.test_conf.prefix) assert.equal("", stderr) assert.not_equal("", stdout) end) it("start with inexistent prefix", function() finally(function() - helpers.execute(KILL_ALL) pcall(helpers.dir.rmtree, "foobar") end) - local _, _, stdout, stderr = exec "start --prefix foobar" + local _, stderr, stdout = helpers.kong_exec "start --prefix foobar" assert.equal("", stderr) assert.not_equal("", stdout) end) - it("start dump config in prefix", function() - local _, _, stdout, stderr = exec("start --conf "..helpers.test_conf_path) + it("start dumps Kong config in prefix", function() + local _, stderr, stdout = helpers.kong_exec("start --conf "..helpers.test_conf_path) assert.equal("", stderr) assert.not_equal("", stdout) local conf_path = helpers.path.join(helpers.test_conf.prefix, "kong.conf") assert.truthy(helpers.path.exists(conf_path)) - _, _, stdout, stderr = exec("stop --conf "..helpers.test_conf_path) + _, stderr, stdout = helpers.kong_exec("stop --prefix "..helpers.test_conf.prefix) assert.equal("", stderr) assert.not_equal("", stdout) end) describe("verbose args", function() it("accepts verbose --v", function() - local _, _, stdout, stderr = exec("start --v --conf "..helpers.test_conf_path) + local _, stderr, stdout = helpers.kong_exec("start --v --conf "..helpers.test_conf_path) assert.equal("", stderr) assert.matches("[verbose] prefix in use: ", stdout, nil, true) - - finally(function() - helpers.execute(KILL_ALL) - end) end) it("accepts debug --vv", function() - finally(function() - helpers.execute(KILL_ALL) - end) - - local _, _, stdout, stderr = exec("start --vv --conf "..helpers.test_conf_path) + local _, stderr, stdout = helpers.kong_exec("start --vv --conf "..helpers.test_conf_path) assert.matches("[verbose] prefix in use: ", stdout, nil, true) assert.matches("[debug] prefix = ", stdout, nil, true) assert.matches("[debug] database = ", stdout, nil, true) assert.equal("", stderr) end) it("should start with an inexistent prefix", function() - local ok, _, stdout, stderr = exec "start --prefix foobar" - assert.True(ok) - assert.not_equal("", stdout) - assert.equal("", stderr) finally(function() - helpers.execute(KILL_ALL) - helpers.dir.rmtree("foobar") + helpers.kill_all() + pcall(helpers.dir.rmtree, "foobar") end) + + local _, stderr, stdout = helpers.kong_exec "start --prefix foobar" + assert.not_equal("", stdout) + assert.equal("", stderr) end) end) describe("Serf", function() it("starts Serf agent daemon", function() - local _, _, _, stderr = exec("start --conf "..helpers.test_conf_path) - assert.equal("", stderr) + assert(helpers.kong_exec("start --conf "..helpers.test_conf_path)) local serf_pid_path = helpers.path.join(helpers.test_conf.prefix, "pids", "serf.pid") local cmd = string.format("kill -0 `cat %s` >/dev/null 2>&1", serf_pid_path) - local ok, code = helpers.execute(cmd) - assert.True(ok) - assert.equal(0, code) - - assert.True(exec("stop --conf "..helpers.test_conf_path)) + assert(helpers.execute(cmd)) end) it("recovers from expired serf.pid file", function() local serf_pid_path = helpers.path.join(helpers.test_conf.prefix, "pids", "serf.pid") - local _, _, _, stderr = helpers.execute("touch "..serf_pid_path) -- dumb pid - assert.equal("", stderr) - - local _, _, _, stderr = exec("start --conf "..helpers.test_conf_path) - assert.equal("", stderr) + assert(helpers.execute("touch "..serf_pid_path)) -- dumb pid + assert(helpers.kong_exec("start --conf "..helpers.test_conf_path)) local cmd = string.format("kill -0 `cat %s` >/dev/null 2>&1", serf_pid_path) - local ok, code = helpers.execute(cmd) - assert.True(ok) - assert.equal(0, code) - - assert.True(exec("stop --conf "..helpers.test_conf_path)) + assert(helpers.execute(cmd)) end) end) describe("dnsmasq", function() it("starts dnsmasq daemon", function() - local ok = exec("start --conf "..helpers.test_conf_path, { + assert(helpers.kong_exec("start --conf "..helpers.test_conf_path, { dnsmasq = true, dns_resolver = "" - }) - assert.True(ok) + })) local dnsmasq_pid_path = helpers.path.join(helpers.test_conf.prefix, "pids", "dnsmasq.pid") local cmd = string.format("kill -0 `cat %s` >/dev/null 2>&1", dnsmasq_pid_path) - local ok, code = helpers.execute(cmd) - assert.True(ok) + local _, code = helpers.utils.executeex(cmd) assert.equal(0, code) - - assert.True(exec("stop --conf "..helpers.test_conf_path)) end) it("recovers from expired dnsmasq.pid file", function() local dnsmasq_pid_path = helpers.path.join(helpers.test_conf.prefix, "pids", "dnsmasq.pid") - local _, _, _, stderr = helpers.execute("touch "..dnsmasq_pid_path) -- dumb pid - assert.equal("", stderr) + assert(helpers.execute("touch "..dnsmasq_pid_path)) -- dumb pid - assert.True(exec("start --conf "..helpers.test_conf_path, { + assert(helpers.kong_exec("start --conf "..helpers.test_conf_path, { dnsmasq = true, dns_resolver = "" })) + local cmd = string.format("kill -0 `cat %s` >/dev/null 2>&1", dnsmasq_pid_path) - local ok, code = helpers.execute(cmd) - assert.True(ok) + local _, code = helpers.utils.executeex(cmd) assert.equal(0, code) - - assert.True(exec("stop --conf "..helpers.test_conf_path)) end) end) describe("errors", function() it("start inexistent Kong conf file", function() - local _, _, stdout, stderr = exec "start --conf foobar.conf" - assert.equal("", stdout) + local _, stderr, stdout = helpers.kong_exec "start --conf foobar.conf" + assert.is_nil(stdout) assert.is_string(stderr) assert.matches("Error: no file at: foobar.conf", stderr, nil, true) end) it("stop inexistent prefix", function() finally(function() - helpers.execute(KILL_ALL) - helpers.dir.rmtree(helpers.test_conf.prefix) + pcall(helpers.dir.rmtree, helpers.test_conf.prefix) end) - assert(helpers.dir.makepath(helpers.test_conf.prefix)) - - local ok, _, stdout, stderr = exec("start --prefix "..helpers.test_conf.prefix) - assert.True(ok) - assert.not_equal("", stdout) + local _, stderr, stdout = helpers.kong_exec("start --prefix "..helpers.test_conf.prefix) assert.equal("", stderr) + assert.not_equal("", stdout) - ok, _, stdout, stderr = exec("stop --prefix inexistent --conf "..helpers.test_conf_path) - assert.False(ok) - assert.equal("", stdout) - assert.matches("Error: could not get Nginx pid", stderr, nil, true) + _, stderr, stdout = helpers.kong_exec("stop --prefix inexistent") + assert.is_nil(stdout) + assert.matches("Error: no such prefix: inexistent", stderr, nil, true) end) it("notifies when Nginx is already running", function() finally(function() - helpers.execute(KILL_ALL) - helpers.dir.rmtree(helpers.test_conf.prefix) + pcall(helpers.dir.rmtree, helpers.test_conf.prefix) end) assert(helpers.dir.makepath(helpers.test_conf.prefix)) - local ok, _, stdout, stderr = exec("start --prefix "..helpers.test_conf.prefix) - assert.True(ok) - assert.not_equal("", stdout) + local _, stderr, stdout = helpers.kong_exec("start --prefix "..helpers.test_conf.prefix) assert.equal("", stderr) + assert.not_equal("", stdout) - local ok, _, stdout, stderr = exec("start --prefix "..helpers.test_conf.prefix) - assert.False(ok) - assert.equal("", stdout) + _, stderr, stdout = helpers.kong_exec("start --prefix "..helpers.test_conf.prefix) + assert.is_nil(stdout) assert.matches("Nginx is already running in", stderr) end) end) diff --git a/spec/02-integration/01-cmd/03-compile_spec.lua b/spec/02-integration/01-cmd/03-compile_spec.lua index 9ce8adb649fb..7ff50ef80008 100644 --- a/spec/02-integration/01-cmd/03-compile_spec.lua +++ b/spec/02-integration/01-cmd/03-compile_spec.lua @@ -1,15 +1,9 @@ local helpers = require "spec.helpers" -local function exec(args) - args = args or "" - return helpers.execute(helpers.bin_path.." "..args) -end - describe("kong compile", function() it("compiles a Kong NGINX config", function() - local ok, _, stdout, stderr = exec "compile" + local _, stderr, stdout = helpers.kong_exec "compile" assert.equal("", stderr) - assert.True(ok) assert.matches("init_by_lua_block", stdout) assert.matches("init_worker_by_lua_block", stdout) assert.matches("lua_code_cache on", stdout) @@ -20,9 +14,8 @@ describe("kong compile", function() assert.matches("listen 0.0.0.0:8443 ssl", stdout, nil, true) end) it("accepts a custom Kong conf", function() - local ok, _, stdout, stderr = exec("compile --conf "..helpers.test_conf_path) + local _, stderr, stdout = helpers.kong_exec("compile --conf "..helpers.test_conf_path) assert.equal("", stderr) - assert.True(ok) assert.matches("listen 0.0.0.0:9000", stdout, nil, true) assert.matches("listen 0.0.0.0:9001", stdout, nil, true) assert.matches("listen 0.0.0.0:9443 ssl", stdout, nil, true) diff --git a/spec/02-integration/01-cmd/04-reload_spec.lua b/spec/02-integration/01-cmd/04-reload_spec.lua index f3c06aaf385b..c46952b50d30 100644 --- a/spec/02-integration/01-cmd/04-reload_spec.lua +++ b/spec/02-integration/01-cmd/04-reload_spec.lua @@ -1,27 +1,26 @@ local helpers = require "spec.helpers" -local KILL_ALL = "pkill nginx; pkill serf; pkill dnsmasq" - describe("kong reload", function() setup(function() - helpers.execute(KILL_ALL) + helpers.kill_all() helpers.prepare_prefix() end) teardown(function() - helpers.execute(KILL_ALL) + helpers.kill_all() helpers.clean_prefix() end) it("send a HUP signal to a running Nginx master process", function() finally(function() - helpers.execute(KILL_ALL) + helpers.kill_all() end) assert(helpers.start_kong()) - local pid_path = helpers.path.join(helpers.test_conf.prefix, "logs", "nginx.pid") + local pid_path = helpers.path.join(helpers.test_conf.prefix, "pids", "nginx.pid") local nginx_pid = helpers.file.read(pid_path) - assert(helpers.kong_exec("reload")) -- kong_exec uses test conf too, so same prefix + local ok, err = helpers.kong_exec("reload --prefix "..helpers.test_conf.prefix) -- kong_exec uses test conf too, so same prefix + assert(ok, err) -- same master PID assert.equal(nginx_pid, helpers.file.read(pid_path)) diff --git a/spec/02-integration/01-cmd/05-version_spec.lua b/spec/02-integration/01-cmd/05-version_spec.lua index 0d6cb3aab269..72427c350c27 100644 --- a/spec/02-integration/01-cmd/05-version_spec.lua +++ b/spec/02-integration/01-cmd/05-version_spec.lua @@ -4,14 +4,12 @@ local meta = require "kong.meta" describe("kong version", function() it("outputs Kong version", function() - local ok, _, stdout, stderr = helpers.execute(helpers.bin_path.." version") - assert.True(ok) + local _, stderr, stdout = helpers.kong_exec("version") assert.equal("", stderr) assert.equal(meta._VERSION, pl_stringx.strip(stdout)) end) it("--all outputs all deps versions", function() - local ok, _, stdout, stderr = helpers.execute(helpers.bin_path.." version -a") - assert.True(ok) + local _, stderr, stdout = helpers.kong_exec("version -a") assert.equal("", stderr) assert.matches([[ Kong: %d+%.%d+%.%d+ diff --git a/spec/02-integration/01-cmd/06-check_spec.lua b/spec/02-integration/01-cmd/06-check_spec.lua index 3a582e8298e4..b182393b772b 100644 --- a/spec/02-integration/01-cmd/06-check_spec.lua +++ b/spec/02-integration/01-cmd/06-check_spec.lua @@ -2,29 +2,21 @@ local helpers = require "spec.helpers" local INVALID_CONF_PATH = "spec/fixtures/invalid.conf" -local function exec(args) - args = args or "" - return helpers.execute(helpers.bin_path.." "..args) -end - describe("kong check", function() it("validates a conf", function() - local ok, _, stdout, stderr = exec("check "..helpers.test_conf_path) - assert.True(ok) + local _, stderr, stdout = helpers.kong_exec("check "..helpers.test_conf_path) assert.equal("", stderr) assert.matches("configuration at .- is valid", stdout) end) it("reports invalid conf", function() - local ok, _, stdout, stderr = exec("check "..INVALID_CONF_PATH) - assert.False(ok) - assert.equal("", stdout) + local _, stderr, stdout = helpers.kong_exec("check "..INVALID_CONF_PATH) + assert.is_nil(stdout) assert.matches("[error] cassandra_repl_strategy has", stderr, nil, true) assert.matches("[error] when specifying a custom DNS resolver you must turn off dnsmasq", stderr, nil, true) end) it("doesn't like invalid files", function() - local ok, _, stdout, stderr = exec("check inexistent.conf") - assert.False(ok) - assert.equal("", stdout) + local _, stderr, stdout = helpers.kong_exec("check inexistent.conf") + assert.is_nil(stdout) assert.matches("[error] no file at: inexistent.conf", stderr, nil, true) end) end) diff --git a/spec/02-integration/01-cmd/07-cluster_spec.lua b/spec/02-integration/01-cmd/07-cluster_spec.lua index 8e6f03978c4d..93df414cec6c 100644 --- a/spec/02-integration/01-cmd/07-cluster_spec.lua +++ b/spec/02-integration/01-cmd/07-cluster_spec.lua @@ -1,14 +1,8 @@ local helpers = require "spec.helpers" -local function exec(args) - args = args or "" - return helpers.execute(helpers.bin_path.." "..args) -end - describe("kong cluster", function() it("keygen", function() - local ok, _, stdout, stderr = exec "cluster keygen" - assert.True(ok) + local _, stderr, stdout = helpers.kong_exec "cluster keygen" assert.equal("", stderr) assert.equal(26, stdout:len()) -- 24 + \r\n end) diff --git a/spec/02-integration/03-admin_api/06-cluster_routes_spec.lua b/spec/02-integration/03-admin_api/06-cluster_routes_spec.lua index e04af4018969..08225581f7b3 100644 --- a/spec/02-integration/03-admin_api/06-cluster_routes_spec.lua +++ b/spec/02-integration/03-admin_api/06-cluster_routes_spec.lua @@ -4,8 +4,7 @@ local cjson = require "cjson" describe("Admin API", function() local client setup(function() - helpers.dao:truncate_tables() - helpers.execute "pkill nginx; pkill serf" + helpers.kill_all() assert(helpers.prepare_prefix()) assert(helpers.start_kong()) @@ -78,9 +77,7 @@ describe("Admin API", function() it("force-leaves a node", function() -- old test converted local cmd = string.format("serf join -rpc-addr=%s 127.0.0.1:20001", helpers.test_conf.cluster_listen_rpc) - local ok, _, _, stderr = helpers.execute(cmd) - assert.equal("", stderr) - assert.True(ok) + assert(helpers.execute(cmd)) local res = assert(client:send { method = "GET", diff --git a/spec/02-integration/05-proxy/01-resolver_spec.lua b/spec/02-integration/05-proxy/01-resolver_spec.lua index cacaf9bcb1a1..5f42b5ec255b 100644 --- a/spec/02-integration/05-proxy/01-resolver_spec.lua +++ b/spec/02-integration/05-proxy/01-resolver_spec.lua @@ -5,6 +5,7 @@ local meta = require "kong.meta" describe("Resolver", function() local client setup(function() + helpers.kill_all() helpers.dao:truncate_tables() assert(helpers.prepare_prefix()) diff --git a/spec/02-integration/05-proxy/02-real_ip_spec.lua b/spec/02-integration/05-proxy/02-real_ip_spec.lua index 0603f3dce4e0..2c7249ef6544 100644 --- a/spec/02-integration/05-proxy/02-real_ip_spec.lua +++ b/spec/02-integration/05-proxy/02-real_ip_spec.lua @@ -4,6 +4,7 @@ local cjson = require "cjson" describe("Real IP proxying", function() local client setup(function() + helpers.kill_all() helpers.dao:truncate_tables() assert(helpers.dao.apis:insert { diff --git a/spec/02-integration/05-proxy/03-plugins_triggering_spec.lua b/spec/02-integration/05-proxy/03-plugins_triggering_spec.lua index 8728c93a0815..bd4858ef1614 100644 --- a/spec/02-integration/05-proxy/03-plugins_triggering_spec.lua +++ b/spec/02-integration/05-proxy/03-plugins_triggering_spec.lua @@ -3,6 +3,7 @@ local helpers = require "spec.helpers" describe("Plugins triggering", function() local client setup(function() + helpers.kill_all() helpers.dao:truncate_tables() local consumer = assert(helpers.dao.consumers:insert { diff --git a/spec/02-integration/06-cluster/cluster_spec.lua b/spec/02-integration/06-cluster/cluster_spec.lua index a47ae893b76d..ce6e7f287bc8 100644 --- a/spec/02-integration/06-cluster/cluster_spec.lua +++ b/spec/02-integration/06-cluster/cluster_spec.lua @@ -3,7 +3,6 @@ local cache = require "kong.tools.database_cache" local pl_stringx = require "pl.stringx" local pl_tablex = require "pl.tablex" local cjson = require "cjson" -local KILL_ALL = "pkill nginx; pkill serf; pkill dnsmasq" local function exec(args, env) args = args or "" @@ -45,11 +44,11 @@ local NODES = { describe("Cluster", function() before_each(function() + helpers.kill_all() helpers.dao:truncate_tables() - helpers.execute(KILL_ALL) end) after_each(function() - helpers.execute(KILL_ALL) + helpers.kill_all() for k, v in pairs(NODES) do helpers.clean_prefix(k) end @@ -344,26 +343,28 @@ describe("Cluster", function() end, 60) -- The member has now failed, let's bring him up again - os.execute(string.format("serf agent -profile=wan -node=%s -rpc-addr=%s -bind=%s -event-handler=member-join,member-leave,member-failed,member-update,member-reap,user:kong=%s/serf/serf_event.sh > /dev/null &", - node_name, NODES.servroot2.cluster_listen_rpc, NODES.servroot2.cluster_listen, NODES.servroot2.prefix)) + os.execute(string.format("serf agent -profile=wan -node=%s -rpc-addr=%s" + .." -bind=%s event-handler=member-join," + .."member-leave,member-failed,member-update," + .."member-reap,user:kong=%s/serf/serf_event.sh > /dev/null &", + node_name, + NODES.servroot2.cluster_listen_rpc, + NODES.servroot2.cluster_listen, + NODES.servroot2.prefix)) -- Now wait until the nodes becomes active again helpers.wait_until(function() local res = assert(api_client:send { method = "GET", - path = "/cluster/", - headers = {} + path = "/cluster/" }) local body = cjson.decode(assert.res_status(200, res)) - local all_alive = true for _, v in ipairs(body.data) do if v.status == "failed" then - all_alive = false + return false end end - if not all_alive then ngx.sleep(1) end - - return all_alive + return true end, 60) -- The cache should have been deleted on every node available diff --git a/spec/helpers.lua b/spec/helpers.lua index be10ae77cd46..9754d195cded 100644 --- a/spec/helpers.lua +++ b/spec/helpers.lua @@ -185,15 +185,23 @@ luassert:register("assertion", "res_status", res_status, -- Shell helpers ---------------- local function exec(...) - local ok, _, _, stderr = pl_utils.executeex(...) - return ok, stderr + local ok, _, stdout, stderr = pl_utils.executeex(...) + if not ok then + stdout = nil -- don't return 3rd value if fail because of busted's `assert` + end + return ok, stderr, stdout end -local function kong_exec(args, prefix) - args = args or "" - prefix = prefix or conf.prefix +local function kong_exec(cmd, env) + cmd = cmd or "" + env = env or {} - return exec(BIN_PATH.." "..args.." --prefix "..prefix) + local env_vars = "" + for k, v in pairs(env) do + env_vars = string.format("%s KONG_%s=%s", env_vars, k:upper(), v) + end + + return exec(env_vars.." "..BIN_PATH.." "..cmd) end ---------- @@ -204,7 +212,7 @@ return { dir = pl_dir, path = pl_path, file = pl_file, - execute = pl_utils.executeex, + utils = pl_utils, -- Kong testing properties dao = dao, @@ -213,6 +221,7 @@ return { test_conf_path = TEST_CONF_PATH, -- Kong testing helpers + execute = exec, kong_exec = kong_exec, http_client = http_client, wait_until = wait_until, @@ -221,7 +230,6 @@ return { prepare_prefix = function(prefix) prefix = prefix or conf.prefix return pl_dir.makepath(prefix) - --kong_exec("stop", prefix) end, clean_prefix = function(prefix) prefix = prefix or conf.prefix @@ -229,10 +237,14 @@ return { pl_dir.rmtree(prefix) end end, - start_kong = function(prefix) - return kong_exec("start --conf "..TEST_CONF_PATH, prefix) + start_kong = function() + return kong_exec("start --conf "..TEST_CONF_PATH) + end, + stop_kong = function() + return kong_exec("stop --conf "..TEST_CONF_PATH) end, - stop_kong = function(prefix) - return kong_exec("stop --conf "..TEST_CONF_PATH, prefix) + kill_all = function() + dao:truncate_tables() -- truncate nodes table too + return exec "pkill nginx; pkill serf; pkill dnsmasq" end } diff --git a/spec/kong_tests.conf b/spec/kong_tests.conf index 4267df313f53..47efc3fa8d0a 100644 --- a/spec/kong_tests.conf +++ b/spec/kong_tests.conf @@ -21,4 +21,4 @@ nginx_worker_processes = 1 nginx_optimizations = off prefix = servroot -log_level=info +log_level = debug From 320ad229650e6b294b12faa6d261e8a8f876bfa5 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Wed, 22 Jun 2016 18:43:06 -0700 Subject: [PATCH 24/29] refactor(cli) use assert() instead of error() --- kong/cmd/cluster.lua | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/kong/cmd/cluster.lua b/kong/cmd/cluster.lua index affa6a5e9d2e..edc9f0657e36 100644 --- a/kong/cmd/cluster.lua +++ b/kong/cmd/cluster.lua @@ -19,9 +19,7 @@ local function execute(args) print(assert(serf:reachability())) elseif args.command == "force-leave" then local node_name = args[1] - if not node_name then - error("you need to specify the node name to leave") - end + assert(node_name ~= nil, "you need to specify the node name to leave") log("force-leaving %s", node_name) assert(serf:force_leave(node_name)) log("left node %s", node_name) @@ -44,5 +42,10 @@ Options: return { lapp = lapp, execute = execute, - sub_commands = {members = true, keygen = true, reachability = true, ["force-leave"] = true} + sub_commands = { + members = true, + keygen = true, + reachability = true, + ["force-leave"] = true + } } From 42329af9df56245914701b4bd7aa764e382f9c20 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Wed, 22 Jun 2016 18:53:21 -0700 Subject: [PATCH 25/29] docs(cli) correct help messages for --prefix arg --- kong/cmd/reload.lua | 2 +- kong/cmd/start.lua | 9 +-------- kong/cmd/stop.lua | 2 +- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/kong/cmd/reload.lua b/kong/cmd/reload.lua index 41ae6f51a7c5..f92c8d31cf2b 100644 --- a/kong/cmd/reload.lua +++ b/kong/cmd/reload.lua @@ -28,7 +28,7 @@ local lapp = [[ Usage: kong reload [OPTIONS] Options: - --prefix (optional string) Nginx prefix path + --prefix (optional string) prefix Kong is running at ]] return { diff --git a/kong/cmd/start.lua b/kong/cmd/start.lua index af91e47c9011..1baff47f40fd 100644 --- a/kong/cmd/start.lua +++ b/kong/cmd/start.lua @@ -6,13 +6,6 @@ local conf_loader = require "kong.conf_loader" local DAOFactory = require "kong.dao.factory" local log = require "kong.cmd.utils.log" ---[[ -Start Kong. - -Kong being a bundle of several applications and services, start acts -as follows: ---]] - local function execute(args) local conf = assert(conf_loader(args.conf, { prefix = args.prefix @@ -34,7 +27,7 @@ Usage: kong start [OPTIONS] Options: -c,--conf (optional string) configuration file - --prefix (optional string) Nginx prefix path + --prefix (optional string) override prefix directory ]] return { diff --git a/kong/cmd/stop.lua b/kong/cmd/stop.lua index 42caa5b6317f..def717eb5ede 100644 --- a/kong/cmd/stop.lua +++ b/kong/cmd/stop.lua @@ -29,7 +29,7 @@ local lapp = [[ Usage: kong stop [OPTIONS] Options: - --prefix (optional string) Nginx prefix path + --prefix (optional string) prefix Kong is running at ]] return { From a9d7287168eed6a97a3d078a99458591530ce830 Mon Sep 17 00:00:00 2001 From: Thibault Charbonnier Date: Wed, 22 Jun 2016 20:49:09 -0700 Subject: [PATCH 26/29] refactor(cli) polishing SSL generation --- kong/cmd/cluster.lua | 4 +- kong/cmd/utils/dnsmasq_signals.lua | 5 +- kong/cmd/utils/nginx_signals.lua | 3 +- kong/cmd/utils/ssl.lua | 21 +++--- kong/plugins/acl/api.lua | 2 +- kong/serf.lua | 11 ++-- spec/01-unit/16-ssl_spec.lua | 26 ++++---- .../01-cmd/02-start_stop_spec.lua | 65 +++++++------------ 8 files changed, 57 insertions(+), 80 deletions(-) diff --git a/kong/cmd/cluster.lua b/kong/cmd/cluster.lua index edc9f0657e36..ca1bc5595a02 100644 --- a/kong/cmd/cluster.lua +++ b/kong/cmd/cluster.lua @@ -19,7 +19,7 @@ local function execute(args) print(assert(serf:reachability())) elseif args.command == "force-leave" then local node_name = args[1] - assert(node_name ~= nil, "you need to specify the node name to leave") + assert(node_name ~= nil, "must specify the name of the node to leave") log("force-leaving %s", node_name) assert(serf:force_leave(node_name)) log("left node %s", node_name) @@ -43,8 +43,8 @@ return { lapp = lapp, execute = execute, sub_commands = { - members = true, keygen = true, + members = true, reachability = true, ["force-leave"] = true } diff --git a/kong/cmd/utils/dnsmasq_signals.lua b/kong/cmd/utils/dnsmasq_signals.lua index 857c7e6151e1..688d9509309d 100644 --- a/kong/cmd/utils/dnsmasq_signals.lua +++ b/kong/cmd/utils/dnsmasq_signals.lua @@ -1,8 +1,8 @@ local pl_utils = require "pl.utils" local pl_path = require "pl.path" local pl_file = require "pl.file" -local log = require "kong.cmd.utils.log" local kill = require "kong.cmd.utils.kill" +local log = require "kong.cmd.utils.log" local fmt = string.format local _M = {} @@ -25,8 +25,7 @@ function _M.find_bin() for _, path in ipairs(dnsmasq_search_paths) do local path_to_check = pl_path.join(path, dnsmasq_bin_name) local cmd = fmt("%s -v", path_to_check) - local ok = pl_utils.executeex(cmd) - if ok then + if pl_utils.executeex(cmd) then found = path_to_check break end diff --git a/kong/cmd/utils/nginx_signals.lua b/kong/cmd/utils/nginx_signals.lua index b6cadff1d82f..8c8cb754d93a 100644 --- a/kong/cmd/utils/nginx_signals.lua +++ b/kong/cmd/utils/nginx_signals.lua @@ -49,8 +49,7 @@ function _M.find_bin() local found for _, path in ipairs(nginx_search_paths) do local path_to_check = pl_path.join(path, nginx_bin_name) - local ok = is_openresty(path_to_check) - if ok then + if is_openresty(path_to_check) then found = path_to_check break end diff --git a/kong/cmd/utils/ssl.lua b/kong/cmd/utils/ssl.lua index 3bdf76db0125..4746c148e345 100644 --- a/kong/cmd/utils/ssl.lua +++ b/kong/cmd/utils/ssl.lua @@ -1,8 +1,8 @@ +local log = require "kong.cmd.utils.log" local utils = require "kong.tools.utils" +local pl_dir = require "pl.dir" local pl_path = require "pl.path" local pl_utils = require "pl.utils" -local pl_dir = require "pl.dir" -local log = require "kong.cmd.utils.log" local fmt = string.format local _M = {} @@ -22,19 +22,18 @@ function _M.get_ssl_cert_and_key(kong_config, nginx_prefix) ssl_cert_key = pl_path.join(nginx_prefix, SSL_FOLDER, SSL_CERT_KEY) end - -- Check that the files exist + -- check that the files exist if not pl_path.exists(ssl_cert) then return nil, "cannot find SSL certificate at: "..ssl_cert - end - if not pl_path.exists(ssl_cert_key) then + elseif not pl_path.exists(ssl_cert_key) then return nil, "cannot find SSL key at: "..ssl_cert_key end - return { ssl_cert = ssl_cert, ssl_cert_key = ssl_cert_key } + return {ssl_cert = ssl_cert, ssl_cert_key = ssl_cert_key} end function _M.prepare_ssl_cert_and_key(prefix) - -- Create SSL directory + -- create SSL directory local ssl_path = pl_path.join(prefix, SSL_FOLDER) local ok, err = pl_dir.makepath(ssl_path) if not ok then return nil, err end @@ -44,8 +43,8 @@ function _M.prepare_ssl_cert_and_key(prefix) local ssl_cert_csr = pl_path.join(prefix, SSL_FOLDER, SSL_CERT_CSR) if not (pl_path.exists(ssl_cert) and pl_path.exists(ssl_cert_key)) then - -- Autogenerating the certificates for the first time - log.verbose("Auto-generating the default SSL certificate and key..") + -- generating the certificates for the first time + log.verbose("auto-generating default SSL certificate and key...") local passphrase = utils.random_string() local commands = { @@ -60,7 +59,7 @@ function _M.prepare_ssl_cert_and_key(prefix) for _, cmd in ipairs(commands) do local ok, _, _, stderr = pl_utils.executeex(cmd) if not ok then - return nil, "there was an error when auto-generating the default SSL certificate: "..stderr + return nil, "could not generate default SSL certificate: "..stderr end end end @@ -68,4 +67,4 @@ function _M.prepare_ssl_cert_and_key(prefix) return true end -return _M \ No newline at end of file +return _M diff --git a/kong/plugins/acl/api.lua b/kong/plugins/acl/api.lua index fd4f97f8c7ca..49821de3aa44 100644 --- a/kong/plugins/acl/api.lua +++ b/kong/plugins/acl/api.lua @@ -31,7 +31,7 @@ return { } if err then return helpers.yield_error(err) - elseif next(acls) == nil then + elseif #acls == 0 then return helpers.responses.send_HTTP_NOT_FOUND() end diff --git a/kong/serf.lua b/kong/serf.lua index 792206fde249..7e540679bbca 100644 --- a/kong/serf.lua +++ b/kong/serf.lua @@ -76,15 +76,11 @@ function Serf:members() end function Serf:keygen() - local res, err = self:invoke_signal("keygen") - if not res then return nil, err end - return res + return self:invoke_signal("keygen") end function Serf:reachability() - local res, err = self:invoke_signal("reachability") - if not res then return nil, err end - return res + return self:invoke_signal("reachability") end function Serf:autojoin() @@ -108,7 +104,8 @@ function Serf:autojoin() joined = true break else - log.warn("could not join %s, if the node does not exist anymore it will be automatically purged", v.cluster_listening_address) + log.warn("could not join %s (if the node does not exist anymore it will be automatically purged)", + v.cluster_listening_address) end end if not joined then diff --git a/spec/01-unit/16-ssl_spec.lua b/spec/01-unit/16-ssl_spec.lua index aed5788d7fa8..5a4d15a259d7 100644 --- a/spec/01-unit/16-ssl_spec.lua +++ b/spec/01-unit/16-ssl_spec.lua @@ -1,26 +1,26 @@ -local pl_path = require "pl.path" -local pl_dir = require "pl.dir" +local helpers = require "spec.helpers" local ssl = require "kong.cmd.utils.ssl" -describe("SSL Utils", function() - +describe("SSL utils", function() + local exists = helpers.path.exists + local join = helpers.path.join setup(function() - pcall(pl_dir.rmtree, "/tmp/ssl") + helpers.dir.makepath("ssl_tmp") + end) + teardown(function() + pcall(helpers.dir.rmtree, "ssl_tmp") end) it("should auto-generate an SSL certificate and key", function() - assert(ssl.prepare_ssl_cert_and_key("/tmp")) - assert(pl_path.exists("/tmp/ssl/kong-default.crt")) - assert(pl_path.exists("/tmp/ssl/kong-default.key")) + assert(ssl.prepare_ssl_cert_and_key("ssl_tmp")) + assert(exists(join("ssl_tmp", "ssl", "kong-default.crt"))) + assert(exists(join("ssl_tmp", "ssl", "kong-default.key"))) end) it("retrieve the default SSL certificate and key", function() - local ssl_data, err = ssl.get_ssl_cert_and_key({}, "/tmp") + local ssl_data = assert(ssl.get_ssl_cert_and_key({}, "ssl_tmp")) assert.is_table(ssl_data) - assert.is_nil(err) - assert.is_string(ssl_data.ssl_cert) assert.is_string(ssl_data.ssl_cert_key) end) - -end) \ No newline at end of file +end) diff --git a/spec/02-integration/01-cmd/02-start_stop_spec.lua b/spec/02-integration/01-cmd/02-start_stop_spec.lua index 113096a94bf9..0f942d5020b6 100644 --- a/spec/02-integration/01-cmd/02-start_stop_spec.lua +++ b/spec/02-integration/01-cmd/02-start_stop_spec.lua @@ -13,55 +13,39 @@ describe("kong start/stop", function() end) it("start help", function() - local _, stderr, stdout = helpers.kong_exec "start --help" - assert.is_nil(stdout) + local _, stderr = helpers.kong_exec "start --help" assert.not_equal("", stderr) end) it("stop help", function() - local _, stderr, stdout = helpers.kong_exec "stop --help" - assert.is_nil(stdout) + local _, stderr = helpers.kong_exec "stop --help" assert.not_equal("", stderr) end) it("start/stop default conf/prefix", function() -- don't want to force migrations to be run on default -- keyspace/database - local _, stderr, stdout = helpers.kong_exec "start" - assert.equal("", stderr) - assert.not_equal("", stdout) - - _, stderr, stdout = helpers.kong_exec "stop" - assert.not_equal("", stdout) - assert.equal("", stderr) + assert(helpers.kong_exec "start", { + database = helpers.test_conf.database, + pg_database = helpers.test_conf.pg_database, + cassandra_keyspace = helpers.test_conf.cassandra_keyspace + }) + assert(helpers.kong_exec "stop") end) it("start/stop custom Kong conf/prefix", function() - local _, stderr, stdout = helpers.kong_exec("start --conf "..helpers.test_conf_path) - assert.equal("", stderr) - assert.not_equal("", stdout) - - _, stderr, stdout = helpers.kong_exec("stop --prefix "..helpers.test_conf.prefix) - assert.equal("", stderr) - assert.not_equal("", stdout) + assert(helpers.kong_exec("start --conf "..helpers.test_conf_path)) + assert(helpers.kong_exec("stop --prefix "..helpers.test_conf.prefix)) end) it("start with inexistent prefix", function() finally(function() pcall(helpers.dir.rmtree, "foobar") end) - local _, stderr, stdout = helpers.kong_exec "start --prefix foobar" - assert.equal("", stderr) - assert.not_equal("", stdout) + assert(helpers.kong_exec "start --prefix foobar") end) it("start dumps Kong config in prefix", function() - local _, stderr, stdout = helpers.kong_exec("start --conf "..helpers.test_conf_path) - assert.equal("", stderr) - assert.not_equal("", stdout) + assert(helpers.kong_exec("start --conf "..helpers.test_conf_path)) local conf_path = helpers.path.join(helpers.test_conf.prefix, "kong.conf") assert.truthy(helpers.path.exists(conf_path)) - - _, stderr, stdout = helpers.kong_exec("stop --prefix "..helpers.test_conf.prefix) - assert.equal("", stderr) - assert.not_equal("", stdout) end) describe("verbose args", function() @@ -83,9 +67,8 @@ describe("kong start/stop", function() pcall(helpers.dir.rmtree, "foobar") end) - local _, stderr, stdout = helpers.kong_exec "start --prefix foobar" - assert.not_equal("", stdout) - assert.equal("", stderr) + assert(helpers.kong_exec "start --prefix foobar") + assert.truthy(helpers.path.exists("foobar")) end) end) @@ -136,8 +119,8 @@ describe("kong start/stop", function() describe("errors", function() it("start inexistent Kong conf file", function() - local _, stderr, stdout = helpers.kong_exec "start --conf foobar.conf" - assert.is_nil(stdout) + local ok, stderr = helpers.kong_exec "start --conf foobar.conf" + assert.False(ok) assert.is_string(stderr) assert.matches("Error: no file at: foobar.conf", stderr, nil, true) end) @@ -146,12 +129,12 @@ describe("kong start/stop", function() pcall(helpers.dir.rmtree, helpers.test_conf.prefix) end) - local _, stderr, stdout = helpers.kong_exec("start --prefix "..helpers.test_conf.prefix) + local ok, stderr = helpers.kong_exec("start --prefix "..helpers.test_conf.prefix) assert.equal("", stderr) - assert.not_equal("", stdout) + assert.True(ok) - _, stderr, stdout = helpers.kong_exec("stop --prefix inexistent") - assert.is_nil(stdout) + ok, stderr, stdout = helpers.kong_exec("stop --prefix inexistent") + assert.False(ok) assert.matches("Error: no such prefix: inexistent", stderr, nil, true) end) it("notifies when Nginx is already running", function() @@ -161,12 +144,12 @@ describe("kong start/stop", function() assert(helpers.dir.makepath(helpers.test_conf.prefix)) - local _, stderr, stdout = helpers.kong_exec("start --prefix "..helpers.test_conf.prefix) + local ok, stderr = helpers.kong_exec("start --prefix "..helpers.test_conf.prefix) assert.equal("", stderr) - assert.not_equal("", stdout) + assert.True(ok) - _, stderr, stdout = helpers.kong_exec("start --prefix "..helpers.test_conf.prefix) - assert.is_nil(stdout) + ok, stderr = helpers.kong_exec("start --prefix "..helpers.test_conf.prefix) + assert.False(ok) assert.matches("Nginx is already running in", stderr) end) end) From 417ae6a2e42c6e74ab65f1246937e1927af5fc94 Mon Sep 17 00:00:00 2001 From: thefosk Date: Wed, 22 Jun 2016 22:36:34 -0700 Subject: [PATCH 27/29] oauth2 wip --- kong/dao/postgres_db.lua | 32 +- kong/plugins/oauth2/access.lua | 3 + kong/plugins/oauth2/api.lua | 14 +- spec/02-integration/02-dao/03-crud_spec.lua | 50 +- spec/03-plugins/oauth2/access_spec.lua | 425 +++++++++++++++ spec/03-plugins/oauth2/api_old.lua | 263 --------- spec/03-plugins/oauth2/api_spec.lua | 514 ++++++++++++++++++ spec/03-plugins/oauth2/hooks_old.lua | 302 ---------- spec/03-plugins/oauth2/hooks_spec.lua | 512 +++++++++++++++++ .../{schema_old.lua => schema_spec.lua} | 0 10 files changed, 1540 insertions(+), 575 deletions(-) create mode 100644 spec/03-plugins/oauth2/access_spec.lua delete mode 100644 spec/03-plugins/oauth2/api_old.lua create mode 100644 spec/03-plugins/oauth2/api_spec.lua delete mode 100644 spec/03-plugins/oauth2/hooks_old.lua create mode 100644 spec/03-plugins/oauth2/hooks_spec.lua rename spec/03-plugins/oauth2/{schema_old.lua => schema_spec.lua} (100%) diff --git a/kong/dao/postgres_db.lua b/kong/dao/postgres_db.lua index 166984dc65a6..9b3a0e160e79 100644 --- a/kong/dao/postgres_db.lua +++ b/kong/dao/postgres_db.lua @@ -80,7 +80,7 @@ local function escape_literal(val, field) return "'"..tostring((val:gsub("'", "''"))).."'" elseif t_val == "boolean" then return val and "TRUE" or "FALSE" - elseif t_val == "table" and field and field.type == "table" then + elseif t_val == "table" and field and (field.type == "table" or field.type == "array") then local json = require "cjson" return escape_literal(json.encode(val)) end @@ -129,7 +129,7 @@ end -- Querying -function PostgresDB:query(query) +function PostgresDB:query(query, schema) PostgresDB.super.query(self, query) local conn_opts = self:_get_conn_options() @@ -148,6 +148,8 @@ function PostgresDB:query(query) if res == nil then return nil, parse_error(err) + elseif schema ~= nil then + self:deserialize_rows(res, schema) end return res @@ -199,6 +201,20 @@ function PostgresDB:get_select_query(select_clause, schema, table, where, offset return query end +function PostgresDB:deserialize_rows(rows, schema) + if schema then + local json = require "cjson" + for i, row in ipairs(rows) do + for col, value in pairs(row) do + if type(value) == "string" and schema.fields[col] and + (schema.fields[col].type == "table" or schema.fields[col].type == "array") then + rows[i][col] = json.decode(value) + end + end + end + end +end + function PostgresDB:deserialize_timestamps(row, schema) local result = row for k, v in pairs(schema.fields) do @@ -299,7 +315,7 @@ function PostgresDB:insert(table_name, schema, model, _, options) local query = string.format("INSERT INTO %s(%s) VALUES(%s) RETURNING *", table_name, cols, args) - local res, err = self:query(query) + local res, err = self:query(query, schema) if err then return nil, err elseif #res > 0 then @@ -322,7 +338,7 @@ end function PostgresDB:find(table_name, schema, primary_keys) local where = get_where(primary_keys) local query = self:get_select_query(get_select_fields(schema), schema, table_name, where) - local rows, err = self:query(query) + local rows, err = self:query(query, schema) if err then return nil, err elseif rows and #rows > 0 then @@ -337,7 +353,7 @@ function PostgresDB:find_all(table_name, tbl, schema) end local query = self:get_select_query(get_select_fields(schema), schema, table_name, where) - return self:query(query) + return self:query(query, schema) end function PostgresDB:find_page(table_name, tbl, page, page_size, schema) @@ -359,7 +375,7 @@ function PostgresDB:find_page(table_name, tbl, page, page_size, schema) end local query = self:get_select_query(get_select_fields(schema), schema, table_name, where, offset, page_size) - local rows, err = self:query(query) + local rows, err = self:query(query, schema) if err then return nil, err end @@ -405,7 +421,7 @@ function PostgresDB:update(table_name, schema, _, filter_keys, values, nils, ful local where = get_where(filter_keys) local query = string.format("UPDATE %s SET %s WHERE %s RETURNING *", table_name, args, where) - local res, err = self:query(query) + local res, err = self:query(query, schema) if err then return nil, err elseif res and res.affected_rows == 1 then @@ -429,7 +445,7 @@ function PostgresDB:delete(table_name, schema, primary_keys) local where = get_where(primary_keys) local query = string.format("DELETE FROM %s WHERE %s RETURNING *", table_name, where) - local res, err = self:query(query) + local res, err = self:query(query, schema) if err then return nil, err end diff --git a/kong/plugins/oauth2/access.lua b/kong/plugins/oauth2/access.lua index c76a9ca4ac0d..6711f49009b8 100644 --- a/kong/plugins/oauth2/access.lua +++ b/kong/plugins/oauth2/access.lua @@ -9,6 +9,7 @@ local url = require "socket.url" local Multipart = require "multipart" local string_find = string.find local req_get_headers = ngx.req.get_headers +local json = require "cjson" local _M = {} @@ -101,6 +102,8 @@ local function retrieve_parameters() local content_type = req_get_headers()[CONTENT_TYPE] if content_type and string_find(content_type:lower(), "multipart/form-data", nil, true) then body_parameters = Multipart(ngx.req.get_body_data(), content_type):get_all() + elseif content_type and string_find(content_type:lower(), "application/json", nil, true) then + body_parameters = json.decode(ngx.req.get_body_data()) else body_parameters = ngx.req.get_post_args() end diff --git a/kong/plugins/oauth2/api.lua b/kong/plugins/oauth2/api.lua index 40450ce06f7c..c197e317e632 100644 --- a/kong/plugins/oauth2/api.lua +++ b/kong/plugins/oauth2/api.lua @@ -60,8 +60,20 @@ return { ["/consumers/:username_or_id/oauth2/:id"] = { before = function(self, dao_factory, helpers) - crud.find_consumer_by_username_or_id(self, dao_factory, helpers) + crud.find_consumer_by_username_or_id(self, dao_factory, helpers) self.params.consumer_id = self.consumer.id + + local credentials, err = dao_factory.oauth2_credentials:find_all { + consumer_id = self.params.consumer_id, + id = self.params.id + } + if err then + return helpers.yield_error(err) + elseif next(credentials) == nil then + return helpers.responses.send_HTTP_NOT_FOUND() + end + + self.oauth2_credential = credentials[1] end, GET = function(self, dao_factory) diff --git a/spec/02-integration/02-dao/03-crud_spec.lua b/spec/02-integration/02-dao/03-crud_spec.lua index bcc527f73c07..b27e34d2e4f8 100644 --- a/spec/02-integration/02-dao/03-crud_spec.lua +++ b/spec/02-integration/02-dao/03-crud_spec.lua @@ -32,10 +32,15 @@ local api_tbl = { helpers.for_each_dao(function(kong_config) describe("Model (CRUD) with DB: #"..kong_config.database, function() - local factory, apis + local factory, apis, oauth2_credentials setup(function() factory = Factory(kong_config) apis = factory.apis + + -- DAO used for testing arrays + oauth2_credentials = factory.oauth2_credentials + oauth2_credentials.constraints.unique.client_id.schema.fields.consumer_id.required = false + assert(factory:run_migrations()) end) teardown(function() @@ -67,6 +72,32 @@ helpers.for_each_dao(function(kong_config) assert.equal("httpbin", api.name) assert.raw_table(api) end) + it("insert a valid array field and return it properly", function() + local res, err = oauth2_credentials:insert { + name = "test_app", + redirect_uri = "https://mockbin.com" + } + assert.falsy(err) + assert.is_table(res) + assert.equal("test_app", res.name) + assert.is_table(res.redirect_uri) + assert.equal(1, #res.redirect_uri) + assert.same({"https://mockbin.com"}, res.redirect_uri) + assert.raw_table(res) + end) + it("insert a valid array field and return it properly bis", function() + local res, err = oauth2_credentials:insert { + name = "test_app", + redirect_uri = "https://mockbin.com, https://mockbin.org" + } + assert.falsy(err) + assert.is_table(res) + assert.equal("test_app", res.name) + assert.is_table(res.redirect_uri) + assert.equal(2, #res.redirect_uri) + assert.same({"https://mockbin.com", "https://mockbin.org"}, res.redirect_uri) + assert.raw_table(res) + end) it("add DAO-inserted values", function() local api, err = apis:insert(api_tbl) assert.falsy(err) @@ -183,6 +214,13 @@ helpers.for_each_dao(function(kong_config) assert.falsy(err) assert.truthy(api) end + + local res, err = oauth2_credentials:insert { + name = "test_app", + redirect_uri = "https://mockbin.com, https://mockbin.org" + } + assert.falsy(err) + assert.truthy(res) end) teardown(function() factory:truncate_tables() @@ -215,6 +253,16 @@ helpers.for_each_dao(function(kong_config) assert.equal(1, #rows) assert.equal("fixture_100", rows[1].name) end) + it("return rows with arrays", function() + local rows, err = oauth2_credentials:find_all() + assert.falsy(err) + assert.is_table(rows) + assert.equal(1, #rows) + assert.equal("test_app", rows[1].name) + assert.is_table(rows[1].redirect_uri) + assert.equal(2, #rows[1].redirect_uri) + assert.same({"https://mockbin.com", "https://mockbin.org"}, rows[1].redirect_uri) + end) it("return empty table if no row match", function() local rows, err = apis:find_all { request_host = "inexistent.com" diff --git a/spec/03-plugins/oauth2/access_spec.lua b/spec/03-plugins/oauth2/access_spec.lua new file mode 100644 index 000000000000..b8d3afc93130 --- /dev/null +++ b/spec/03-plugins/oauth2/access_spec.lua @@ -0,0 +1,425 @@ +local helpers = require "spec.helpers" +local cjson = require "cjson" +local meta = require "kong.meta" +local pl_stringx = require "pl.stringx" + +describe("Plugin: oauth2", function() + local client + setup(function() + helpers.dao:truncate_tables() + helpers.execute "pkill nginx; pkill serf; pkill dnsmasq" + assert(helpers.prepare_prefix()) + + local consumer = assert(helpers.dao.consumers:insert { + username = "bob" + }) + assert(helpers.dao.oauth2_credentials:insert { + client_id = "clientid123", + client_secret = "secret123", + redirect_uri = "http://google.com/kong", + name = "testapp", + consumer_id = consumer.id + }) + assert(helpers.dao.oauth2_credentials:insert { + client_id = "clientid789", + client_secret = "secret789", + redirect_uri = "http://google.com/kong?foo=bar&code=123", + name = "testapp2", + consumer_id = consumer.id + }) + + local api1 = assert(helpers.dao.apis:insert { + request_host = "oauth2.com", + upstream_url = "http://mockbin.com" + }) + assert(helpers.dao.plugins:insert { + name = "oauth2", + api_id = api1.id, + config = { + scopes = { "email", "profile", "user.email" }, + mandatory_scope = true, + provision_key = "provision123", + token_expiration = 5, + enable_implicit_grant = true + } + }) + + local api2 = assert(helpers.dao.apis:insert { + request_host = "mockbin-path.com", + request_path = "/somepath/", + upstream_url = "http://mockbin.com" + }) + assert(helpers.dao.plugins:insert { + name = "oauth2", + api_id = api2.id, + config = { + scopes = { "email", "profile" }, + mandatory_scope = true, + provision_key = "provision123", + token_expiration = 5, + enable_implicit_grant = true + } + }) + + local api3 = assert(helpers.dao.apis:insert { + request_host = "oauth2_3.com", + upstream_url = "http://mockbin.com" + }) + assert(helpers.dao.plugins:insert { + name = "oauth2", + api_id = api3.id, + config = { + scopes = { "email", "profile" }, + mandatory_scope = true, + provision_key = "provision123", + token_expiration = 5, + enable_implicit_grant = true, + hide_credentials = true + } + }) + + local api4 = assert(helpers.dao.apis:insert { + request_host = "oauth2_4.com", + upstream_url = "http://mockbin.com" + }) + assert(helpers.dao.plugins:insert { + name = "oauth2", + api_id = api4.id, + config = { + scopes = { "email", "profile" }, + mandatory_scope = true, + provision_key = "provision123", + token_expiration = 5, + enable_client_credentials = true, + enable_authorization_code = false + } + }) + + local api5 = assert(helpers.dao.apis:insert { + request_host = "oauth2_5.com", + upstream_url = "http://mockbin.com" + }) + assert(helpers.dao.plugins:insert { + name = "oauth2", + api_id = api5.id, + config = { + scopes = { "email", "profile" }, + mandatory_scope = true, + provision_key = "provision123", + token_expiration = 5, + enable_password_grant = true, + enable_authorization_code = false + } + }) + + local api6 = assert(helpers.dao.apis:insert { + request_host = "oauth2_6.com", + upstream_url = "http://mockbin.com" + }) + assert(helpers.dao.plugins:insert { + name = "oauth2", + api_id = api6.id, + config = { + scopes = { "email", "profile", "user.email" }, + mandatory_scope = true, + provision_key = "provision123", + token_expiration = 5, + enable_implicit_grant = true, + accept_http_if_already_terminated = true + } + }) + + assert(helpers.start_kong()) + proxy_client = assert(helpers.http_client("127.0.0.1", pl_stringx.split(helpers.test_conf.proxy_listen_ssl, ":")[2])) + proxy_client:ssl_handshake() + end) + teardown(function() + if client then + client:close() + end + helpers.stop_kong() + --helpers.clean_prefix() + end) + + local function provision_code() + local res = assert(proxy_client:send { + method = "POST", + path = "/oauth2/authorize", + body = { + provision_key = "provision123", + client_id = "client123", + scope = "email", + response_type = "code", + state = "hello", + authenticated_userid = "userid123" + }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + local body = cjson.decode(res:read_body()) + if body.redirect_uri then + local iterator, err = ngx.re.gmatch(body.redirect_uri, "^http://google\\.com/kong\\?code=([\\w]{32,32})&state=hello$") + assert.is_nil(err) + local m, err = iterator() + assert.is_nil(err) + return m[1] + end + end + + local function provision_token() + local code = provision_code() + local res = assert(proxy_client:send { + method = "POST", + path = "/oauth2/token", + body = { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code" }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + local token = cjson.decode(assert.res_status(200, res)) + assert.is_table(token) + return token + end + + describe("OAuth2 Authorization", function() + describe("Code Grant", function() + it("returns an error when no provision_key is being sent", function() + + end) + it("returns an error when no parameter is being sent", function() + + end) + it("returns an error when only provision_key and authenticated_userid are sent", function() + + end) + it("returns an error when only the client_is being sent", function() + + end) + it("returns an error when only the client_is being sent", function() + + end) + it("returns an error when no response_type is being sent", function() + + end) + it("returns an error with a state when no response_type is being sent", function() + + end) + it("returns error when the redirect_uri does not match", function() + + end) + it("works even if redirect_uri contains a query string", function() + + end) + it("fails when not under HTTPS", function() + + end) + it("works when not under HTTPS but accept_http_if_already_terminated is true", function() + + end) + it("fails when not under HTTPS and accept_http_if_already_terminated is false", function() + + end) + it("returns success", function() + + end) + it("fails with a path when using the DNS", function() + + end) + it("returns success with a path", function() + + end) + it("should return success when requesting the url with final slash", function() + + end) + it("should return success with a state", function() + + end) + it("returns success and store authenticated user properties", function() + + end) + it("should return success with a dotted scope and store authenticated user properties", function() + + end) + end) + + describe("Implicit Grant", function() + it("should return success", function() + + end) + it("should return success and the state", function() + + end) + it("should return success and the token should have the right expiration", function() + + end) + it("should return success and store authenticated user properties", function() + + end) + it("should return set the right upstream headers", function() + + end) + end) + + describe("Client Credentials", function() + it("should return an error when client_secret is not sent", function() + + end) + it("should return an error when client_secret is not sent", function() + + end) + it("should fail when not under HTTPS", function() + + end) + it("should return fail when setting authenticated_userid and no provision_key", function() + + end) + it("should return fail when setting authenticated_userid and invalid provision_key", function() + + end) + it("should return success", function() + + end) + it("should return success with authenticated_userid and valid provision_key", function() + + end) + it("should return success with authorization header", function() + + end) + it("should return an error with a wrong authorization header", function() + + end) + it("should return set the right upstream headers", function() + + end) + it("should return set the right upstream headers", function() + + end) + end) + + describe("Password Grant", function() + it("should block unauthorized requests", function() + + end) + it("should return an error when client_secret is not sent", function() + + end) + it("should return an error when client_secret is not sent", function() + + end) + it("should fail when no provision key is being sent", function() + + end) + it("should fail when no provision key is being sent", function() + + end) + it("should fail when no authenticated user id is being sent", function() + + end) + it("should fail when no authenticated user id is being sent", function() + + end) + it("should fail when no authenticated user id is being sent", function() + + end) + it("should return an error with a wrong authorization header", function() + + end) + it("should return an error with a wrong authorization header", function() + + end) + end) + end) + + describe("OAuth2 Access Token", function() + it("should return an error when nothing is being sent", function() + + end) + it("should return an error when only the code is being sent", function() + + end) + it("should return an error when only the code and client_secret are being sent", function() + + end) + it("should return an error when only the code and client_secret and client_id are being sent", function() + + end) + it("should return an error when only the code and client_secret and client_id are being sent", function() + + end) + it("should return success without state", function() + + end) + it("should return success with state", function() + + end) + it("should return set the right upstream headers", function() + + end) + end) + + describe("Making a request", function() + it("should work when a correct access_token is being sent in the querystring", function() + + end) + it("should work when a correct access_token is being sent in a form body", function() + + end) + it("should work when a correct access_token is being sent in an authorization header (bearer)", function() + + end) + it("should work when a correct access_token is being sent in an authorization header (token)", function() + + end) + end) + + describe("Authentication challenge", function() + it("should return 401 Unauthorized without error if it lacks any authentication information", function() + + end) + it("should return 401 Unauthorized when an invalid access token is being sent via url parameter", function() + + end) + it("should return 401 Unauthorized when an invalid access token is being sent via the Authorization header", function() + + end) + it("should return 401 Unauthorized when token has expired", function() + + end) + end) + + describe("Refresh Token", function() + it("should not refresh an invalid access token", function() + + end) + it("should refresh an valid access token", function() + + end) + it("should expire after 5 seconds", function() + + end) + end) + + describe("Hide Credentials", function() + it("should not hide credentials in the body", function() + + end) + it("should hide credentials in the body", function() + + end) + it("should hide credentials in the body", function() + + end) + it("should hide credentials in the querystring", function() + + end) + it("should hide credentials in the querystring", function() + + end) + it("should hide credentials in the querystring", function() + + end) + end) +end) diff --git a/spec/03-plugins/oauth2/api_old.lua b/spec/03-plugins/oauth2/api_old.lua deleted file mode 100644 index 169642b1e25a..000000000000 --- a/spec/03-plugins/oauth2/api_old.lua +++ /dev/null @@ -1,263 +0,0 @@ -local json = require "cjson" -local http_client = require "kong.tools.http_client" -local spec_helper = require "spec.spec_helpers" -local utils = require "kong.tools.utils" - -describe("OAuth 2 Credentials API", function() - local BASE_URL, credential, consumer - - setup(function() - spec_helper.prepare_db() - spec_helper.start_kong() - end) - - teardown(function() - spec_helper.stop_kong() - end) - - describe("/consumers/:consumer/oauth2/", function() - setup(function() - local fixtures = spec_helper.insert_fixtures { - consumer = { - {username = "bob"} - } - } - consumer = fixtures.consumer[1] - BASE_URL = spec_helper.API_URL.."/consumers/bob/oauth2/" - end) - - describe("POST", function() - it("[SUCCESS] should create a oauth2 credential", function() - local response, status = http_client.post(BASE_URL, {name = "Test APP", redirect_uri = "http://google.com/"}) - assert.equal(201, status) - credential = json.decode(response) - assert.equal(consumer.id, credential.consumer_id) - end) - it("[SUCCESS] should create a oauth2 credential with several redirect_uris", function() - local response, status = http_client.post(BASE_URL, {name = "Test APP", redirect_uri = "http://google.com/,http://aaa.com"}) - assert.equal(201, status) - credential = json.decode(response) - assert.equal(consumer.id, credential.consumer_id) - assert.equal(2, utils.table_size(credential.redirect_uri)) - end) - it("[FAILURE] should return proper errors", function() - local response, status = http_client.post(BASE_URL, {}) - assert.equal(400, status) - assert.equal('{"redirect_uri":"redirect_uri is required","name":"name is required"}\n', response) - end) - it("[FAILURE] should return redirect_uris errors", function() - local response, status = http_client.post(BASE_URL, {name = "Test App", redirect_uri = "not-valid"}) - assert.equal(400, status) - local output = json.decode(response) - assert.equal(output.redirect_uri, "cannot parse 'not-valid'") - - local response2, status2 = http_client.post(BASE_URL, {name = "Test App", redirect_uri = "http://test.com/#with-fragment"}) - assert.equal(400, status2) - local output2 = json.decode(response2) - assert.equal(output2.redirect_uri, "fragment not allowed in 'http://test.com/#with-fragment'") - - -- same tests but with multiple redirect_uris - local response3, status3 = http_client.post(BASE_URL, {name = "Test App", redirect_uri = {"http://valid.com", "not-valid"}}) - assert.equal(400, status3) - local output3 = json.decode(response3) - assert.equal(output3.redirect_uri, "cannot parse 'not-valid'") - - local response4, status4 = http_client.post(BASE_URL, {name = "Test App", redirect_uri = {"http://valid.com", "http://test.com/#with-fragment"}}) - assert.equal(400, status4) - local output4 = json.decode(response4) - assert.equal(output4.redirect_uri, "fragment not allowed in 'http://test.com/#with-fragment'") - end) - end) - - describe("PUT", function() - setup(function() - local credentials = spec_helper.get_env().dao_factory.keyauth_credentials - credentials:delete({id = credential.id}) - end) - - it("[SUCCESS] should create and update", function() - local response, status = http_client.put(BASE_URL, {redirect_uri = "http://google.com/", name = "Test APP"}) - assert.equal(201, status) - credential = json.decode(response) - assert.equal(consumer.id, credential.consumer_id) - end) - it("[FAILURE] should return proper errors", function() - local response, status = http_client.put(BASE_URL, {}) - assert.equal(400, status) - assert.equal('{"redirect_uri":"redirect_uri is required","name":"name is required"}\n', response) - end) - end) - - describe("GET", function() - it("should retrieve all", function() - local response, status = http_client.get(BASE_URL) - assert.equal(200, status) - local body = json.decode(response) - assert.equal(3, #(body.data)) - end) - end) - end) - - describe("/consumers/:consumer/oauth2/:id", function() - describe("GET", function() - it("should retrieve by id", function() - local response, status = http_client.get(BASE_URL..credential.id) - assert.equal(200, status) - local body = json.decode(response) - assert.equals(credential.id, body.id) - end) - end) - - describe("PATCH", function() - it("[SUCCESS] should update a credential", function() - local response, status = http_client.patch(BASE_URL..credential.id, {redirect_uri = "http://getkong.org/"}) - assert.equal(200, status) - credential = json.decode(response) - assert.equal("http://getkong.org/", credential.redirect_uri[1]) - end) - it("[FAILURE] should return proper errors", function() - local response, status = http_client.patch(BASE_URL..credential.id, {redirect_uri = ""}) - assert.equal(400, status) - assert.equal('{"redirect_uri":"redirect_uri is not a array"}\n', response) - end) - end) - - describe("DELETE", function() - it("[FAILURE] should return proper errors", function() - local _, status = http_client.delete(BASE_URL.."blah") - assert.equal(400, status) - - _, status = http_client.delete(BASE_URL.."00000000-0000-0000-0000-000000000000") - assert.equal(404, status) - end) - it("[SUCCESS] should delete a credential", function() - local _, status = http_client.delete(BASE_URL..credential.id) - assert.equal(204, status) - end) - end) - end) - - describe("/oauth2_tokens/", function() - - -- Create credential - local response, status = http_client.post(BASE_URL, {name = "Test APP", redirect_uri = "http://google.com/"}) - assert.equal(201, status) - credential = json.decode(response) - - local token - - BASE_URL = spec_helper.API_URL.."/oauth2_tokens/" - - describe("POST", function() - it("[SUCCESS] should create a oauth2 token", function() - local response, status = http_client.post(BASE_URL, {credential_id = credential.id, expires_in = 10}) - assert.equal(201, status) - token = json.decode(response) - assert.equal(credential.id, token.credential_id) - assert.equal(10, token.expires_in) - assert.truthy(token.access_token) - assert.falsy(token.refresh_token) - assert.equal("bearer", token.token_type) - end) - it("[FAILURE] should return proper errors", function() - local response, status = http_client.post(BASE_URL, {}) - assert.equal(400, status) - assert.equal('{"credential_id":"credential_id is required","expires_in":"expires_in is required"}\n', response) - end) - end) - - describe("PUT", function() - it("[SUCCESS] should create a oauth2 token", function() - local response, status = http_client.put(BASE_URL, {credential_id = credential.id, expires_in = 10}) - assert.equal(201, status) - token = json.decode(response) - assert.equal(credential.id, token.credential_id) - assert.equal(10, token.expires_in) - assert.truthy(token.access_token) - assert.falsy(token.refresh_token) - assert.equal("bearer", token.token_type) - end) - it("[FAILURE] should return proper errors", function() - local response, status = http_client.put(BASE_URL, {}) - assert.equal(400, status) - assert.equal('{"credential_id":"credential_id is required","expires_in":"expires_in is required"}\n', response) - end) - end) - - describe("GET", function() - it("should retrieve by id", function() - local response, status = http_client.get(BASE_URL..token.id) - assert.equal(200, status) - local body = json.decode(response) - assert.equals(credential.id, body.credential_id) - end) - it("should retrieve all", function() - local response, status = http_client.get(BASE_URL) - assert.equal(200, status) - local body = json.decode(response) - assert.equals(2, body.total) - end) - end) - - describe("PATCH", function() - it("should update partial fields", function() - local response, status = http_client.patch(BASE_URL..token.id, { access_token = "helloworld" }) - assert.equal(200, status) - local body = json.decode(response) - assert.equals("helloworld", body.access_token) - assert.falsy(body.refresh_token) - - -- Check it has really been updated - response, status = http_client.get(BASE_URL..token.id) - assert.equal(200, status) - body = json.decode(response) - assert.equals("helloworld", body.access_token) - assert.falsy(body.refresh_token) - end) - - describe("PUT", function() - it("should update every field", function() - local response, status = http_client.get(BASE_URL..token.id) - assert.equal(200, status) - local body = json.decode(response) - body.refresh_token = nil - body.access_token = "helloworld" - - local response, status = http_client.put(BASE_URL..token.id, body) - assert.equal(200, status) - local body = json.decode(response) - assert.equals("helloworld", body.access_token) - assert.falsy(body.refresh_token) - - -- Check it has really been updated - response, status = http_client.get(BASE_URL..token.id) - assert.equal(200, status) - body = json.decode(response) - assert.equals("helloworld", body.access_token) - assert.falsy(body.refresh_token) - end) - end) - end) - - describe("PUT", function() - it("should update the entire object", function() - local response, status = http_client.get(BASE_URL..token.id) - assert.equal(200, status) - local body = json.decode(response) - body.access_token = "puthelloworld" - body.created_at = nil - - response, status = http_client.put(BASE_URL..token.id, body) - assert.equal(200, status) - body = json.decode(response) - assert.equals("puthelloworld", body.access_token) - - -- Check it has really been updated - response, status = http_client.get(BASE_URL..token.id) - assert.equal(200, status) - body = json.decode(response) - assert.equals("puthelloworld", body.access_token) - end) - end) - end) -end) diff --git a/spec/03-plugins/oauth2/api_spec.lua b/spec/03-plugins/oauth2/api_spec.lua new file mode 100644 index 000000000000..8ff9fcb10741 --- /dev/null +++ b/spec/03-plugins/oauth2/api_spec.lua @@ -0,0 +1,514 @@ +local helpers = require "spec.helpers" +local cjson = require "cjson" + +describe("OAuth 2.0 API", function() + local consumer, admin_client + setup(function() + helpers.dao:truncate_tables() + helpers.execute "pkill nginx; pkill serf; pkill dnsmasq" + assert(helpers.prepare_prefix()) + assert(helpers.start_kong()) + admin_client = assert(helpers.http_client("127.0.0.1", helpers.test_conf.admin_port)) + end) + teardown(function() + if admin_client then + admin_client:close() + end + helpers.stop_kong() + end) + + describe("/consumers/:consumer/oauth2/", function() + setup(function() + consumer = assert(helpers.dao.consumers:insert { + username = "bob" + }) + end) + after_each(function() + helpers.dao:truncate_table("oauth2_credentials") + end) + + describe("POST", function() + it("creates a oauth2 credential", function() + local res = assert(admin_client:send { + method = "POST", + path = "/consumers/bob/oauth2", + body = { + name = "Test APP", + redirect_uri = "http://google.com/" + }, + headers = { + ["Content-Type"] = "application/json" + } + }) + local body = cjson.decode(assert.res_status(201, res)) + assert.equal(consumer.id, body.consumer_id) + assert.equal("Test APP", body.name) + assert.equal("http://google.com/", body.redirect_uri[1]) + end) + it("creates a oauth2 credential with multiple redirect_uri", function() + local res = assert(admin_client:send { + method = "POST", + path = "/consumers/bob/oauth2", + body = { + name = "Test APP", + redirect_uri = "http://google.com/, http://google.org/" + }, + headers = { + ["Content-Type"] = "application/json" + } + }) + local body = cjson.decode(assert.res_status(201, res)) + assert.equal(consumer.id, body.consumer_id) + assert.equal("Test APP", body.name) + assert.equal(2, #body.redirect_uri) + end) + describe("errors", function() + it("returns bad request", function() + local res = assert(admin_client:send { + method = "POST", + path = "/consumers/bob/oauth2", + body = {}, + headers = { + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"redirect_uri":"redirect_uri is required","name":"name is required"}]], body) + end) + it("returns bad request with invalid redirect_uri", function() + local res = assert(admin_client:send { + method = "POST", + path = "/consumers/bob/oauth2", + body = { + name = "Test APP", + redirect_uri = "not-valid" + }, + headers = { + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"redirect_uri":"cannot parse 'not-valid'"}]], body) + + local res = assert(admin_client:send { + method = "POST", + path = "/consumers/bob/oauth2", + body = { + name = "Test APP", + redirect_uri = "http://test.com/#with-fragment" + }, + headers = { + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"redirect_uri":"fragment not allowed in 'http:\/\/test.com\/#with-fragment'"}]], body) + + local res = assert(admin_client:send { + method = "POST", + path = "/consumers/bob/oauth2", + body = { + name = "Test APP", + redirect_uri = {"http://valid.com", "not-valid"} + }, + headers = { + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"redirect_uri":"cannot parse 'not-valid'"}]], body) + + local res = assert(admin_client:send { + method = "POST", + path = "/consumers/bob/oauth2", + body = { + name = "Test APP", + redirect_uri = {"http://valid.com", "http://test.com/#with-fragment"} + }, + headers = { + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"redirect_uri":"fragment not allowed in 'http:\/\/test.com\/#with-fragment'"}]], body) + end) + end) + end) + + describe("PUT", function() + it("creates an oauth2 credential", function() + local res = assert(admin_client:send { + method = "PUT", + path = "/consumers/bob/oauth2", + body = { + name = "Test APP", + redirect_uri = "http://google.com/" + }, + headers = { + ["Content-Type"] = "application/json" + } + }) + local body = cjson.decode(assert.res_status(201, res)) + assert.equal(consumer.id, body.consumer_id) + assert.equal("Test APP", body.name) + assert.equal("http://google.com/", body.redirect_uri[1]) + end) + describe("errors", function() + it("returns bad request", function() + local res = assert(admin_client:send { + method = "PUT", + path = "/consumers/bob/oauth2", + body = {}, + headers = { + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"redirect_uri":"redirect_uri is required","name":"name is required"}]], body) + end) + end) + end) + + describe("GET", function() + setup(function() + for i = 1, 3 do + assert(helpers.dao.oauth2_credentials:insert { + name = "app"..i, + redirect_uri = "https://mockbin.org", + consumer_id = consumer.id + }) + end + end) + teardown(function() + helpers.dao:truncate_table("oauth2_credentials") + end) + it("retrieves the first page", function() + local res = assert(admin_client:send { + method = "GET", + path = "/consumers/bob/oauth2" + }) + local body = assert.res_status(200, res) + local json = cjson.decode(body) + assert.is_table(json.data) + assert.equal(3, #json.data) + assert.equal(3, json.total) + end) + end) + end) + + describe("/consumers/:consumer/oauth2/:id", function() + local credential + before_each(function() + helpers.dao:truncate_table("oauth2_credentials") + credential = assert(helpers.dao.oauth2_credentials:insert { + name = "test app", + redirect_uri = "https://mockbin.org", + consumer_id = consumer.id + }) + end) + describe("GET", function() + it("retrieves oauth2 credential by id", function() + local res = assert(admin_client:send { + method = "GET", + path = "/consumers/bob/oauth2/"..credential.id + }) + local body = assert.res_status(200, res) + local json = cjson.decode(body) + assert.equal(credential.id, json.id) + end) + it("retrieves credential by id only if the credential belongs to the specified consumer", function() + assert(helpers.dao.consumers:insert { + username = "alice" + }) + + local res = assert(admin_client:send { + method = "GET", + path = "/consumers/bob/oauth2/"..credential.id + }) + assert.res_status(200, res) + + res = assert(admin_client:send { + method = "GET", + path = "/consumers/alice/oauth2/"..credential.id + }) + assert.res_status(404, res) + end) + end) + + describe("PATCH", function() + it("updates a credential", function() + local previous_name = credential.name + + local res = assert(admin_client:send { + method = "PATCH", + path = "/consumers/bob/oauth2/"..credential.id, + body = { + name = "4321" + }, + headers = { + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(200, res) + local json = cjson.decode(body) + assert.not_equal(previous_name, json.name) + end) + describe("errors", function() + it("handles invalid input", function() + local res = assert(admin_client:send { + method = "PATCH", + path = "/consumers/bob/oauth2/"..credential.id, + body = { + redirect_uri = "not-valid" + }, + headers = { + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"redirect_uri":"cannot parse 'not-valid'"}]], body) + end) + end) + end) + + describe("DELETE", function() + it("deletes a credential", function() + local res = assert(admin_client:send { + method = "DELETE", + path = "/consumers/bob/oauth2/"..credential.id, + }) + assert.res_status(204, res) + end) + describe("errors", function() + it("returns 400 on invalid input", function() + local res = assert(admin_client:send { + method = "DELETE", + path = "/consumers/bob/oauth2/blah" + }) + assert.res_status(400, res) + end) + it("returns 404 if not found", function() + local res = assert(admin_client:send { + method = "DELETE", + path = "/consumers/bob/oauth2/00000000-0000-0000-0000-000000000000" + }) + assert.res_status(404, res) + end) + end) + end) + end) + + describe("/oauth2_tokens/", function() + local oauth2_credential + setup(function() + oauth2_credential = assert(helpers.dao.oauth2_credentials:insert { + name = "Test APP", + redirect_uri = "https://mockin.com", + consumer_id = consumer.id + }) + end) + after_each(function() + helpers.dao:truncate_table("oauth2_tokens") + end) + + describe("POST", function() + it("creates a oauth2 token", function() + local res = assert(admin_client:send { + method = "POST", + path = "/oauth2_tokens", + body = { + credential_id = oauth2_credential.id, + expires_in = 10 + }, + headers = { + ["Content-Type"] = "application/json" + } + }) + local body = cjson.decode(assert.res_status(201, res)) + assert.equal(oauth2_credential.id, body.credential_id) + assert.equal(10, body.expires_in) + assert.truthy(body.access_token) + assert.falsy(body.refresh_token) + assert.equal("bearer", body.token_type) + end) + describe("errors", function() + it("returns bad request", function() + local res = assert(admin_client:send { + method = "POST", + path = "/oauth2_tokens", + body = {}, + headers = { + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"credential_id":"credential_id is required","expires_in":"expires_in is required"}]], body) + end) + end) + end) + + describe("PUT", function() + it("creates an oauth2 credential", function() + local res = assert(admin_client:send { + method = "PUT", + path = "/oauth2_tokens", + body = { + credential_id = oauth2_credential.id, + expires_in = 10 + }, + headers = { + ["Content-Type"] = "application/json" + } + }) + local body = cjson.decode(assert.res_status(201, res)) + assert.equal(oauth2_credential.id, body.credential_id) + assert.equal(10, body.expires_in) + assert.truthy(body.access_token) + assert.falsy(body.refresh_token) + assert.equal("bearer", body.token_type) + end) + describe("errors", function() + it("returns bad request", function() + local res = assert(admin_client:send { + method = "PUT", + path = "/oauth2_tokens", + body = {}, + headers = { + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"credential_id":"credential_id is required","expires_in":"expires_in is required"}]], body) + end) + end) + end) + + describe("GET", function() + setup(function() + for i = 1, 3 do + assert(helpers.dao.oauth2_tokens:insert { + credential_id = oauth2_credential.id, + expires_in = 10 + }) + end + end) + teardown(function() + helpers.dao:truncate_table("oauth2_tokens") + end) + it("retrieves the first page", function() + local res = assert(admin_client:send { + method = "GET", + path = "/oauth2_tokens" + }) + local body = assert.res_status(200, res) + local json = cjson.decode(body) + assert.is_table(json.data) + assert.equal(3, #json.data) + assert.equal(3, json.total) + end) + end) + + describe("/oauth2_tokens/:id", function() + local token + before_each(function() + helpers.dao:truncate_table("oauth2_tokens") + token = assert(helpers.dao.oauth2_tokens:insert { + credential_id = oauth2_credential.id, + expires_in = 10 + }) + end) + + describe("GET", function() + it("retrieves oauth2 token by id", function() + local res = assert(admin_client:send { + method = "GET", + path = "/oauth2_tokens/"..token.id + }) + local body = assert.res_status(200, res) + local json = cjson.decode(body) + assert.equal(token.id, json.id) + end) + end) + + describe("PUT", function() + it("should update every field", function() + token.access_token = "helloworld" + token.refresh_token = nil + local res = assert(admin_client:send { + method = "PUT", + path = "/oauth2_tokens/"..token.id, + body = token, + headers = { + ["Content-Type"] = "application/json" + } + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.is_nil(body.refresh_token) + assert.equal("helloworld", body.access_token) + end) + end) + + describe("PATCH", function() + it("updates a token", function() + local previous_expires_in = token.expires_in + + local res = assert(admin_client:send { + method = "PATCH", + path = "/oauth2_tokens/"..token.id, + body = { + expires_in = 20 + }, + headers = { + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(200, res) + local json = cjson.decode(body) + assert.not_equal(previous_expires_in, json.expires_in) + end) + describe("errors", function() + it("handles invalid input", function() + local res = assert(admin_client:send { + method = "PATCH", + path = "/oauth2_tokens/"..token.id, + body = { + expires_in = "hello" + }, + headers = { + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"expires_in":"expires_in is not a number"}]], body) + end) + end) + end) + + describe("DELETE", function() + it("deletes a token", function() + local res = assert(admin_client:send { + method = "DELETE", + path = "/oauth2_tokens/"..token.id, + }) + assert.res_status(204, res) + end) + describe("errors", function() + it("returns 400 on invalid input", function() + local res = assert(admin_client:send { + method = "DELETE", + path = "/oauth2_tokens/blah" + }) + assert.res_status(400, res) + end) + it("returns 404 if not found", function() + local res = assert(admin_client:send { + method = "DELETE", + path = "/oauth2_tokens/00000000-0000-0000-0000-000000000000" + }) + assert.res_status(404, res) + end) + end) + end) + end) + end) +end) diff --git a/spec/03-plugins/oauth2/hooks_old.lua b/spec/03-plugins/oauth2/hooks_old.lua deleted file mode 100644 index 5d8d048b31d9..000000000000 --- a/spec/03-plugins/oauth2/hooks_old.lua +++ /dev/null @@ -1,302 +0,0 @@ -local json = require "cjson" -local http_client = require "kong.tools.http_client" -local spec_helper = require "spec.spec_helpers" -local cache = require "kong.tools.database_cache" -local rex = require "rex_pcre" - -local STUB_GET_URL = spec_helper.STUB_GET_URL -local PROXY_SSL_URL = spec_helper.PROXY_SSL_URL -local API_URL = spec_helper.API_URL - -local env = spec_helper.get_env() -- test environment -local dao_factory = env.dao_factory -local configuration = env.configuration -configuration.cassandra = configuration[configuration.database].properties - -describe("OAuth2 Authentication Hooks", function() - - setup(function() - spec_helper.prepare_db() - end) - - teardown(function() - spec_helper.stop_kong() - end) - - before_each(function() - spec_helper.restart_kong() - - spec_helper.drop_db() - spec_helper.insert_fixtures { - api = { - { request_host = "oauth2.com", upstream_url = "http://mockbin.com" } - }, - consumer = { - { username = "auth_tests_consumer" } - }, - plugin = { - { name = "oauth2", config = { scopes = { "email", "profile" }, mandatory_scope = true, provision_key = "provision123", token_expiration = 5, enable_implicit_grant = true }, __api = 1 } - }, - oauth2_credential = { - { client_id = "clientid123", client_secret = "secret123", redirect_uri = "http://google.com/kong", name="testapp", __consumer = 1 } - } - } - end) - - local function provision_code(client_id) - local response = http_client.post(PROXY_SSL_URL.."/oauth2/authorize", { provision_key = "provision123", client_id = client_id, scope = "email", response_type = "code", state = "hello", authenticated_userid = "userid123" }, {host = "oauth2.com"}) - local body = json.decode(response) - if body.redirect_uri then - local matches = rex.gmatch(body.redirect_uri, "^http://google\\.com/kong\\?code=([\\w]{32,32})&state=hello$") - local code - for line in matches do - code = line - end - local data = dao_factory.oauth2_authorization_codes:find_all({code = code}) - return data[1].code - end - end - - describe("OAuth2 Credentials entity invalidation", function() - it("should invalidate when OAuth2 Credential entity is deleted", function() - -- It should work - local code = provision_code("clientid123") - local _, status = http_client.post(PROXY_SSL_URL.."/oauth2/token", { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code" }, {host = "oauth2.com"}) - assert.are.equal(200, status) - - -- Check that cache is populated - local cache_key = cache.oauth2_credential_key("clientid123") - local _, status = http_client.get(API_URL.."/cache/"..cache_key) - assert.equals(200, status) - - -- Retrieve credential ID - local response, status = http_client.get(API_URL.."/consumers/auth_tests_consumer/oauth2/") - assert.equals(200, status) - local credential_id = json.decode(response).data[1].id - assert.truthy(credential_id) - - -- Delete OAuth2 credential (which triggers invalidation) - local _, status = http_client.delete(API_URL.."/consumers/auth_tests_consumer/oauth2/"..credential_id) - assert.equals(204, status) - - -- Wait for cache to be invalidated - local exists = true - while(exists) do - local _, status = http_client.get(API_URL.."/cache/"..cache_key) - if status ~= 200 then - exists = false - end - end - - -- It should not work - local code = provision_code("clientid123") - local _, status = http_client.post(PROXY_SSL_URL.."/oauth2/token", { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code" }, {host = "oauth2.com"}) - assert.are.equal(400, status) - end) - it("should invalidate when OAuth2 Credential entity is updated", function() - -- It should work - local code = provision_code("clientid123") - local _, status = http_client.post(PROXY_SSL_URL.."/oauth2/token", { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code" }, {host = "oauth2.com"}) - assert.are.equal(200, status) - - -- It should not work - local code = provision_code("updclientid123") - local _, status = http_client.post(PROXY_SSL_URL.."/oauth2/token", { code = code, client_id = "updclientid123", client_secret = "secret123", grant_type = "authorization_code" }, {host = "oauth2.com"}) - assert.are.equal(400, status) - - -- Check that cache is populated - local cache_key = cache.oauth2_credential_key("clientid123") - local _, status = http_client.get(API_URL.."/cache/"..cache_key) - assert.equals(200, status) - - -- Retrieve credential ID - local response, status = http_client.get(API_URL.."/consumers/auth_tests_consumer/oauth2/") - assert.equals(200, status) - local credential_id = json.decode(response).data[1].id - assert.truthy(credential_id) - - -- Update OAuth2 credential (which triggers invalidation) - local _, status = http_client.patch(API_URL.."/consumers/auth_tests_consumer/oauth2/"..credential_id, {client_id="updclientid123"}) - assert.equals(200, status) - - -- Wait for cache to be invalidated - local exists = true - while(exists) do - local _, status = http_client.get(API_URL.."/cache/"..cache_key) - if status ~= 200 then - exists = false - end - end - - -- It should work - local code = provision_code("updclientid123") - local _, status = http_client.post(PROXY_SSL_URL.."/oauth2/token", { code = code, client_id = "updclientid123", client_secret = "secret123", grant_type = "authorization_code" }, {host = "oauth2.com"}) - assert.are.equal(200, status) - - -- It should not work - local code = provision_code("clientid123") - local _, status = http_client.post(PROXY_SSL_URL.."/oauth2/token", { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code" }, {host = "oauth2.com"}) - assert.are.equal(400, status) - end) - end) - - describe("Consumer entity invalidation", function() - it("should invalidate when Consumer entity is deleted", function() - -- It should work - local code = provision_code("clientid123") - local _, status = http_client.post(PROXY_SSL_URL.."/oauth2/token", { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code" }, {host = "oauth2.com"}) - assert.are.equal(200, status) - - -- Check that cache is populated - local cache_key = cache.oauth2_credential_key("clientid123") - local _, status = http_client.get(API_URL.."/cache/"..cache_key) - assert.equals(200, status) - - -- Delete Consumer (which triggers invalidation) - local _, status = http_client.delete(API_URL.."/consumers/auth_tests_consumer") - assert.equals(204, status) - - -- Wait for cache to be invalidated - local exists = true - while(exists) do - local _, status = http_client.get(API_URL.."/cache/"..cache_key) - if status ~= 200 then - exists = false - end - end - - -- It should not work - local code = provision_code("clientid123") - local _, status = http_client.post(PROXY_SSL_URL.."/oauth2/token", { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code" }, {host = "oauth2.com"}) - assert.are.equal(400, status) - end) - end) - - describe("OAuth2 access token entity invalidation", function() - it("should invalidate when OAuth2 token entity is deleted", function() - -- It should work - local code = provision_code("clientid123") - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/token", { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code" }, {host = "oauth2.com"}) - assert.are.equal(200, status) - local token = json.decode(response) - assert.truthy(token) - - local _, status = http_client.post(STUB_GET_URL, { access_token = token.access_token }, {host = "oauth2.com"}) - assert.are.equal(200, status) - - -- Check that cache is populated - local cache_key = cache.oauth2_token_key(token.access_token) - local _, status = http_client.get(API_URL.."/cache/"..cache_key) - assert.equals(200, status) - - -- Delete token (which triggers invalidation) - local res = dao_factory.oauth2_tokens:find_all({access_token=token.access_token}) - local token_id = res[1].id - assert.truthy(token_id) - - local _, status = http_client.delete(API_URL.."/oauth2_tokens/"..token_id) - assert.equals(204, status) - - -- Wait for cache to be invalidated - local exists = true - while(exists) do - local _, status = http_client.get(API_URL.."/cache/"..cache_key) - if status ~= 200 then - exists = false - end - end - - -- It should not work - local _, status = http_client.post(STUB_GET_URL, { access_token = token.access_token }, {host = "oauth2.com"}) - assert.are.equal(401, status) - end) - it("should invalidate when Oauth2 token entity is updated", function() - -- It should work - local code = provision_code("clientid123") - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/token", { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code" }, {host = "oauth2.com"}) - assert.are.equal(200, status) - local token = json.decode(response) - assert.truthy(token) - - local _, status = http_client.post(STUB_GET_URL, { access_token = token.access_token }, {host = "oauth2.com"}) - assert.are.equal(200, status) - - -- It should not work - local _, status = http_client.post(STUB_GET_URL, { access_token = "hello_token" }, {host = "oauth2.com"}) - assert.are.equal(401, status) - - -- Check that cache is populated - local cache_key = cache.oauth2_token_key(token.access_token) - local _, status = http_client.get(API_URL.."/cache/"..cache_key) - assert.equals(200, status) - - -- Update OAuth 2 token (which triggers invalidation) - local res = dao_factory.oauth2_tokens:find_all({access_token=token.access_token}) - local token_id = res[1].id - assert.truthy(token_id) - - local _, status = http_client.patch(API_URL.."/oauth2_tokens/"..token_id, {access_token="hello_token"}) - assert.equals(200, status) - - -- Wait for cache to be invalidated - local exists = true - while(exists) do - local _, status = http_client.get(API_URL.."/cache/"..cache_key) - if status ~= 200 then - exists = false - end - end - - -- It should work - local _, status = http_client.post(STUB_GET_URL, { access_token = "hello_token" }, {host = "oauth2.com"}) - assert.are.equal(200, status) - - -- It should not work - local _, status = http_client.post(STUB_GET_URL, { access_token = token.access_token }, {host = "oauth2.com"}) - assert.are.equal(401, status) - end) - end) - - describe("OAuth2 client entity invalidation", function() - it("should invalidate token when OAuth2 client entity is deleted", function() - -- It should work - local code = provision_code("clientid123") - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/token", { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code" }, {host = "oauth2.com"}) - assert.are.equal(200, status) - local token = json.decode(response) - assert.truthy(token) - - local _, status = http_client.post(STUB_GET_URL, { access_token = token.access_token }, {host = "oauth2.com"}) - assert.are.equal(200, status) - - -- Check that cache is populated - local cache_key = cache.oauth2_token_key(token.access_token) - local _, status = http_client.get(API_URL.."/cache/"..cache_key) - assert.equals(200, status) - - -- Retrieve credential ID - local response, status = http_client.get(API_URL.."/consumers/auth_tests_consumer/oauth2/", {client_id="clientid123"}) - assert.equals(200, status) - local credential_id = json.decode(response).data[1].id - assert.truthy(credential_id) - - -- Delete OAuth2 client (which triggers invalidation) - local _, status = http_client.delete(API_URL.."/consumers/auth_tests_consumer/oauth2/"..credential_id) - assert.equals(204, status) - - -- Wait for cache to be invalidated - local exists = true - while(exists) do - local _, status = http_client.get(API_URL.."/cache/"..cache_key) - if status ~= 200 then - exists = false - end - end - - -- It should not work - local _, status = http_client.post(STUB_GET_URL, { access_token = token.access_token }, {host = "oauth2.com"}) - assert.are.equal(401, status) - end) - end) - -end) diff --git a/spec/03-plugins/oauth2/hooks_spec.lua b/spec/03-plugins/oauth2/hooks_spec.lua new file mode 100644 index 000000000000..ef037181a2d4 --- /dev/null +++ b/spec/03-plugins/oauth2/hooks_spec.lua @@ -0,0 +1,512 @@ +local helpers = require "spec.helpers" +local cache = require "kong.tools.database_cache" +local cjson = require "cjson" +local pl_stringx = require "pl.stringx" + +describe("Plugin hooks: oauth2", function() + local admin_client, proxy_client + setup(function() + assert(helpers.prepare_prefix()) + assert(helpers.start_kong()) + proxy_client = assert(helpers.http_client("127.0.0.1", pl_stringx.split(helpers.test_conf.proxy_listen_ssl, ":")[2])) + proxy_client:ssl_handshake() + admin_client = assert(helpers.http_client("127.0.0.1", helpers.test_conf.admin_port)) + end) + teardown(function() + if admin_client and proxy_client then + admin_client:close() + proxy_client:close() + end + helpers.stop_kong() + end) + + before_each(function() + helpers.dao:truncate_tables() + local api = assert(helpers.dao.apis:insert { + request_host = "oauth2.com", + upstream_url = "http://mockbin.com" + }) + assert(helpers.dao.plugins:insert { + name = "oauth2", + api_id = api.id, + config = { + scopes = { "email", "profile" }, + mandatory_scope = true, + provision_key = "provision123", + token_expiration = 5, + enable_implicit_grant = true + } + }) + + local consumer = assert(helpers.dao.consumers:insert { + username = "bob" + }) + assert(helpers.dao.oauth2_credentials:insert { + client_id = "clientid123", + client_secret = "secret123", + redirect_uri = "http://google.com/kong", + name = "testapp", + consumer_id = consumer.id + }) + + -- Invalidate cache + local res = assert(admin_client:send { + method = "DELETE", + path = "/cache/", + headers = {} + }) + assert.res_status(204, res) + end) + + local function provision_code(client_id) + local res = assert(proxy_client:send { + method = "POST", + path = "/oauth2/authorize", + body = { + provision_key = "provision123", + client_id = client_id, + scope = "email", + response_type = "code", + state = "hello", + authenticated_userid = "userid123" + }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + local body = cjson.decode(res:read_body()) + if body.redirect_uri then + local iterator, err = ngx.re.gmatch(body.redirect_uri, "^http://google\\.com/kong\\?code=([\\w]{32,32})&state=hello$") + assert.is_nil(err) + local m, err = iterator() + assert.is_nil(err) + return m[1] + end + end + + describe("OAuth2 Credentials entity invalidation", function() + it("should invalidate when OAuth2 Credential entity is deleted", function() + -- It should properly work + local code = provision_code("clientid123") + local res = assert(proxy_client:send { + method = "POST", + path = "/oauth2/token", + body = { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code" }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + assert.res_status(200, res) + + -- Check that cache is populated + local cache_key = cache.oauth2_credential_key("clientid123") + local res = assert(admin_client:send { + method = "GET", + path = "/cache/"..cache_key, + headers = {} + }) + local credential = cjson.decode(assert.res_status(200, res)) + + -- Delete OAuth2 credential (which triggers invalidation) + local res = assert(admin_client:send { + method = "DELETE", + path = "/consumers/bob/oauth2/"..credential.id, + headers = {} + }) + assert.res_status(204, res) + + -- ensure cache is invalidated + helpers.wait_until(function() + local res = assert(admin_client:send { + method = "GET", + path = "/cache/"..cache_key + }) + res:read_body() + return res.status == 404 + end) + + -- It should not work + local code = provision_code("clientid123") + local res = assert(proxy_client:send { + method = "POST", + path = "/oauth2/token", + body = { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code" }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + assert.res_status(400, res) + end) + + it("should invalidate when OAuth2 Credential entity is updated", function() + -- It should properly work + local code = provision_code("clientid123") + local res = assert(proxy_client:send { + method = "POST", + path = "/oauth2/token", + body = { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code" }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + assert.res_status(200, res) + + -- It should not work + local code = provision_code("updated_clientid123") + local res = assert(proxy_client:send { + method = "POST", + path = "/oauth2/token", + body = { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code" }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + assert.res_status(400, res) + + -- Check that cache is populated + local cache_key = cache.oauth2_credential_key("clientid123") + local res = assert(admin_client:send { + method = "GET", + path = "/cache/"..cache_key, + headers = {} + }) + local credential = cjson.decode(assert.res_status(200, res)) + + -- Update OAuth2 credential (which triggers invalidation) + local res = assert(admin_client:send { + method = "PATCH", + path = "/consumers/bob/oauth2/"..credential.id, + body = { + client_id = "updated_clientid123" + }, + headers = { + ["Content-Type"] = "application/json" + } + }) + assert.res_status(200, res) + + -- ensure cache is invalidated + helpers.wait_until(function() + local res = assert(admin_client:send { + method = "GET", + path = "/cache/"..cache_key + }) + res:read_body() + return res.status == 404 + end) + + -- It should work + local code = provision_code("updated_clientid123") + local res = assert(proxy_client:send { + method = "POST", + path = "/oauth2/token", + body = { code = code, client_id = "updated_clientid123", client_secret = "secret123", grant_type = "authorization_code" }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + assert.res_status(200, res) + + -- It should not work + local code = provision_code("clientid123") + local res = assert(proxy_client:send { + method = "POST", + path = "/oauth2/token", + body = { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code" }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + assert.res_status(400, res) + end) + end) + + describe("Consumer entity invalidation", function() + it("should invalidate when Consumer entity is deleted", function() + -- It should properly work + local code = provision_code("clientid123") + local res = assert(proxy_client:send { + method = "POST", + path = "/oauth2/token", + body = { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code" }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + assert.res_status(200, res) + + -- Check that cache is populated + local cache_key = cache.oauth2_credential_key("clientid123") + local res = assert(admin_client:send { + method = "GET", + path = "/cache/"..cache_key, + headers = {} + }) + assert.res_status(200, res) + + -- Delete Consumer (which triggers invalidation) + local res = assert(admin_client:send { + method = "DELETE", + path = "/consumers/bob", + headers = {} + }) + assert.res_status(204, res) + + -- ensure cache is invalidated + helpers.wait_until(function() + local res = assert(admin_client:send { + method = "GET", + path = "/cache/"..cache_key + }) + res:read_body() + return res.status == 404 + end) + + -- It should not work + local code = provision_code("clientid123") + local res = assert(proxy_client:send { + method = "POST", + path = "/oauth2/token", + body = { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code" }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + assert.res_status(400, res) + end) + end) + + describe("#only OAuth2 access token entity invalidation", function() + it("should invalidate when OAuth2 token entity is deleted", function() + -- It should properly work + local code = provision_code("clientid123") + local res = assert(proxy_client:send { + method = "POST", + path = "/oauth2/token", + body = { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code" }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + local token = cjson.decode(assert.res_status(200, res)) + assert.is_table(token) + + -- The token should work + local res = assert(proxy_client:send { + method = "GET", + path = "/status/200?access_token="..token.access_token, + headers = { + ["Host"] = "oauth2.com" + } + }) + assert.res_status(200, res) + + -- Check that cache is populated + local cache_key = cache.oauth2_token_key(token.access_token) + local res = assert(admin_client:send { + method = "GET", + path = "/cache/"..cache_key, + headers = {} + }) + assert.res_status(200, res) + + local res = helpers.dao.oauth2_tokens:find_all({access_token=token.access_token}) + local token_id = res[1].id + assert.is_string(token_id) + + -- Delete token (which triggers invalidation) + local res = assert(admin_client:send { + method = "DELETE", + path = "/oauth2_tokens/"..token_id, + headers = {} + }) + assert.res_status(204, res) + + -- ensure cache is invalidated + helpers.wait_until(function() + local res = assert(admin_client:send { + method = "GET", + path = "/cache/"..cache_key + }) + res:read_body() + return res.status == 404 + end) + + -- It should not work + local res = assert(proxy_client:send { + method = "GET", + path = "/status/200?access_token="..token.access_token, + headers = { + ["Host"] = "oauth2.com" + } + }) + assert.res_status(401, res) + end) + + it("should invalidate when Oauth2 token entity is updated", function() + -- It should properly work + local code = provision_code("clientid123") + local res = assert(proxy_client:send { + method = "POST", + path = "/oauth2/token", + body = { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code" }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + local token = cjson.decode(assert.res_status(200, res)) + assert.is_table(token) + + -- The token should work + local res = assert(proxy_client:send { + method = "GET", + path = "/status/200?access_token="..token.access_token, + headers = { + ["Host"] = "oauth2.com" + } + }) + assert.res_status(200, res) + + -- Check that cache is populated + local cache_key = cache.oauth2_token_key(token.access_token) + local res = assert(admin_client:send { + method = "GET", + path = "/cache/"..cache_key, + headers = {} + }) + assert.res_status(200, res) + + local res = helpers.dao.oauth2_tokens:find_all({access_token=token.access_token}) + local token_id = res[1].id + assert.is_string(token_id) + + -- Update OAuth 2 token (which triggers invalidation) + local res = assert(admin_client:send { + method = "PATCH", + path = "/oauth2_tokens/"..token_id, + body = { + access_token = "updated_token" + }, + headers = { + ["Content-Type"] = "application/json" + } + }) + assert.res_status(200, res) + + -- ensure cache is invalidated + helpers.wait_until(function() + local res = assert(admin_client:send { + method = "GET", + path = "/cache/"..cache_key + }) + res:read_body() + return res.status == 404 + end) + + -- It should work + local res = assert(proxy_client:send { + method = "GET", + path = "/status/200?access_token=updated_token", + headers = { + ["Host"] = "oauth2.com" + } + }) + assert.res_status(200, res) + + -- It should not work + local res = assert(proxy_client:send { + method = "GET", + path = "/status/200?access_token="..token.access_token, + headers = { + ["Host"] = "oauth2.com" + } + }) + assert.res_status(401, res) + end) + end) + + describe("OAuth2 client entity invalidation", function() + it("should invalidate token when OAuth2 client entity is deleted", function() + -- It should properly work + local code = provision_code("clientid123") + local res = assert(proxy_client:send { + method = "POST", + path = "/oauth2/token", + body = { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code" }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + local token = cjson.decode(assert.res_status(200, res)) + assert.is_table(token) + + -- The token should work + local res = assert(proxy_client:send { + method = "GET", + path = "/status/200?access_token="..token.access_token, + headers = { + ["Host"] = "oauth2.com" + } + }) + assert.res_status(200, res) + + -- Check that cache is populated + local cache_key = cache.oauth2_token_key(token.access_token) + local res = assert(admin_client:send { + method = "GET", + path = "/cache/"..cache_key, + headers = {} + }) + assert.res_status(200, res) + + -- Retrieve credential ID + local res = assert(admin_client:send { + method = "GET", + path = "/cache/"..cache.oauth2_credential_key("clientid123"), + headers = {} + }) + local credential = cjson.decode(assert.res_status(200, res)) + + -- Delete OAuth2 client (which triggers invalidation) + local res = assert(admin_client:send { + method = "DELETE", + path = "/consumers/bob/oauth2/"..credential.id, + headers = {} + }) + assert.res_status(204, res) + + -- ensure cache is invalidated + helpers.wait_until(function() + local res = assert(admin_client:send { + method = "GET", + path = "/cache/"..cache_key + }) + res:read_body() + return res.status == 404 + end) + + -- it should not work + local res = assert(proxy_client:send { + method = "GET", + path = "/status/200?access_token="..token.access_token, + headers = { + ["Host"] = "oauth2.com" + } + }) + assert.res_status(401, res) + end) + end) + +end) diff --git a/spec/03-plugins/oauth2/schema_old.lua b/spec/03-plugins/oauth2/schema_spec.lua similarity index 100% rename from spec/03-plugins/oauth2/schema_old.lua rename to spec/03-plugins/oauth2/schema_spec.lua From 4ceb221a9adb8473e8adada8d1c8e293e25128b0 Mon Sep 17 00:00:00 2001 From: thefosk Date: Thu, 23 Jun 2016 17:00:51 -0700 Subject: [PATCH 28/29] oauth2 --- kong/cmd/utils/serf_signals.lua | 86 +- kong/serf.lua | 24 +- .../01-cmd/02-start_stop_spec.lua | 2 +- spec/03-plugins/oauth2/access_spec.lua | 1440 +++++++++++++++-- spec/03-plugins/rate-limiting/access_spec.lua | 2 +- spec/helpers.lua | 36 +- 6 files changed, 1407 insertions(+), 183 deletions(-) diff --git a/kong/cmd/utils/serf_signals.lua b/kong/cmd/utils/serf_signals.lua index 37aa501cec15..c6bb194ad32d 100644 --- a/kong/cmd/utils/serf_signals.lua +++ b/kong/cmd/utils/serf_signals.lua @@ -8,16 +8,20 @@ local pl_stringx = require "pl.stringx" local pl_utils = require "pl.utils" local pl_path = require "pl.path" local pl_file = require "pl.file" +local pl_dir = require "pl.dir" local kill = require "kong.cmd.utils.kill" local log = require "kong.cmd.utils.log" +local utils = require "kong.tools.utils" +local fmt = string.format local serf_bin_name = "serf" local serf_pid_name = "serf.pid" +local serf_node_id = "serf.id" local serf_event_name = "kong" local start_timeout = 5 local function check_serf_bin() - local cmd = string.format("%s -v", serf_bin_name) + local cmd = fmt("%s -v", serf_bin_name) local ok, _, stdout = pl_utils.executeex(cmd) if ok and stdout then if not stdout:match "^Serf v0%.7%.0" then @@ -29,6 +33,62 @@ local function check_serf_bin() return nil, "could not find Serf executable (is it in your $PATH?)" end +-- script from old services.serf module +local script_template = [[ +#!/bin/sh + +PAYLOAD=`cat` # Read from stdin +if [ "$SERF_EVENT" != "user" ]; then + PAYLOAD="{\"type\":\"${SERF_EVENT}\",\"entity\": \"${PAYLOAD}\"}" +fi + +CMD="\ +local http = require 'resty.http' \ +local client = http.new() \ +client:connect('%s', %d) \ +client:request { \ + method = 'POST', \ + path = '/cluster/events/', \ + body = [=[${PAYLOAD}]=], \ + headers = { \ + ['content-type'] = 'application/json' \ + } \ +}" + +resty -e "$CMD" +]] + +local function prepare_identifier(kong_config, nginx_prefix) + local serf_path = pl_path.join(nginx_prefix, "serf") + local ok, err = pl_dir.makepath(serf_path) + if not ok then return nil, err end + + local id_path = pl_path.join(nginx_prefix, "serf", serf_node_id) + if not pl_path.exists(id_path) then + local id = utils.get_hostname().."_"..kong_config.cluster_listen.."_"..utils.random_string() + + log.verbose("saving Serf identifier in %s", id_path) + local ok, err = pl_file.write(id_path, id) + if not ok then return nil, err end + end + return true +end + +local function prepare_prefix(kong_config, nginx_prefix, script_path) + local ok, err = prepare_identifier(kong_config, nginx_prefix) + if not ok then return nil, err end + + log.verbose("dumping Serf shell script handler in %s", script_path) + local script = fmt(script_template, "127.0.0.1", kong_config.admin_port) + + local ok, err = pl_file.write(script_path, script) + if not ok then return nil, err end + local ok, _, _, stderr = pl_utils.executeex("chmod +x "..script_path) + if not ok then return nil, stderr end + + return true +end + local function is_running(pid_path) if not pl_path.exists(pid_path) then return nil end local code = kill(pid_path, "-0") @@ -48,14 +108,19 @@ function _M.start(kong_config, nginx_prefix, dao) pl_file.delete(pid_path) end + -- prepare shell script + local script_path = pl_path.join(nginx_prefix, "serf", "serf_event.sh") + local ok, err = prepare_prefix(kong_config, nginx_prefix, script_path) + if not ok then return nil, err end + -- make sure Serf is in PATH local ok, err = check_serf_bin() if not ok then return nil, err end local serf = Serf.new(kong_config, nginx_prefix, dao) + local node_name = serf.node_name local log_path = pl_path.join(nginx_prefix, "logs", "serf.log") - local script_path = pl_path.join(nginx_prefix, "serf", "serf_event.sh") local args = setmetatable({ ["-bind"] = kong_config.cluster_listen, @@ -64,15 +129,15 @@ function _M.start(kong_config, nginx_prefix, dao) ["-encrypt"] = kong_config.cluster_encrypt, ["-log-level"] = "err", ["-profile"] = "wan", - ["-node"] = serf.node_name, + ["-node"] = node_name, ["-event-handler"] = "member-join,member-leave,member-failed," .."member-update,member-reap,user:" ..serf_event_name.."="..script_path }, Serf.args_mt) - local cmd = string.format("nohup %s agent %s > %s 2>&1 & echo $! > %s", - serf_bin_name, tostring(args), - log_path, pid_path) + local cmd = fmt("nohup %s agent %s > %s 2>&1 & echo $! > %s", + serf_bin_name, tostring(args), + log_path, pid_path) log.debug("starting Serf agent: %s", cmd) @@ -99,12 +164,11 @@ function _M.start(kong_config, nginx_prefix, dao) return nil, "could not start Serf:\n "..err end - log.debug("Serf agent running") - log.verbose("auto-joining cluster...") + log.verbose("auto-joining Serf cluster...") local ok, err = serf:autojoin() if not ok then return nil, err end - log.verbose("adding node to cluster (in datastore)...") + log.verbose("adding node to Serf cluster (in datastore)...") local ok, err = serf:add_node() if not ok then return nil, err end @@ -112,14 +176,14 @@ function _M.start(kong_config, nginx_prefix, dao) end function _M.stop(kong_config, nginx_prefix, dao) - log.info("leaving cluster...") + log.info("Leaving cluster") local serf = Serf.new(kong_config, nginx_prefix, dao) local ok, err = serf:leave() if not ok then return nil, err end - local pid_path = pl_path.join(nginx_prefix, "pids", serf_pid_name) + local pid_path = pl_path.join(nginx_prefix, serf_pid_name) log.verbose("stopping Serf agent at %s", pid_path) return kill(pid_path, "-9") end diff --git a/kong/serf.lua b/kong/serf.lua index 7e540679bbca..987df736b21b 100644 --- a/kong/serf.lua +++ b/kong/serf.lua @@ -7,6 +7,7 @@ local pl_path = require "pl.path" local pl_file = require "pl.file" local cjson = require "cjson.safe" local log = require "kong.cmd.utils.log" +local fmt = string.format local serf_node_id = "serf.id" @@ -23,7 +24,7 @@ Serf.args_mt = { function Serf.new(kong_config, nginx_prefix, dao) return setmetatable({ - node_name = pl_file.read(pl_path.join(nginx_prefix, "serf", serf_node_id)), + node_name = assert(pl_file.read(pl_path.join(nginx_prefix, "serf", serf_node_id))), config = kong_config, dao = dao }, Serf) @@ -37,7 +38,7 @@ function Serf:invoke_signal(signal, args, no_rpc) setmetatable(args, Serf.args_mt) end local rpc = no_rpc and "" or "-rpc-addr="..self.config.cluster_listen_rpc - local cmd = string.format("serf %s %s %s", signal, rpc, tostring(args)) + local cmd = fmt("serf %s %s %s", signal, rpc, tostring(args)) local ok, code, stdout = pl_utils.executeex(cmd) if not ok or code ~= 0 then return nil, pl_stringx.splitlines(stdout)[1] end -- always print the first error line of serf @@ -76,11 +77,15 @@ function Serf:members() end function Serf:keygen() - return self:invoke_signal("keygen") + local res, err = self:invoke_signal("keygen") + if not res then return nil, err end + return res end function Serf:reachability() - return self:invoke_signal("reachability") + local res, err = self:invoke_signal("reachability") + if not res then return nil, err end + return res end function Serf:autojoin() @@ -92,7 +97,7 @@ function Serf:autojoin() local nodes, err = self.dao.nodes:find_all() if err then return nil, tostring(err) elseif #nodes == 0 then - log.info("no other Kong nodes were found in the cluster") + log.info("No other Kong nodes were found in the cluster") else -- Sort by newest to oldest (although by TTL would be a better sort) table.sort(nodes, function(a, b) return a.created_at > b.created_at end) @@ -100,16 +105,15 @@ function Serf:autojoin() local joined for _, v in ipairs(nodes) do if self:join_node(v.cluster_listening_address) then - log("successfully auto-joined %s", v.cluster_listening_address) + log("Successfully auto-joined %s", v.cluster_listening_address) joined = true break else - log.warn("could not join %s (if the node does not exist anymore it will be automatically purged)", - v.cluster_listening_address) + log.warn("could not join %s, if the node does not exist anymore it will be automatically purged", v.cluster_listening_address) end end if not joined then - log.warn("could not join existing cluster") + log.warn("could not join the existing cluster") end end @@ -147,7 +151,7 @@ function Serf:event(t_payload) if #payload > 512 then -- Serf can't send a payload greater than 512 bytes - return nil, "encoded payload is "..#payload.." and exceeds the limit of 512 bytes!" + return nil, "Encoded payload is "..#payload.." and exceeds the limit of 512 bytes!" end return self:invoke_signal("event -coalesce=false", " kong '"..payload.."'") diff --git a/spec/02-integration/01-cmd/02-start_stop_spec.lua b/spec/02-integration/01-cmd/02-start_stop_spec.lua index 0f942d5020b6..db95a5980db2 100644 --- a/spec/02-integration/01-cmd/02-start_stop_spec.lua +++ b/spec/02-integration/01-cmd/02-start_stop_spec.lua @@ -133,7 +133,7 @@ describe("kong start/stop", function() assert.equal("", stderr) assert.True(ok) - ok, stderr, stdout = helpers.kong_exec("stop --prefix inexistent") + ok, stderr = helpers.kong_exec("stop --prefix inexistent") assert.False(ok) assert.matches("Error: no such prefix: inexistent", stderr, nil, true) end) diff --git a/spec/03-plugins/oauth2/access_spec.lua b/spec/03-plugins/oauth2/access_spec.lua index b8d3afc93130..4a51d7cb32c5 100644 --- a/spec/03-plugins/oauth2/access_spec.lua +++ b/spec/03-plugins/oauth2/access_spec.lua @@ -1,10 +1,9 @@ local helpers = require "spec.helpers" local cjson = require "cjson" -local meta = require "kong.meta" local pl_stringx = require "pl.stringx" describe("Plugin: oauth2", function() - local client + local proxy_ssl_client, proxy_client setup(function() helpers.dao:truncate_tables() helpers.execute "pkill nginx; pkill serf; pkill dnsmasq" @@ -130,24 +129,28 @@ describe("Plugin: oauth2", function() }) assert(helpers.start_kong()) - proxy_client = assert(helpers.http_client("127.0.0.1", pl_stringx.split(helpers.test_conf.proxy_listen_ssl, ":")[2])) - proxy_client:ssl_handshake() + proxy_client = assert(helpers.http_client("127.0.0.1", helpers.test_conf.proxy_port)) + proxy_ssl_client = assert(helpers.http_client("127.0.0.1", pl_stringx.split(helpers.test_conf.proxy_listen_ssl, ":")[2])) + proxy_ssl_client:ssl_handshake() end) teardown(function() - if client then - client:close() + if proxy_client then + proxy_client:close() + end + if proxy_ssl_client then + proxy_ssl_client:close() end helpers.stop_kong() --helpers.clean_prefix() end) local function provision_code() - local res = assert(proxy_client:send { + local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/authorize", body = { provision_key = "provision123", - client_id = "client123", + client_id = "clientid123", scope = "email", response_type = "code", state = "hello", @@ -170,7 +173,7 @@ describe("Plugin: oauth2", function() local function provision_token() local code = provision_code() - local res = assert(proxy_client:send { + local res = assert(proxy_ssl_client:send { method = "POST", path = "/oauth2/token", body = { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code" }, @@ -187,239 +190,1404 @@ describe("Plugin: oauth2", function() describe("OAuth2 Authorization", function() describe("Code Grant", function() it("returns an error when no provision_key is being sent", function() - + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/authorize", + headers = { + ["Host"] = "oauth2.com" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"error_description":"Invalid Kong provision_key","error":"invalid_provision_key"}]], body) + assert.are.equal("no-store", res.headers["cache-control"]) + assert.are.equal("no-cache", res.headers["pragma"]) end) it("returns an error when no parameter is being sent", function() - + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/authorize", + body = { + provision_key = "provision123" + }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"error_description":"Missing authenticated_userid parameter","error":"invalid_authenticated_userid"}]], body) end) it("returns an error when only provision_key and authenticated_userid are sent", function() - + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/authorize", + body = { + provision_key = "provision123", + authenticated_userid = "id123" + }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"error_description":"Invalid client authentication","error":"invalid_client"}]], body) + assert.are.equal("no-store", res.headers["cache-control"]) + assert.are.equal("no-cache", res.headers["pragma"]) end) - it("returns an error when only the client_is being sent", function() - + it("returns an error when only the client_id is being sent", function() + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/authorize", + body = { + provision_key = "provision123", + authenticated_userid = "id123", + client_id = "clientid123" + }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"redirect_uri":"http:\/\/google.com\/kong?error=invalid_scope&error_description=You%20must%20specify%20a%20scope"}]], body) end) - it("returns an error when only the client_is being sent", function() - + it("returns an error when an invalid scope is being sent", function() + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/authorize", + body = { + provision_key = "provision123", + authenticated_userid = "id123", + client_id = "clientid123", + scope = "wot" + }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"redirect_uri":"http:\/\/google.com\/kong?error=invalid_scope&error_description=%22wot%22%20is%20an%20invalid%20scope"}]], body) end) it("returns an error when no response_type is being sent", function() - + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/authorize", + body = { + provision_key = "provision123", + authenticated_userid = "id123", + client_id = "clientid123", + scope = "email" + }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"redirect_uri":"http:\/\/google.com\/kong?error=unsupported_response_type&error_description=Invalid%20response_type"}]], body) end) it("returns an error with a state when no response_type is being sent", function() - + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/authorize", + body = { + provision_key = "provision123", + authenticated_userid = "id123", + client_id = "clientid123", + scope = "email", + state = "somestate" + }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"redirect_uri":"http:\/\/google.com\/kong?error=unsupported_response_type&error_description=Invalid%20response_type&state=somestate"}]], body) end) it("returns error when the redirect_uri does not match", function() - + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/authorize", + body = { + provision_key = "provision123", + authenticated_userid = "id123", + client_id = "clientid123", + scope = "email", + response_type = "code", + redirect_uri = "http://hello.com/" + }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"redirect_uri":"http:\/\/google.com\/kong?error=invalid_request&error_description=Invalid%20redirect_uri%20that%20does%20not%20match%20with%20any%20redirect_uri%20created%20with%20the%20application"}]], body) end) it("works even if redirect_uri contains a query string", function() - + local res = assert(proxy_client:send { + method = "POST", + path = "/oauth2/authorize", + body = { + provision_key = "provision123", + authenticated_userid = "id123", + client_id = "clientid789", + scope = "email", + response_type = "code" + }, + headers = { + ["Host"] = "oauth2_6.com", + ["Content-Type"] = "application/json", + ["X-Forwarded-Proto"] = "https" + } + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.com/kong\\?code=[\\w]{32,32}&foo=bar$")) end) it("fails when not under HTTPS", function() - + local res = assert(proxy_client:send { + method = "POST", + path = "/oauth2/authorize", + body = { + provision_key = "provision123", + authenticated_userid = "id123", + client_id = "clientid123", + scope = "email", + response_type = "code" + }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"error_description":"You must use HTTPS","error":"access_denied"}]], body) end) it("works when not under HTTPS but accept_http_if_already_terminated is true", function() - + local res = assert(proxy_client:send { + method = "POST", + path = "/oauth2/authorize", + body = { + provision_key = "provision123", + authenticated_userid = "id123", + client_id = "clientid123", + scope = "email", + response_type = "code" + }, + headers = { + ["Host"] = "oauth2_6.com", + ["Content-Type"] = "application/json", + ["X-Forwarded-Proto"] = "https" + } + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.com/kong\\?code=[\\w]{32,32}$")) end) it("fails when not under HTTPS and accept_http_if_already_terminated is false", function() - + local res = assert(proxy_client:send { + method = "POST", + path = "/oauth2/authorize", + body = { + provision_key = "provision123", + authenticated_userid = "id123", + client_id = "clientid123", + scope = "email", + response_type = "code" + }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json", + ["X-Forwarded-Proto"] = "https" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"error_description":"You must use HTTPS","error":"access_denied"}]], body) end) it("returns success", function() - + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/authorize", + body = { + provision_key = "provision123", + authenticated_userid = "id123", + client_id = "clientid123", + scope = "email", + response_type = "code" + }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.com/kong\\?code=[\\w]{32,32}$")) end) it("fails with a path when using the DNS", function() - + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/authorize", + body = { + provision_key = "provision123a", + authenticated_userid = "id123", + client_id = "clientid123", + scope = "email", + response_type = "code" + }, + headers = { + ["Host"] = "mockbin-path.com", + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"error_description":"Invalid Kong provision_key","error":"invalid_provision_key"}]], body) end) it("returns success with a path", function() - + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/somepath/oauth2/authorize", + body = { + provision_key = "provision123", + authenticated_userid = "id123", + client_id = "clientid123", + scope = "email", + response_type = "code" + }, + headers = { + ["Host"] = "mockbin-path.com", + ["Content-Type"] = "application/json" + } + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.com/kong\\?code=[\\w]{32,32}$")) end) - it("should return success when requesting the url with final slash", function() - + it("returns success when requesting the url with final slash", function() + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/authorize/", + body = { + provision_key = "provision123", + authenticated_userid = "id123", + client_id = "clientid123", + scope = "email", + response_type = "code" + }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.com/kong\\?code=[\\w]{32,32}$")) end) - it("should return success with a state", function() - + it("returns success with a state", function() + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/authorize", + body = { + provision_key = "provision123", + authenticated_userid = "id123", + client_id = "clientid123", + scope = "email", + response_type = "code", + state = "hello" + }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.com/kong\\?code=[\\w]{32,32}&state=hello$")) + -- Checking headers + assert.are.equal("no-store", res.headers["cache-control"]) + assert.are.equal("no-cache", res.headers["pragma"]) end) it("returns success and store authenticated user properties", function() - + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/authorize", + body = { + provision_key = "provision123", + client_id = "clientid123", + scope = "email", + response_type = "code", + state = "hello", + authenticated_userid = "userid123" + }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.com/kong\\?code=[\\w]{32,32}&state=hello$")) + + local iterator, err = ngx.re.gmatch(body.redirect_uri, "^http://google\\.com/kong\\?code=([\\w]{32,32})&state=hello$") + assert.is_nil(err) + local m, err = iterator() + assert.is_nil(err) + local data = helpers.dao.oauth2_authorization_codes:find_all {code = m[1]} + assert.are.equal(1, #data) + assert.are.equal(m[1], data[1].code) + assert.are.equal("userid123", data[1].authenticated_userid) + assert.are.equal("email", data[1].scope) end) - it("should return success with a dotted scope and store authenticated user properties", function() - + it("returns success with a dotted scope and store authenticated user properties", function() + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/authorize", + body = { + provision_key = "provision123", + client_id = "clientid123", + scope = "user.email", + response_type = "code", + state = "hello", + authenticated_userid = "userid123" + }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.com/kong\\?code=[\\w]{32,32}&state=hello$")) + + local iterator, err = ngx.re.gmatch(body.redirect_uri, "^http://google\\.com/kong\\?code=([\\w]{32,32})&state=hello$") + assert.is_nil(err) + local m, err = iterator() + assert.is_nil(err) + local data = helpers.dao.oauth2_authorization_codes:find_all {code = m[1]} + assert.are.equal(1, #data) + assert.are.equal(m[1], data[1].code) + assert.are.equal("userid123", data[1].authenticated_userid) + assert.are.equal("user.email", data[1].scope) end) end) describe("Implicit Grant", function() - it("should return success", function() - + it("returns success", function() + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/authorize", + body = { + provision_key = "provision123", + authenticated_userid = "id123", + client_id = "clientid123", + scope = "email", + response_type = "token" + }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.com/kong\\?access_token=[\\w]{32,32}&expires_in=[\\d]+&token_type=bearer$")) + assert.are.equal("no-store", res.headers["cache-control"]) + assert.are.equal("no-cache", res.headers["pragma"]) end) - it("should return success and the state", function() - + it("returns success and the state", function() + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/authorize", + body = { + provision_key = "provision123", + authenticated_userid = "id123", + client_id = "clientid123", + scope = "email", + response_type = "token", + state = "wot" + }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.com/kong\\?access_token=[\\w]{32,32}&expires_in=[\\d]+&state=wot&token_type=bearer$")) end) - it("should return success and the token should have the right expiration", function() - + it("returns success and the token should have the right expiration", function() + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/authorize", + body = { + provision_key = "provision123", + authenticated_userid = "id123", + client_id = "clientid123", + scope = "email", + response_type = "token" + }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.com/kong\\?access_token=[\\w]{32,32}&expires_in=[\\d]+&token_type=bearer$")) + + local iterator, err = ngx.re.gmatch(body.redirect_uri, "^http://google\\.com/kong\\?access_token=([\\w]{32,32})&expires_in=[\\d]+&token_type=bearer$") + assert.is_nil(err) + local m, err = iterator() + assert.is_nil(err) + local data = helpers.dao.oauth2_tokens:find_all {access_token = m[1]} + assert.are.equal(1, #data) + assert.are.equal(m[1], data[1].access_token) + assert.are.equal(5, data[1].expires_in) + assert.falsy(data[1].refresh_token) end) - it("should return success and store authenticated user properties", function() - + it("returns success and store authenticated user properties", function() + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/authorize", + body = { + provision_key = "provision123", + client_id = "clientid123", + scope = "email profile", + response_type = "token", + authenticated_userid = "userid123" + }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.com/kong\\?access_token=[\\w]{32,32}&expires_in=[\\d]+&token_type=bearer$")) + + local iterator, err = ngx.re.gmatch(body.redirect_uri, "^http://google\\.com/kong\\?access_token=([\\w]{32,32})&expires_in=[\\d]+&token_type=bearer$") + assert.is_nil(err) + local m, err = iterator() + assert.is_nil(err) + local data = helpers.dao.oauth2_tokens:find_all {access_token = m[1]} + assert.are.equal(1, #data) + assert.are.equal(m[1], data[1].access_token) + assert.are.equal("userid123", data[1].authenticated_userid) + assert.are.equal("email profile", data[1].scope) + + -- Checking that there is no refresh token since it's an implicit grant + assert.are.equal(5, data[1].expires_in) + assert.falsy(data[1].refresh_token) end) - it("should return set the right upstream headers", function() - + it("returns set the right upstream headers", function() + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/authorize", + body = { + provision_key = "provision123", + client_id = "clientid123", + scope = "email profile", + response_type = "token", + authenticated_userid = "userid123" + }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + local body = cjson.decode(assert.res_status(200, res)) + local iterator, err = ngx.re.gmatch(body.redirect_uri, "^http://google\\.com/kong\\?access_token=([\\w]{32,32})&expires_in=[\\d]+&token_type=bearer$") + assert.is_nil(err) + local m, err = iterator() + assert.is_nil(err) + local access_token = m[1] + + local res = assert(proxy_ssl_client:send { + method = "GET", + path = "/request?access_token="..access_token, + headers = { + ["Host"] = "oauth2.com" + } + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.truthy(body.headers["x-consumer-id"]) + assert.are.equal("bob", body.headers["x-consumer-username"]) + assert.are.equal("email profile", body.headers["x-authenticated-scope"]) + assert.are.equal("userid123", body.headers["x-authenticated-userid"]) end) end) describe("Client Credentials", function() - it("should return an error when client_secret is not sent", function() - + it("returns an error when client_secret is not sent", function() + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/token", + body = { + client_id = "clientid123", + scope = "email", + response_type = "token" + }, + headers = { + ["Host"] = "oauth2_4.com", + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"error_description":"Invalid client authentication","error":"invalid_client"}]], body) end) - it("should return an error when client_secret is not sent", function() - + it("returns an error when client_secret is not sent", function() + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/token", + body = { + client_id = "clientid123", + client_secret="secret123", + scope = "email", + response_type = "token" + }, + headers = { + ["Host"] = "oauth2_4.com", + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"error":"unsupported_grant_type","error_description":"Invalid grant_type"}]], body) end) - it("should fail when not under HTTPS", function() - + it("fails when not under HTTPS", function() + local res = assert(proxy_client:send { + method = "POST", + path = "/oauth2/token", + body = { + client_id = "clientid123", + client_secret="secret123", + scope = "email", + grant_type = "client_credentials" + }, + headers = { + ["Host"] = "oauth2_4.com", + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"error_description":"You must use HTTPS","error":"access_denied"}]], body) end) - it("should return fail when setting authenticated_userid and no provision_key", function() - + it("fails when setting authenticated_userid and no provision_key", function() + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/token", + body = { + client_id = "clientid123", + client_secret="secret123", + scope = "email", + grant_type = "client_credentials", + authenticated_userid = "user123" + }, + headers = { + ["Host"] = "oauth2_4.com", + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"error_description":"Invalid Kong provision_key","error":"invalid_provision_key"}]], body) end) - it("should return fail when setting authenticated_userid and invalid provision_key", function() - + it("fails when setting authenticated_userid and invalid provision_key", function() + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/token", + body = { + client_id = "clientid123", + client_secret="secret123", + scope = "email", + grant_type = "client_credentials", + authenticated_userid = "user123", + provision_key = "hello" + }, + headers = { + ["Host"] = "oauth2_4.com", + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"error_description":"Invalid Kong provision_key","error":"invalid_provision_key"}]], body) end) - it("should return success", function() - + it("returns success", function() + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/token", + body = { + client_id = "clientid123", + client_secret="secret123", + scope = "email", + grant_type = "client_credentials" + }, + headers = { + ["Host"] = "oauth2_4.com", + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(200, res) + assert.is_table(ngx.re.match(body, [[^\{"token_type":"bearer","access_token":"[\w]{32,32}","expires_in":5\}$]])) end) - it("should return success with authenticated_userid and valid provision_key", function() - + it("returns success with authenticated_userid and valid provision_key", function() + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/token", + body = { + client_id = "clientid123", + client_secret="secret123", + scope = "email", + grant_type = "client_credentials", + authenticated_userid = "hello", + provision_key = "provision123" + }, + headers = { + ["Host"] = "oauth2_4.com", + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(200, res) + assert.is_table(ngx.re.match(body, [[^\{"token_type":"bearer","access_token":"[\w]{32,32}","expires_in":5\}$]])) end) - it("should return success with authorization header", function() - + it("returns success with authorization header", function() + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/token", + body = { + scope = "email", + grant_type = "client_credentials" + }, + headers = { + ["Host"] = "oauth2_4.com", + ["Content-Type"] = "application/json", + Authorization = "Basic Y2xpZW50aWQxMjM6c2VjcmV0MTIz" + } + }) + local body = assert.res_status(200, res) + assert.is_table(ngx.re.match(body, [[^\{"token_type":"bearer","access_token":"[\w]{32,32}","expires_in":5\}$]])) end) - it("should return an error with a wrong authorization header", function() - + it("returns an error with a wrong authorization header", function() + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/token", + body = { + scope = "email", + grant_type = "client_credentials" + }, + headers = { + ["Host"] = "oauth2_4.com", + ["Content-Type"] = "application/json", + Authorization = "Basic Y2xpZW50aWQxMjM6c2VjcmV0MTI0" + } + }) + local body = assert.res_status(401, res) + assert.equal([[{"error_description":"Invalid client authentication","error":"invalid_client"}]], body) + assert.are.equal("Basic realm=\"OAuth2.0\"", res.headers["www-authenticate"]) end) - it("should return set the right upstream headers", function() - + it("sets the right upstream headers", function() + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/token", + body = { + client_id = "clientid123", + client_secret="secret123", + scope = "email", + grant_type = "client_credentials", + authenticated_userid = "hello", + provision_key = "provision123" + }, + headers = { + ["Host"] = "oauth2_4.com", + ["Content-Type"] = "application/json" + } + }) + local body = cjson.decode(assert.res_status(200, res)) + + local res = assert(proxy_ssl_client:send { + method = "GET", + path = "/request?access_token="..body.access_token, + headers = { + ["Host"] = "oauth2_4.com" + } + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.truthy(body.headers["x-consumer-id"]) + assert.are.equal("bob", body.headers["x-consumer-username"]) + assert.are.equal("email", body.headers["x-authenticated-scope"]) + assert.are.equal("hello", body.headers["x-authenticated-userid"]) end) - it("should return set the right upstream headers", function() - + it("works in a multipart request", function() + error("TODO") + --[[ + local response, status = http_client.post_multipart(PROXY_SSL_URL.."/oauth2/token", { client_id = "clientid123", client_secret="secret123", scope = "email", grant_type = "client_credentials", authenticated_userid = "hello", provision_key = "provision123" }, {host = "oauth2_4.com"}) + assert.are.equal(200, status) + + local _, status = http_client.post_multipart(PROXY_SSL_URL.."/request", { access_token = cjson.decode(response).access_token }, {host = "oauth2_4.com"}) + assert.are.equal(200, status) + --]] end) end) describe("Password Grant", function() - it("should block unauthorized requests", function() - + it("blocks unauthorized requests", function() + local res = assert(proxy_ssl_client:send { + method = "GET", + path = "/request", + headers = { + ["Host"] = "oauth2_5.com" + } + }) + local body = assert.res_status(401, res) + assert.equal([[{"error_description":"The access token is missing","error":"invalid_request"}]], body) end) - it("should return an error when client_secret is not sent", function() - + it("returns an error when client_secret is not sent", function() + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/token", + body = { + client_id = "clientid123", + scope = "email", + response_type = "token" + }, + headers = { + ["Host"] = "oauth2_5.com", + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"error_description":"Invalid client authentication","error":"invalid_client"}]], body) end) - it("should return an error when client_secret is not sent", function() - + it("returns an error when grant_type is not sent", function() + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/token", + body = { + client_id = "clientid123", + client_secret="secret123", + scope = "email", + response_type = "token" + }, + headers = { + ["Host"] = "oauth2_5.com", + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"error":"unsupported_grant_type","error_description":"Invalid grant_type"}]], body) end) - it("should fail when no provision key is being sent", function() - + it("fails when no provision key is being sent", function() + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/token", + body = { + client_id = "clientid123", + client_secret="secret123", + scope = "email", + response_type = "token", + grant_type = "password" + }, + headers = { + ["Host"] = "oauth2_5.com", + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"error_description":"Invalid Kong provision_key","error":"invalid_provision_key"}]], body) end) - it("should fail when no provision key is being sent", function() - + it("fails when no provision key is being sent", function() + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/token", + body = { + client_id = "clientid123", + client_secret="secret123", + scope = "email", + grant_type = "password" + }, + headers = { + ["Host"] = "oauth2_5.com", + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"error_description":"Invalid Kong provision_key","error":"invalid_provision_key"}]], body) end) - it("should fail when no authenticated user id is being sent", function() - + it("fails when no authenticated user id is being sent", function() + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/token", + body = { + provision_key = "provision123", + client_id = "clientid123", + client_secret="secret123", + scope = "email", + grant_type = "password" + }, + headers = { + ["Host"] = "oauth2_5.com", + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"error_description":"Missing authenticated_userid parameter","error":"invalid_authenticated_userid"}]], body) end) - it("should fail when no authenticated user id is being sent", function() - + it("returns success", function() + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/token", + body = { + provision_key = "provision123", + authenticated_userid = "id123", + client_id = "clientid123", + client_secret="secret123", + scope = "email", + grant_type = "password" + }, + headers = { + ["Host"] = "oauth2_5.com", + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(200, res) + assert.is_table(ngx.re.match(body, [[^\{"refresh_token":"[\w]{32,32}","token_type":"bearer","access_token":"[\w]{32,32}","expires_in":5\}$]])) end) - it("should fail when no authenticated user id is being sent", function() - + it("returns success with authorization header", function() + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/token", + body = { + provision_key = "provision123", + authenticated_userid = "id123", + scope = "email", + grant_type = "password" + }, + headers = { + ["Host"] = "oauth2_5.com", + ["Content-Type"] = "application/json", + Authorization = "Basic Y2xpZW50aWQxMjM6c2VjcmV0MTIz" + } + }) + local body = assert.res_status(200, res) + assert.is_table(ngx.re.match(body, [[^\{"refresh_token":"[\w]{32,32}","token_type":"bearer","access_token":"[\w]{32,32}","expires_in":5\}$]])) end) - it("should return an error with a wrong authorization header", function() - + it("returns an error with a wrong authorization header", function() + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/token", + body = { + provision_key = "provision123", + authenticated_userid = "id123", + scope = "email", + grant_type = "password" + }, + headers = { + ["Host"] = "oauth2_5.com", + ["Content-Type"] = "application/json", + Authorization = "Basic Y2xpZW50aWQxMjM6c2VjcmV0MTI0" + } + }) + local body = assert.res_status(401, res) + assert.equal([[{"error_description":"Invalid client authentication","error":"invalid_client"}]], body) + assert.are.equal("Basic realm=\"OAuth2.0\"", res.headers["www-authenticate"]) end) - it("should return an error with a wrong authorization header", function() - + it("sets the right upstream headers", function() + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/token", + body = { + provision_key = "provision123", + authenticated_userid = "id123", + scope = "email", + grant_type = "password" + }, + headers = { + ["Host"] = "oauth2_5.com", + ["Content-Type"] = "application/json", + Authorization = "Basic Y2xpZW50aWQxMjM6c2VjcmV0MTIz" + } + }) + local body = cjson.decode(assert.res_status(200, res)) + + local res = assert(proxy_ssl_client:send { + method = "GET", + path = "/request?access_token="..body.access_token, + headers = { + ["Host"] = "oauth2_5.com" + } + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.truthy(body.headers["x-consumer-id"]) + assert.are.equal("bob", body.headers["x-consumer-username"]) + assert.are.equal("email", body.headers["x-authenticated-scope"]) + assert.are.equal("id123", body.headers["x-authenticated-userid"]) end) end) end) describe("OAuth2 Access Token", function() - it("should return an error when nothing is being sent", function() - + it("returns an error when nothing is being sent", function() + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/token", + headers = { + ["Host"] = "oauth2.com" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"error_description":"Invalid client authentication","error":"invalid_client"}]], body) + -- Checking headers + assert.are.equal("no-store", res.headers["cache-control"]) + assert.are.equal("no-cache", res.headers["pragma"]) end) - it("should return an error when only the code is being sent", function() - + it("returns an error when only the code is being sent", function() + local code = provision_code() + + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/token", + body = { + code = code + }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"error_description":"Invalid client authentication","error":"invalid_client"}]], body) + -- Checking headers + assert.are.equal("no-store", res.headers["cache-control"]) + assert.are.equal("no-cache", res.headers["pragma"]) end) - it("should return an error when only the code and client_secret are being sent", function() - + it("returns an error when only the code and client_secret are being sent", function() + local code = provision_code() + + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/token", + body = { + code = code, + client_secret = "secret123" + }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"error_description":"Invalid client authentication","error":"invalid_client"}]], body) + -- Checking headers + assert.are.equal("no-store", res.headers["cache-control"]) + assert.are.equal("no-cache", res.headers["pragma"]) end) - it("should return an error when only the code and client_secret and client_id are being sent", function() - + it("returns an error when grant_type is not being sent", function() + local code = provision_code() + + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/token", + body = { + code = code, + client_id = "clientid123", + client_secret = "secret123" + }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"error":"unsupported_grant_type","error_description":"Invalid grant_type"}]], body) end) - it("should return an error when only the code and client_secret and client_id are being sent", function() - + it("returns an error with a wrong code", function() + local code = provision_code() + + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/token", + body = { + code = code.."hello", + client_id = "clientid123", + client_secret = "secret123", + grant_type = "authorization_code" + }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"error":"invalid_request","error_description":"Invalid code"}]], body) end) - it("should return success without state", function() - + it("returns success without state", function() + local code = provision_code() + + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/token", + body = { + code = code, + client_id = "clientid123", + client_secret = "secret123", + grant_type = "authorization_code" + }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(200, res) + assert.is_table(ngx.re.match(body, [[^\{"refresh_token":"[\w]{32,32}","token_type":"bearer","access_token":"[\w]{32,32}","expires_in":5\}$]])) end) - it("should return success with state", function() - + it("returns success with state", function() + local code = provision_code() + + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/token", + body = { + code = code, + client_id = "clientid123", + client_secret = "secret123", + grant_type = "authorization_code", + state = "wot" + }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(200, res) + assert.is_table(ngx.re.match(body, [[^\{"refresh_token":"[\w]{32,32}","token_type":"bearer","state":"wot","access_token":"[\w]{32,32}","expires_in":5\}$]])) end) - it("should return set the right upstream headers", function() - + it("sets the right upstream headers", function() + local code = provision_code() + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/token", + body = { + code = code, + client_id = "clientid123", + client_secret = "secret123", + grant_type = "authorization_code" + }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + local body = cjson.decode(assert.res_status(200, res)) + + local res = assert(proxy_ssl_client:send { + method = "GET", + path = "/request?access_token="..body.access_token, + headers = { + ["Host"] = "oauth2.com" + } + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.truthy(body.headers["x-consumer-id"]) + assert.are.equal("bob", body.headers["x-consumer-username"]) + assert.are.equal("email", body.headers["x-authenticated-scope"]) + assert.are.equal("userid123", body.headers["x-authenticated-userid"]) end) end) describe("Making a request", function() - it("should work when a correct access_token is being sent in the querystring", function() - + it("works when a correct access_token is being sent in the querystring", function() + local token = provision_token() + + local res = assert(proxy_ssl_client:send { + method = "GET", + path = "/request?access_token="..token.access_token, + headers = { + ["Host"] = "oauth2.com" + } + }) + assert.res_status(200, res) end) - it("should work when a correct access_token is being sent in a form body", function() - + it("works when a correct access_token is being sent in a form body", function() + local token = provision_token() + + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/request", + body = { + access_token = token.access_token + }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + assert.res_status(200, res) end) - it("should work when a correct access_token is being sent in an authorization header (bearer)", function() - + it("works when a correct access_token is being sent in an authorization header (bearer)", function() + local token = provision_token() + + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/request", + headers = { + ["Host"] = "oauth2.com", + Authorization = "bearer "..token.access_token + } + }) + assert.res_status(200, res) end) - it("should work when a correct access_token is being sent in an authorization header (token)", function() - + it("works when a correct access_token is being sent in an authorization header (token)", function() + local token = provision_token() + + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/request", + headers = { + ["Host"] = "oauth2.com", + Authorization = "bearer "..token.access_token + } + }) + local body = cjson.decode(assert.res_status(200, res)) + + local consumer = helpers.dao.consumers:find_all({username = "bob"})[1] + assert.are.equal(consumer.id, body.headers["x-consumer-id"]) + assert.are.equal(consumer.username, body.headers["x-consumer-username"]) + assert.are.equal("userid123", body.headers["x-authenticated-userid"]) + assert.are.equal("email", body.headers["x-authenticated-scope"]) end) end) describe("Authentication challenge", function() - it("should return 401 Unauthorized without error if it lacks any authentication information", function() - + it("returns 401 Unauthorized without error if it lacks any authentication information", function() + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/request", + headers = { + ["Host"] = "oauth2.com" + } + }) + local body = assert.res_status(401, res) + assert.equal([[{"error_description":"The access token is missing","error":"invalid_request"}]], body) + assert.are.equal('Bearer realm="service"', res.headers['www-authenticate']) end) - it("should return 401 Unauthorized when an invalid access token is being sent via url parameter", function() - + it("returns 401 Unauthorized when an invalid access token is being sent via url parameter", function() + local res = assert(proxy_ssl_client:send { + method = "GET", + path = "/request?access_token=invalid", + headers = { + ["Host"] = "oauth2.com" + } + }) + local body = assert.res_status(401, res) + assert.equal([[{"error_description":"The access token is invalid or has expired","error":"invalid_token"}]], body) + assert.are.equal('Bearer realm="service" error="invalid_token" error_description="The access token is invalid or has expired"', res.headers['www-authenticate']) end) - it("should return 401 Unauthorized when an invalid access token is being sent via the Authorization header", function() - + it("returns 401 Unauthorized when an invalid access token is being sent via the Authorization header", function() + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/request", + headers = { + ["Host"] = "oauth2.com", + Authorization = "bearer invalid" + } + }) + local body = assert.res_status(401, res) + assert.equal([[{"error_description":"The access token is invalid or has expired","error":"invalid_token"}]], body) + assert.are.equal('Bearer realm="service" error="invalid_token" error_description="The access token is invalid or has expired"', res.headers['www-authenticate']) end) - it("should return 401 Unauthorized when token has expired", function() - + it("returns 401 Unauthorized when token has expired", function() + local token = provision_token() + + -- Token expires in (5 seconds) + ngx.sleep(7) + + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/request", + headers = { + ["Host"] = "oauth2.com", + Authorization = "bearer "..token.access_token + } + }) + local body = assert.res_status(401, res) + assert.equal([[{"error_description":"The access token is invalid or has expired","error":"invalid_token"}]], body) + assert.are.equal('Bearer realm="service" error="invalid_token" error_description="The access token is invalid or has expired"', res.headers['www-authenticate']) end) end) describe("Refresh Token", function() - it("should not refresh an invalid access token", function() - + it("does not refresh an invalid access token", function() + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/token", + body = { + refresh_token = "hello", + client_id = "clientid123", + client_secret = "secret123", + grant_type = "refresh_token" + }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(400, res) + assert.equal([[{"error":"invalid_request","error_description":"Invalid refresh_token"}]], body) end) - it("should refresh an valid access token", function() - + it("refreshes an valid access token", function() + local token = provision_token() + + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/token", + body = { + refresh_token = token.refresh_token, + client_id = "clientid123", + client_secret = "secret123", + grant_type = "refresh_token" + }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json" + } + }) + local body = assert.res_status(200, res) + assert.is_table(ngx.re.match(body, [[^\{"refresh_token":"[\w]{32,32}","token_type":"bearer","access_token":"[\w]{32,32}","expires_in":5\}$]])) end) - it("should expire after 5 seconds", function() - + it("expires after 5 seconds", function() + local token = provision_token() + + local res = assert(proxy_client:send { + method = "POST", + path = "/request", + headers = { + ["Host"] = "oauth2.com", + authorization = "bearer "..token.access_token + } + }) + assert.res_status(200, res) + + local id = helpers.dao.oauth2_tokens:find_all({access_token = token.access_token })[1].id + assert.truthy(helpers.dao.oauth2_tokens:find({id=id})) + + -- But waiting after the cache expiration (5 seconds) should block the request + ngx.sleep(7) + + local res = assert(proxy_client:send { + method = "POST", + path = "/request", + headers = { + ["Host"] = "oauth2.com", + authorization = "bearer "..token.access_token + } + }) + local body = assert.res_status(401, res) + assert.equal([[{"error_description":"The access token is invalid or has expired","error":"invalid_token"}]], body) + + -- Refreshing the token + local res = assert(proxy_ssl_client:send { + method = "POST", + path = "/oauth2/token", + body = { + refresh_token = token.refresh_token, + client_id = "clientid123", + client_secret = "secret123", + grant_type = "refresh_token" + }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/json", + authorization = "bearer "..token.access_token + } + }) + local body = assert.res_status(200, res) + assert.is_table(ngx.re.match(body, [[^\{"refresh_token":"[\w]{32,32}","token_type":"bearer","access_token":"[\w]{32,32}","expires_in":5\}$]])) + + assert.falsy(token.access_token == cjson.decode(body).access_token) + assert.falsy(token.refresh_token == cjson.decode(body).refresh_token) + + assert.falsy(helpers.dao.oauth2_tokens:find({id=id})) end) end) - describe("Hide Credentials", function() - it("should not hide credentials in the body", function() - + describe("#only Hide Credentials", function() + it("does not hide credentials in the body", function() + local token = provision_token() + + local res = assert(proxy_client:send { + method = "POST", + path = "/request", + body = { + access_token = token.access_token + }, + headers = { + ["Host"] = "oauth2.com", + ["Content-Type"] = "application/x-www-form-urlencoded" + } + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.equal(token.access_token, body.postData.params.access_token) end) - it("should hide credentials in the body", function() - + it("hides credentials in the body", function() + local token = provision_token() + + local res = assert(proxy_client:send { + method = "POST", + path = "/request", + body = { + access_token = token.access_token + }, + headers = { + ["Host"] = "oauth2_3.com", + ["Content-Type"] = "application/x-www-form-urlencoded" + } + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.is_nil(body.postData.params.access_token) end) - it("should hide credentials in the body", function() - + it("does not hide credentials in the querystring", function() + local token = provision_token() + + local res = assert(proxy_client:send { + method = "GET", + path = "/request?access_token="..token.access_token, + headers = { + ["Host"] = "oauth2.com" + } + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.equal(token.access_token, body.queryString.access_token) end) - it("should hide credentials in the querystring", function() - + it("hides credentials in the querystring", function() + local token = provision_token() + + local res = assert(proxy_client:send { + method = "GET", + path = "/request?access_token="..token.access_token, + headers = { + ["Host"] = "oauth2_3.com" + } + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.is_nil(body.queryString.access_token) end) - it("should hide credentials in the querystring", function() - + it("does not hide credentials in the header", function() + local token = provision_token() + + local res = assert(proxy_client:send { + method = "GET", + path = "/request", + headers = { + ["Host"] = "oauth2.com", + authorization = "bearer "..token.access_token + } + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.equal("bearer "..token.access_token, body.headers.authorization) end) - it("should hide credentials in the querystring", function() - + it("hides credentials in the header", function() + local token = provision_token() + + local res = assert(proxy_client:send { + method = "GET", + path = "/request", + headers = { + ["Host"] = "oauth2_3.com", + authorization = "bearer "..token.access_token + } + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.is_nil(body.headers.authorization) end) end) end) diff --git a/spec/03-plugins/rate-limiting/access_spec.lua b/spec/03-plugins/rate-limiting/access_spec.lua index 2d81e922bac7..85b0f7b25616 100644 --- a/spec/03-plugins/rate-limiting/access_spec.lua +++ b/spec/03-plugins/rate-limiting/access_spec.lua @@ -334,7 +334,7 @@ describe("Plugin: rate-limiting", function() end) end) - describe("Continue on error", function() + describe("#only Continue on error", function() after_each(function() prepare() end) diff --git a/spec/helpers.lua b/spec/helpers.lua index 9754d195cded..be10ae77cd46 100644 --- a/spec/helpers.lua +++ b/spec/helpers.lua @@ -185,23 +185,15 @@ luassert:register("assertion", "res_status", res_status, -- Shell helpers ---------------- local function exec(...) - local ok, _, stdout, stderr = pl_utils.executeex(...) - if not ok then - stdout = nil -- don't return 3rd value if fail because of busted's `assert` - end - return ok, stderr, stdout + local ok, _, _, stderr = pl_utils.executeex(...) + return ok, stderr end -local function kong_exec(cmd, env) - cmd = cmd or "" - env = env or {} +local function kong_exec(args, prefix) + args = args or "" + prefix = prefix or conf.prefix - local env_vars = "" - for k, v in pairs(env) do - env_vars = string.format("%s KONG_%s=%s", env_vars, k:upper(), v) - end - - return exec(env_vars.." "..BIN_PATH.." "..cmd) + return exec(BIN_PATH.." "..args.." --prefix "..prefix) end ---------- @@ -212,7 +204,7 @@ return { dir = pl_dir, path = pl_path, file = pl_file, - utils = pl_utils, + execute = pl_utils.executeex, -- Kong testing properties dao = dao, @@ -221,7 +213,6 @@ return { test_conf_path = TEST_CONF_PATH, -- Kong testing helpers - execute = exec, kong_exec = kong_exec, http_client = http_client, wait_until = wait_until, @@ -230,6 +221,7 @@ return { prepare_prefix = function(prefix) prefix = prefix or conf.prefix return pl_dir.makepath(prefix) + --kong_exec("stop", prefix) end, clean_prefix = function(prefix) prefix = prefix or conf.prefix @@ -237,14 +229,10 @@ return { pl_dir.rmtree(prefix) end end, - start_kong = function() - return kong_exec("start --conf "..TEST_CONF_PATH) - end, - stop_kong = function() - return kong_exec("stop --conf "..TEST_CONF_PATH) + start_kong = function(prefix) + return kong_exec("start --conf "..TEST_CONF_PATH, prefix) end, - kill_all = function() - dao:truncate_tables() -- truncate nodes table too - return exec "pkill nginx; pkill serf; pkill dnsmasq" + stop_kong = function(prefix) + return kong_exec("stop --conf "..TEST_CONF_PATH, prefix) end } From 4eb34b0302de4a8c80063fb69044bb51cf370222 Mon Sep 17 00:00:00 2001 From: thefosk Date: Thu, 23 Jun 2016 17:01:00 -0700 Subject: [PATCH 29/29] oauth2 --- spec/03-plugins/oauth2/access_old.lua | 874 -------------------------- 1 file changed, 874 deletions(-) delete mode 100644 spec/03-plugins/oauth2/access_old.lua diff --git a/spec/03-plugins/oauth2/access_old.lua b/spec/03-plugins/oauth2/access_old.lua deleted file mode 100644 index 4dec9851f4fb..000000000000 --- a/spec/03-plugins/oauth2/access_old.lua +++ /dev/null @@ -1,874 +0,0 @@ -local spec_helper = require "spec.spec_helpers" -local utils = require "kong.tools.utils" -local http_client = require "kong.tools.http_client" -local cjson = require "cjson" -local rex = require "rex_pcre" - --- Load everything we need from the spec_helper -local env = spec_helper.get_env() -- test environment -local dao_factory = env.dao_factory - -local PROXY_SSL_URL = spec_helper.PROXY_SSL_URL -local PROXY_URL = spec_helper.PROXY_URL -local STUB_GET_URL = spec_helper.STUB_GET_URL -local STUB_POST_URL = spec_helper.STUB_POST_URL - -local function provision_code() - local response = http_client.post(PROXY_SSL_URL.."/oauth2/authorize", { provision_key = "provision123", client_id = "clientid123", scope = "email", response_type = "code", state = "hello", authenticated_userid = "userid123" }, {host = "oauth2.com"}) - local body = cjson.decode(response) - local matches = rex.gmatch(body.redirect_uri, "^http://google\\.com/kong\\?code=([\\w]{32,32})&state=hello$") - local code - for line in matches do - code = line - end - local data = dao_factory.oauth2_authorization_codes:find_all {code = code} - return data[1].code -end - -local function provision_token() - local code = provision_code() - - local response = http_client.post(PROXY_SSL_URL.."/oauth2/token", { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code" }, {host = "oauth2.com"}) - return cjson.decode(response) -end - -describe("Authentication Plugin", function() - - setup(function() - spec_helper.prepare_db() - spec_helper.insert_fixtures { - api = { - { name = "tests-oauth2", request_host = "oauth2.com", upstream_url = "http://mockbin.com" }, - { name = "tests-oauth2-with-path", request_host = "mockbin-path.com", upstream_url = "http://mockbin.com", request_path = "/somepath/" }, - { name = "tests-oauth2-with-hide-credentials", request_host = "oauth2_3.com", upstream_url = "http://mockbin.com" }, - { name = "tests-oauth2-client-credentials", request_host = "oauth2_4.com", upstream_url = "http://mockbin.com" }, - { name = "tests-oauth2-password-grant", request_host = "oauth2_5.com", upstream_url = "http://mockbin.com" }, - { name = "tests-oauth2-accept_http_if_already_terminated", request_host = "oauth2_6.com", upstream_url = "http://mockbin.com" }, - }, - consumer = { - { username = "auth_tests_consumer" } - }, - plugin = { - { name = "oauth2", config = { scopes = { "email", "profile", "user.email" }, mandatory_scope = true, provision_key = "provision123", token_expiration = 5, enable_implicit_grant = true }, __api = 1 }, - { name = "oauth2", config = { scopes = { "email", "profile" }, mandatory_scope = true, provision_key = "provision123", token_expiration = 5, enable_implicit_grant = true }, __api = 2 }, - { name = "oauth2", config = { scopes = { "email", "profile" }, mandatory_scope = true, provision_key = "provision123", token_expiration = 5, enable_implicit_grant = true, hide_credentials = true }, __api = 3 }, - { name = "oauth2", config = { scopes = { "email", "profile" }, mandatory_scope = true, provision_key = "provision123", token_expiration = 5, enable_client_credentials = true, enable_authorization_code = false }, __api = 4 }, - { name = "oauth2", config = { scopes = { "email", "profile" }, mandatory_scope = true, provision_key = "provision123", token_expiration = 5, enable_password_grant = true, enable_authorization_code = false }, __api = 5 }, - { name = "oauth2", config = { scopes = { "email", "profile", "user.email" }, mandatory_scope = true, provision_key = "provision123", token_expiration = 5, enable_implicit_grant = true, accept_http_if_already_terminated = true }, __api = 6 }, - }, - oauth2_credential = { - { client_id = "clientid123", client_secret = "secret123", redirect_uri = {"http://google.com/kong"}, name="testapp", __consumer = 1 }, - { client_id = "clientid789", client_secret = "secret789", redirect_uri = {"http://google.com/kong?foo=bar&code=123"}, name="testapp3", __consumer = 1 } - } - } - spec_helper.start_kong() - end) - - teardown(function() - spec_helper.stop_kong() - end) - - describe("OAuth2 Authorization", function() - - describe("Code Grant", function() - - it("should return an error when no provision_key is being sent", function() - local response, status, headers = http_client.post(PROXY_SSL_URL.."/oauth2/authorize", { }, {host = "oauth2.com"}) - local body = cjson.decode(response) - assert.are.equal(400, status) - assert.are.equal(2, utils.table_size(body)) - assert.are.equal("invalid_provision_key", body.error) - assert.are.equal("Invalid Kong provision_key", body.error_description) - - -- Checking headers - assert.are.equal("no-store", headers["cache-control"]) - assert.are.equal("no-cache", headers["pragma"]) - end) - - it("should return an error when no parameter is being sent", function() - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/authorize", { provision_key = "provision123" }, {host = "oauth2.com"}) - local body = cjson.decode(response) - assert.are.equal(400, status) - assert.are.equal(2, utils.table_size(body)) - assert.are.equal("invalid_authenticated_userid", body.error) - assert.are.equal("Missing authenticated_userid parameter", body.error_description) - end) - - it("should return an error when only provision_key and authenticated_userid are sent", function() - local response, status, headers = http_client.post(PROXY_SSL_URL.."/oauth2/authorize", { provision_key = "provision123", authenticated_userid = "id123" }, {host = "oauth2.com"}) - local body = cjson.decode(response) - assert.are.equal(400, status) - assert.are.equal(2, utils.table_size(body)) - assert.are.equal("invalid_client", body.error) - assert.are.equal("Invalid client authentication", body.error_description) - - -- Checking headers - assert.are.equal("no-store", headers["cache-control"]) - assert.are.equal("no-cache", headers["pragma"]) - end) - - it("should return an error when only the client_is being sent", function() - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/authorize", { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid123" }, {host = "oauth2.com"}) - local body = cjson.decode(response) - assert.are.equal(400, status) - assert.are.equal(1, utils.table_size(body)) - assert.are.equal("http://google.com/kong?error=invalid_scope&error_description=You%20must%20specify%20a%20scope", body.redirect_uri) - end) - - it("should return an error when an invalid scope is being sent", function() - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/authorize", { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid123", scope = "wot" }, {host = "oauth2.com"}) - local body = cjson.decode(response) - assert.are.equal(400, status) - assert.are.equal(1, utils.table_size(body)) - assert.are.equal("http://google.com/kong?error=invalid_scope&error_description=%22wot%22%20is%20an%20invalid%20scope", body.redirect_uri) - end) - - it("should return an error when no response_type is being sent", function() - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/authorize", { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid123", scope = "email" }, {host = "oauth2.com"}) - local body = cjson.decode(response) - assert.are.equal(400, status) - assert.are.equal(1, utils.table_size(body)) - assert.are.equal("http://google.com/kong?error=unsupported_response_type&error_description=Invalid%20response_type", body.redirect_uri) - end) - - it("should return an error with a state when no response_type is being sent", function() - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/authorize", { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid123", scope = "email", state = "somestate" }, {host = "oauth2.com"}) - local body = cjson.decode(response) - assert.are.equal(400, status) - assert.are.equal(1, utils.table_size(body)) - assert.are.equal("http://google.com/kong?error=unsupported_response_type&error_description=Invalid%20response_type&state=somestate", body.redirect_uri) - end) - - it("should return error when the redirect_uri does not match", function() - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/authorize", { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid123", scope = "email", response_type = "code", redirect_uri = "http://hello.com/" }, {host = "oauth2.com"}) - local body = cjson.decode(response) - assert.are.equal(400, status) - assert.are.equal(1, utils.table_size(body)) - assert.are.equal("http://google.com/kong?error=invalid_request&error_description=Invalid%20redirect_uri%20that%20does%20not%20match%20with%20any%20redirect_uri%20created%20with%20the%20application", body.redirect_uri) - end) - - it("should work even if redirect_uri contains a query string", function() - local response, status = http_client.post(PROXY_URL.."/oauth2/authorize", { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid789", scope = "email", response_type = "code" }, {host = "oauth2_6.com", ["X-Forwarded-Proto"] = "https"}) - local body = cjson.decode(response) - assert.are.equal(200, status) - assert.are.equal(1, utils.table_size(body)) - assert.truthy(rex.match(body.redirect_uri, "^http://google\\.com/kong\\?code=[\\w]{32,32}&foo=bar$")) - end) - - it("should fail when not under HTTPS", function() - local response, status = http_client.post(PROXY_URL.."/oauth2/authorize", { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid123", scope = "email", response_type = "code" }, {host = "oauth2.com"}) - local body = cjson.decode(response) - assert.are.equal(400, status) - assert.are.equal(2, utils.table_size(body)) - assert.are.equal("access_denied", body.error) - assert.are.equal("You must use HTTPS", body.error_description) - end) - - it("should work when not under HTTPS but accept_http_if_already_terminated is true", function() - local response, status = http_client.post(PROXY_URL.."/oauth2/authorize", { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid123", scope = "email", response_type = "code" }, {host = "oauth2_6.com", ["X-Forwarded-Proto"] = "https"}) - local body = cjson.decode(response) - assert.are.equal(200, status) - assert.are.equal(1, utils.table_size(body)) - assert.truthy(rex.match(body.redirect_uri, "^http://google\\.com/kong\\?code=[\\w]{32,32}$")) - end) - - it("should fail when not under HTTPS and accept_http_if_already_terminated is false", function() - local response, status = http_client.post(PROXY_URL.."/oauth2/authorize", { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid123", scope = "email", response_type = "code" }, {host = "oauth2.com", ["X-Forwarded-Proto"] = "https"}) - local body = cjson.decode(response) - assert.are.equal(400, status) - assert.are.equal(2, utils.table_size(body)) - assert.are.equal("access_denied", body.error) - assert.are.equal("You must use HTTPS", body.error_description) - end) - - it("should return success", function() - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/authorize", { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid123", scope = "email", response_type = "code" }, {host = "oauth2.com"}) - local body = cjson.decode(response) - assert.are.equal(200, status) - assert.are.equal(1, utils.table_size(body)) - assert.truthy(rex.match(body.redirect_uri, "^http://google\\.com/kong\\?code=[\\w]{32,32}$")) - end) - - it("should fail with a path when using the DNS", function() - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/authorize", { provision_key = "provision123a", authenticated_userid = "id123", client_id = "clientid123", scope = "email", response_type = "code" }, {host = "mockbin-path.com"}) - local body = cjson.decode(response) - assert.are.equal(400, status) - assert.are.equal(2, utils.table_size(body)) - assert.are.equal("invalid_provision_key", body.error) - assert.are.equal("Invalid Kong provision_key", body.error_description) - end) - - it("should return success with a path", function() - local response, status = http_client.post(PROXY_SSL_URL.."/somepath/oauth2/authorize", { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid123", scope = "email", response_type = "code" }, {host = "mockbin-path.com"}) - local body = cjson.decode(response) - assert.are.equal(200, status) - assert.are.equal(1, utils.table_size(body)) - assert.truthy(rex.match(body.redirect_uri, "^http://google\\.com/kong\\?code=[\\w]{32,32}$")) - end) - - it("should return success when requesting the url with final slash", function() - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/authorize/", { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid123", scope = "email", response_type = "code" }, {host = "oauth2.com"}) - local body = cjson.decode(response) - assert.are.equal(200, status) - assert.are.equal(1, utils.table_size(body)) - assert.truthy(rex.match(body.redirect_uri, "^http://google\\.com/kong\\?code=[\\w]{32,32}$")) - end) - - it("should return success with a state", function() - local response, status, headers = http_client.post(PROXY_SSL_URL.."/oauth2/authorize", { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid123", scope = "email", response_type = "code", state = "hello" }, {host = "oauth2.com"}) - local body = cjson.decode(response) - assert.are.equal(200, status) - assert.are.equal(1, utils.table_size(body)) - assert.truthy(rex.match(body.redirect_uri, "^http://google\\.com/kong\\?code=[\\w]{32,32}&state=hello$")) - - -- Checking headers - assert.are.equal("no-store", headers["cache-control"]) - assert.are.equal("no-cache", headers["pragma"]) - end) - - it("should return success and store authenticated user properties", function() - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/authorize", { provision_key = "provision123", client_id = "clientid123", scope = "email", response_type = "code", state = "hello", authenticated_userid = "userid123" }, {host = "oauth2.com"}) - local body = cjson.decode(response) - assert.are.equal(200, status) - assert.are.equal(1, utils.table_size(body)) - assert.truthy(rex.match(body.redirect_uri, "^http://google\\.com/kong\\?code=[\\w]{32,32}&state=hello$")) - - local matches = rex.gmatch(body.redirect_uri, "^http://google\\.com/kong\\?code=([\\w]{32,32})&state=hello$") - local code - for line in matches do - code = line - end - local data = dao_factory.oauth2_authorization_codes:find_all {code = code} - assert.are.equal(1, #data) - assert.are.equal(code, data[1].code) - - assert.are.equal("userid123", data[1].authenticated_userid) - assert.are.equal("email", data[1].scope) - end) - - it("should return success with a dotted scope and store authenticated user properties", function() - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/authorize", { provision_key = "provision123", client_id = "clientid123", scope = "user.email", response_type = "code", state = "hello", authenticated_userid = "userid123" }, {host = "oauth2.com"}) - local body = cjson.decode(response) - assert.are.equal(200, status) - assert.are.equal(1, utils.table_size(body)) - assert.truthy(rex.match(body.redirect_uri, "^http://google\\.com/kong\\?code=[\\w]{32,32}&state=hello$")) - - local matches = rex.gmatch(body.redirect_uri, "^http://google\\.com/kong\\?code=([\\w]{32,32})&state=hello$") - local code - for line in matches do - code = line - end - local data = dao_factory.oauth2_authorization_codes:find_all {code = code} - assert.are.equal(1, #data) - assert.are.equal(code, data[1].code) - - assert.are.equal("userid123", data[1].authenticated_userid) - assert.are.equal("user.email", data[1].scope) - end) - - end) - - describe("Implicit Grant", function() - - it("should return success", function() - local response, status, headers = http_client.post(PROXY_SSL_URL.."/oauth2/authorize", { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid123", scope = "email", response_type = "token" }, {host = "oauth2.com"}) - local body = cjson.decode(response) - assert.are.equal(200, status) - assert.are.equal(1, utils.table_size(body)) - assert.truthy(rex.match(body.redirect_uri, "^http://google\\.com/kong\\?access_token=[\\w]{32,32}&expires_in=[\\d]+&token_type=bearer$")) - - -- Checking headers - assert.are.equal("no-store", headers["cache-control"]) - assert.are.equal("no-cache", headers["pragma"]) - end) - - it("should return success and the state", function() - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/authorize", { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid123", scope = "email", response_type = "token", state = "wot" }, {host = "oauth2.com"}) - local body = cjson.decode(response) - assert.are.equal(200, status) - assert.are.equal(1, utils.table_size(body)) - assert.truthy(rex.match(body.redirect_uri, "^http://google\\.com/kong\\?access_token=[\\w]{32,32}&expires_in=[\\d]+&state=wot&token_type=bearer$")) - end) - - it("should return success and the token should have the right expiration", function() - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/authorize", { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid123", scope = "email", response_type = "token" }, {host = "oauth2.com"}) - local body = cjson.decode(response) - assert.are.equal(200, status) - assert.are.equal(1, utils.table_size(body)) - assert.truthy(rex.match(body.redirect_uri, "^http://google\\.com/kong\\?access_token=[\\w]{32,32}&expires_in=[\\d]+&token_type=bearer$")) - - local matches = rex.gmatch(body.redirect_uri, "^http://google\\.com/kong\\?access_token=([\\w]{32,32})&expires_in=[\\d]+&token_type=bearer$") - local access_token - for line in matches do - access_token = line - end - local data = dao_factory.oauth2_tokens:find_all {access_token = access_token} - assert.are.equal(1, #data) - assert.are.equal(access_token, data[1].access_token) - assert.are.equal(5, data[1].expires_in) - assert.falsy(data[1].refresh_token) - end) - - it("should return success and store authenticated user properties", function() - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/authorize", { provision_key = "provision123", client_id = "clientid123", scope = "email profile", response_type = "token", authenticated_userid = "userid123" }, {host = "oauth2.com"}) - local body = cjson.decode(response) - assert.are.equal(200, status) - assert.are.equal(1, utils.table_size(body)) - assert.truthy(rex.match(body.redirect_uri, "^http://google\\.com/kong\\?access_token=[\\w]{32,32}&expires_in=[\\d]+&token_type=bearer$")) - - local matches = rex.gmatch(body.redirect_uri, "^http://google\\.com/kong\\?access_token=([\\w]{32,32})&expires_in=[\\d]+&token_type=bearer$") - local access_token - for line in matches do - access_token = line - end - local data = dao_factory.oauth2_tokens:find_all {access_token = access_token} - assert.are.equal(1, #data) - assert.are.equal(access_token, data[1].access_token) - - assert.are.equal("userid123", data[1].authenticated_userid) - assert.are.equal("email profile", data[1].scope) - - -- Checking that there is no refresh token since it's an implicit grant - assert.are.equal(5, data[1].expires_in) - assert.falsy(data[1].refresh_token) - end) - - it("should return set the right upstream headers", function() - local response = http_client.post(PROXY_SSL_URL.."/oauth2/authorize", { provision_key = "provision123", client_id = "clientid123", scope = "email profile", response_type = "token", authenticated_userid = "userid123" }, {host = "oauth2.com"}) - local body = cjson.decode(response) - - local matches = rex.gmatch(body.redirect_uri, "^http://google\\.com/kong\\?access_token=([\\w]{32,32})&expires_in=[\\d]+&token_type=bearer$") - local access_token - for line in matches do - access_token = line - end - - local response, status = http_client.get(PROXY_SSL_URL.."/request", { access_token = access_token }, {host = "oauth2.com"}) - assert.are.equal(200, status) - - local body = cjson.decode(response) - assert.truthy(body.headers["x-consumer-id"]) - assert.are.equal("auth_tests_consumer", body.headers["x-consumer-username"]) - assert.are.equal("email profile", body.headers["x-authenticated-scope"]) - assert.are.equal("userid123", body.headers["x-authenticated-userid"]) - end) - - end) - - describe("Client Credentials", function() - - it("should return an error when client_secret is not sent", function() - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/token", { client_id = "clientid123", scope = "email", response_type = "token" }, {host = "oauth2_4.com"}) - local body = cjson.decode(response) - assert.are.equal(400, status) - assert.are.equal(2, utils.table_size(body)) - assert.are.equal("invalid_client", body.error) - assert.are.equal("Invalid client authentication", body.error_description) - end) - - it("should return an error when client_secret is not sent", function() - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/token", { client_id = "clientid123", client_secret="secret123", scope = "email", response_type = "token" }, {host = "oauth2_4.com"}) - local body = cjson.decode(response) - assert.are.equal(400, status) - assert.are.equal(2, utils.table_size(body)) - assert.are.equal("unsupported_grant_type", body.error) - assert.are.equal("Invalid grant_type", body.error_description) - end) - - it("should fail when not under HTTPS", function() - local response, status = http_client.post(PROXY_URL.."/oauth2/token", { client_id = "clientid123", client_secret="secret123", scope = "email", grant_type = "client_credentials" }, {host = "oauth2_4.com"}) - local body = cjson.decode(response) - assert.are.equal(400, status) - assert.are.equal(2, utils.table_size(body)) - assert.are.equal("access_denied", body.error) - assert.are.equal("You must use HTTPS", body.error_description) - end) - - it("should return fail when setting authenticated_userid and no provision_key", function() - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/token", { client_id = "clientid123", client_secret="secret123", scope = "email", grant_type = "client_credentials", authenticated_userid = "user123" }, {host = "oauth2_4.com"}) - local body = cjson.decode(response) - assert.are.equal(400, status) - assert.are.equal(2, utils.table_size(body)) - assert.are.equal("invalid_provision_key", body.error) - assert.are.equal("Invalid Kong provision_key", body.error_description) - end) - - it("should return fail when setting authenticated_userid and invalid provision_key", function() - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/token", { client_id = "clientid123", client_secret="secret123", scope = "email", grant_type = "client_credentials", authenticated_userid = "user123", provision_key = "hello" }, {host = "oauth2_4.com"}) - local body = cjson.decode(response) - assert.are.equal(400, status) - assert.are.equal(2, utils.table_size(body)) - assert.are.equal("invalid_provision_key", body.error) - assert.are.equal("Invalid Kong provision_key", body.error_description) - end) - - it("should return success", function() - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/token", { client_id = "clientid123", client_secret="secret123", scope = "email", grant_type = "client_credentials" }, {host = "oauth2_4.com"}) - local body = cjson.decode(response) - assert.are.equal(200, status) - assert.are.equals(3, utils.table_size(body)) - assert.falsy(body.refresh_token) - assert.truthy(body.access_token) - assert.are.equal("bearer", body.token_type) - assert.are.equal(5, body.expires_in) - end) - - it("should return success with authenticated_userid and valid provision_key", function() - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/token", { client_id = "clientid123", client_secret="secret123", scope = "email", grant_type = "client_credentials", authenticated_userid = "hello", provision_key = "provision123" }, {host = "oauth2_4.com"}) - local body = cjson.decode(response) - assert.are.equal(200, status) - assert.are.equals(3, utils.table_size(body)) - assert.falsy(body.refresh_token) - assert.truthy(body.access_token) - assert.are.equal("bearer", body.token_type) - assert.are.equal(5, body.expires_in) - end) - - it("should return success with authorization header", function() - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/token", { scope = "email", grant_type = "client_credentials" }, {host = "oauth2_4.com", authorization = "Basic Y2xpZW50aWQxMjM6c2VjcmV0MTIz"}) - local body = cjson.decode(response) - assert.are.equal(200, status) - assert.are.equals(3, utils.table_size(body)) - assert.falsy(body.refresh_token) - assert.truthy(body.access_token) - assert.are.equal("bearer", body.token_type) - assert.are.equal(5, body.expires_in) - end) - - it("should return an error with a wrong authorization header", function() - local response, status, headers = http_client.post(PROXY_SSL_URL.."/oauth2/token", { scope = "email", grant_type = "client_credentials" }, {host = "oauth2_4.com", authorization = "Basic Y2xpZW50aWQxMjM6c2VjcmV0MTI0"}) - local body = cjson.decode(response) - assert.are.equal(401, status) - assert.are.equal(2, utils.table_size(body)) - assert.are.equal("invalid_client", body.error) - assert.are.equal("Invalid client authentication", body.error_description) - assert.are.equal("Basic realm=\"OAuth2.0\"", headers["www-authenticate"]) - end) - - it("should return set the right upstream headers", function() - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/token", { client_id = "clientid123", client_secret="secret123", scope = "email", grant_type = "client_credentials", authenticated_userid = "hello", provision_key = "provision123" }, {host = "oauth2_4.com"}) - assert.are.equal(200, status) - - local response, status = http_client.get(PROXY_SSL_URL.."/request", { access_token = cjson.decode(response).access_token }, {host = "oauth2_4.com"}) - assert.are.equal(200, status) - - local body = cjson.decode(response) - assert.truthy(body.headers["x-consumer-id"]) - assert.are.equal("auth_tests_consumer", body.headers["x-consumer-username"]) - assert.are.equal("email", body.headers["x-authenticated-scope"]) - assert.are.equal("hello", body.headers["x-authenticated-userid"]) - end) - - it("should work in a multipart request", function() - local response, status = http_client.post_multipart(PROXY_SSL_URL.."/oauth2/token", { client_id = "clientid123", client_secret="secret123", scope = "email", grant_type = "client_credentials", authenticated_userid = "hello", provision_key = "provision123" }, {host = "oauth2_4.com"}) - assert.are.equal(200, status) - - local _, status = http_client.post_multipart(PROXY_SSL_URL.."/request", { access_token = cjson.decode(response).access_token }, {host = "oauth2_4.com"}) - assert.are.equal(200, status) - end) - - end) - - describe("Password Grant", function() - - it("should block unauthorized requests", function() - local response, status = http_client.get(PROXY_SSL_URL.."/request", {}, {host = "oauth2_5.com"}) - local body = cjson.decode(response) - assert.are.equal(401, status) - assert.are.equal("invalid_request", body.error) - assert.are.equal("The access token is missing", body.error_description) - end) - - it("should return an error when client_secret is not sent", function() - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/token", { client_id = "clientid123", scope = "email", response_type = "token" }, {host = "oauth2_5.com"}) - local body = cjson.decode(response) - assert.are.equal(400, status) - assert.are.equal(2, utils.table_size(body)) - assert.are.equal("invalid_client", body.error) - assert.are.equal("Invalid client authentication", body.error_description) - end) - - it("should return an error when client_secret is not sent", function() - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/token", { client_id = "clientid123", client_secret="secret123", scope = "email", response_type = "token" }, {host = "oauth2_5.com"}) - local body = cjson.decode(response) - assert.are.equal(400, status) - assert.are.equal(2, utils.table_size(body)) - assert.are.equal("unsupported_grant_type", body.error) - assert.are.equal("Invalid grant_type", body.error_description) - end) - - it("should fail when no provision key is being sent", function() - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/token", { client_id = "clientid123", client_secret="secret123", scope = "email", grant_type = "password" }, {host = "oauth2_5.com"}) - local body = cjson.decode(response) - assert.are.equal(400, status) - assert.are.equal(2, utils.table_size(body)) - assert.are.equal("invalid_provision_key", body.error) - assert.are.equal("Invalid Kong provision_key", body.error_description) - end) - - it("should fail when no provision key is being sent", function() - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/token", { client_id = "clientid123", client_secret="secret123", scope = "email", grant_type = "password" }, {host = "oauth2_5.com"}) - local body = cjson.decode(response) - assert.are.equal(400, status) - assert.are.equal(2, utils.table_size(body)) - assert.are.equal("invalid_provision_key", body.error) - assert.are.equal("Invalid Kong provision_key", body.error_description) - end) - - it("should fail when no authenticated user id is being sent", function() - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/token", { provision_key = "provision123", client_id = "clientid123", client_secret="secret123", scope = "email", grant_type = "password" }, {host = "oauth2_5.com"}) - local body = cjson.decode(response) - assert.are.equal(400, status) - assert.are.equal(2, utils.table_size(body)) - assert.are.equal("invalid_authenticated_userid", body.error) - assert.are.equal("Missing authenticated_userid parameter", body.error_description) - end) - - it("should return success", function() - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/token", { provision_key = "provision123", authenticated_userid = "id123", client_id = "clientid123", client_secret="secret123", scope = "email", grant_type = "password" }, {host = "oauth2_5.com"}) - local body = cjson.decode(response) - assert.are.equal(200, status) - assert.are.equals(4, utils.table_size(body)) - assert.truthy(body.refresh_token) - assert.truthy(body.access_token) - assert.are.equal("bearer", body.token_type) - assert.are.equal(5, body.expires_in) - end) - - it("should return success with authorization header", function() - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/token", { provision_key = "provision123", authenticated_userid = "id123", scope = "email", grant_type = "password" }, {host = "oauth2_5.com", authorization = "Basic Y2xpZW50aWQxMjM6c2VjcmV0MTIz"}) - local body = cjson.decode(response) - assert.are.equal(200, status) - assert.are.equals(4, utils.table_size(body)) - assert.truthy(body.refresh_token) - assert.truthy(body.access_token) - assert.are.equal("bearer", body.token_type) - assert.are.equal(5, body.expires_in) - end) - - it("should return an error with a wrong authorization header", function() - local response, status, headers = http_client.post(PROXY_SSL_URL.."/oauth2/token", { provision_key = "provision123", authenticated_userid = "id123", scope = "email", grant_type = "password" }, {host = "oauth2_5.com", authorization = "Basic Y2xpZW50aWQxMjM6c2VjcmV0MTI0"}) - local body = cjson.decode(response) - assert.are.equal(401, status) - assert.are.equal(2, utils.table_size(body)) - assert.are.equal("invalid_client", body.error) - assert.are.equal("Invalid client authentication", body.error_description) - assert.are.equal("Basic realm=\"OAuth2.0\"", headers["www-authenticate"]) - end) - - it("should return set the right upstream headers", function() - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/token", { provision_key = "provision123", authenticated_userid = "id123", scope = "email", grant_type = "password" }, {host = "oauth2_5.com", authorization = "Basic Y2xpZW50aWQxMjM6c2VjcmV0MTIz"}) - assert.are.equal(200, status) - - local response, status = http_client.get(PROXY_SSL_URL.."/request", { access_token = cjson.decode(response).access_token }, {host = "oauth2_5.com"}) - assert.are.equal(200, status) - - local body = cjson.decode(response) - assert.truthy(body.headers["x-consumer-id"]) - assert.are.equal("auth_tests_consumer", body.headers["x-consumer-username"]) - assert.are.equal("email", body.headers["x-authenticated-scope"]) - assert.are.equal("id123", body.headers["x-authenticated-userid"]) - end) - - end) - - end) - - describe("OAuth2 Access Token", function() - - it("should return an error when nothing is being sent", function() - local response, status, headers = http_client.post(PROXY_SSL_URL.."/oauth2/token", { }, {host = "oauth2.com"}) - local body = cjson.decode(response) - assert.are.equal(400, status) - assert.are.equal(2, utils.table_size(body)) - assert.are.equal("invalid_client", body.error) - assert.are.equal("Invalid client authentication", body.error_description) - - -- Checking headers - assert.are.equal("no-store", headers["cache-control"]) - assert.are.equal("no-cache", headers["pragma"]) - end) - - it("should return an error when only the code is being sent", function() - local code = provision_code() - - local response, status, headers = http_client.post(PROXY_SSL_URL.."/oauth2/token", { code = code }, {host = "oauth2.com"}) - local body = cjson.decode(response) - assert.are.equal(400, status) - assert.are.equal(2, utils.table_size(body)) - assert.are.equal("invalid_client", body.error) - assert.are.equal("Invalid client authentication", body.error_description) - - -- Checking headers - assert.are.equal("no-store", headers["cache-control"]) - assert.are.equal("no-cache", headers["pragma"]) - end) - - it("should return an error when only the code and client_secret are being sent", function() - local code = provision_code() - - local response, status, headers = http_client.post(PROXY_SSL_URL.."/oauth2/token", { code = code, client_secret = "secret123" }, {host = "oauth2.com"}) - local body = cjson.decode(response) - assert.are.equal(400, status) - assert.are.equal(2, utils.table_size(body)) - assert.are.equal("invalid_client", body.error) - assert.are.equal("Invalid client authentication", body.error_description) - - -- Checking headers - assert.are.equal("no-store", headers["cache-control"]) - assert.are.equal("no-cache", headers["pragma"]) - end) - - it("should return an error when only the code and client_secret and client_id are being sent", function() - local code = provision_code() - - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/token", { code = code, client_id = "clientid123", client_secret = "secret123" }, {host = "oauth2.com"}) - local body = cjson.decode(response) - assert.are.equal(400, status) - assert.are.equal(2, utils.table_size(body)) - assert.are.equal("unsupported_grant_type", body.error) - assert.are.equal("Invalid grant_type", body.error_description) - end) - - it("should return an error with a wrong code", function() - local code = provision_code() - - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/token", { code = code.."hello", client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code" }, {host = "oauth2.com"}) - local body = cjson.decode(response) - assert.are.equal(400, status) - assert.are.equal(2, utils.table_size(body)) - assert.are.equal("invalid_request", body.error) - assert.are.equal("Invalid code", body.error_description) - end) - - it("should return success without state", function() - local code = provision_code() - - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/token", { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code" }, {host = "oauth2.com"}) - local body = cjson.decode(response) - assert.are.equal(200, status) - assert.are.equals(4, utils.table_size(body)) - assert.truthy(body.refresh_token) - assert.truthy(body.access_token) - assert.are.equal("bearer", body.token_type) - assert.are.equal(5, body.expires_in) - end) - - it("should return success with state", function() - local code = provision_code() - - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/token", { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code", state = "wot" }, {host = "oauth2.com"}) - local body = cjson.decode(response) - assert.are.equal(200, status) - assert.are.equals(5, utils.table_size(body)) - assert.truthy(body.refresh_token) - assert.truthy(body.access_token) - assert.are.equal("bearer", body.token_type) - assert.are.equal(5, body.expires_in) - assert.are.equal("wot", body.state) - end) - - it("should return set the right upstream headers", function() - local code = provision_code() - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/token", { code = code, client_id = "clientid123", client_secret = "secret123", grant_type = "authorization_code" }, {host = "oauth2.com"}) - assert.are.equal(200, status) - - local response, status = http_client.get(PROXY_SSL_URL.."/request", { access_token = cjson.decode(response).access_token }, {host = "oauth2.com"}) - assert.are.equal(200, status) - - local body = cjson.decode(response) - assert.truthy(body.headers["x-consumer-id"]) - assert.are.equal("auth_tests_consumer", body.headers["x-consumer-username"]) - assert.are.equal("email", body.headers["x-authenticated-scope"]) - assert.are.equal("userid123", body.headers["x-authenticated-userid"]) - end) - end) - - describe("Making a request", function() - it("should work when a correct access_token is being sent in the querystring", function() - local token = provision_token() - local _, status = http_client.post(STUB_GET_URL, { access_token = token.access_token }, {host = "oauth2.com"}) - assert.are.equal(200, status) - end) - - it("should work when a correct access_token is being sent in a form body", function() - local token = provision_token() - local _, status = http_client.post(STUB_POST_URL, { access_token = token.access_token }, {host = "oauth2.com"}) - assert.are.equal(200, status) - end) - - it("should work when a correct access_token is being sent in an authorization header (bearer)", function() - local token = provision_token() - local _, status = http_client.post(STUB_POST_URL, { }, {host = "oauth2.com", authorization = "bearer "..token.access_token}) - assert.are.equal(200, status) - end) - - it("should work when a correct access_token is being sent in an authorization header (token)", function() - local token = provision_token() - local response, status = http_client.post(STUB_POST_URL, { }, {host = "oauth2.com", authorization = "token "..token.access_token}) - local body = cjson.decode(response) - assert.are.equal(200, status) - - local consumer = dao_factory.consumers:find_all({username = "auth_tests_consumer"})[1] - - assert.are.equal(consumer.id, body.headers["x-consumer-id"]) - assert.are.equal(consumer.username, body.headers["x-consumer-username"]) - assert.are.equal("userid123", body.headers["x-authenticated-userid"]) - assert.are.equal("email", body.headers["x-authenticated-scope"]) - end) - end) - - describe("Authentication challenge", function() - it("should return 401 Unauthorized without error if it lacks any authentication information", function() - local response, status, headers = http_client.post(STUB_GET_URL, { }, {host = "oauth2.com"}) - local body = cjson.decode(response) - assert.are.equal(401, status) - assert.are.equal('Bearer realm="service"', headers['www-authenticate']) - assert.are.equal("invalid_request", body.error) - assert.are.equal("The access token is missing", body.error_description) - end) - - it("should return 401 Unauthorized when an invalid access token is being sent via url parameter", function() - local response, status, headers = http_client.get(STUB_GET_URL, { access_token = "invalid" }, {host = "oauth2.com"}) - local body = cjson.decode(response) - assert.are.equal(401, status) - assert.are.equal('Bearer realm="service" error="invalid_token" error_description="The access token is invalid or has expired"', headers['www-authenticate']) - assert.are.equal("invalid_token", body.error) - assert.are.equal("The access token is invalid or has expired", body.error_description) - end) - - it("should return 401 Unauthorized when an invalid access token is being sent via the Authorization header", function() - local response, status, headers = http_client.post(STUB_POST_URL, { }, {host = "oauth2.com", authorization = "bearer invalid"}) - local body = cjson.decode(response) - assert.are.equal(401, status) - assert.are.equal('Bearer realm="service" error="invalid_token" error_description="The access token is invalid or has expired"', headers['www-authenticate']) - assert.are.equal("invalid_token", body.error) - assert.are.equal("The access token is invalid or has expired", body.error_description) - end) - - it("should return 401 Unauthorized when token has expired", function() - local token = provision_token() - - -- Token expires in (5 seconds) - os.execute("sleep 7") - - local response, status, headers = http_client.post(STUB_POST_URL, { }, {host = "oauth2.com", authorization = "bearer "..token.access_token}) - local body = cjson.decode(response) - assert.are.equal(401, status) - assert.are.equal(2, utils.table_size(body)) - assert.are.equal('Bearer realm="service" error="invalid_token" error_description="The access token is invalid or has expired"', headers['www-authenticate']) - assert.are.equal("invalid_token", body.error) - assert.are.equal("The access token is invalid or has expired", body.error_description) - end) - end) - - describe("Refresh Token", function() - - it("should not refresh an invalid access token", function() - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/token", { refresh_token = "hello", client_id = "clientid123", client_secret = "secret123", grant_type = "refresh_token" }, {host = "oauth2.com"}) - local body = cjson.decode(response) - assert.are.equal(400, status) - assert.are.equal(2, utils.table_size(body)) - assert.are.equal("invalid_request", body.error) - assert.are.equal("Invalid refresh_token", body.error_description) - end) - - it("should refresh an valid access token", function() - local token = provision_token() - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/token", { refresh_token = token.refresh_token, client_id = "clientid123", client_secret = "secret123", grant_type = "refresh_token" }, {host = "oauth2.com"}) - local body = cjson.decode(response) - assert.are.equal(200, status) - assert.are.equals(4, utils.table_size(body)) - assert.truthy(body.refresh_token) - assert.truthy(body.access_token) - assert.are.equal("bearer", body.token_type) - assert.are.equal(5, body.expires_in) - end) - - it("should expire after 5 seconds", function() - local token = provision_token() - local _, status = http_client.post(STUB_POST_URL, { }, {host = "oauth2.com", authorization = "bearer "..token.access_token}) - assert.are.equal(200, status) - - local id = dao_factory.oauth2_tokens:find_all({access_token = token.access_token })[1].id - assert.truthy(dao_factory.oauth2_tokens:find({id=id})) - - -- But waiting after the cache expiration (5 seconds) should block the request - os.execute("sleep 7") - - local response, status = http_client.post(STUB_POST_URL, { }, {host = "oauth2.com", authorization = "bearer "..token.access_token}) - local body = cjson.decode(response) - assert.are.equal(401, status) - assert.are.equal("The access token is invalid or has expired", body.error_description) - - -- Refreshing the token - local response, status = http_client.post(PROXY_SSL_URL.."/oauth2/token", { refresh_token = token.refresh_token, client_id = "clientid123", client_secret = "secret123", grant_type = "refresh_token" }, {host = "oauth2.com"}) - local body = cjson.decode(response) - assert.are.equal(200, status) - assert.are.equal(4, utils.table_size(body)) - assert.truthy(body.refresh_token) - assert.truthy(body.access_token) - assert.are.equal("bearer", body.token_type) - assert.are.equal(5, body.expires_in) - - assert.falsy(token.access_token == body.access_token) - assert.falsy(token.refresh_token == body.refresh_token) - - assert.falsy(dao_factory.oauth2_tokens:find({id=id})) - end) - - end) - - describe("Hide Credentials", function() - - it("should not hide credentials in the body", function() - local token = provision_token() - local response, status = http_client.post(STUB_POST_URL, { access_token = token.access_token }, {host = "oauth2.com"}) - local body = cjson.decode(response) - assert.are.equal(200, status) - assert.are.equal(token.access_token, body.postData.params.access_token) - end) - - it("should hide credentials in the body", function() - local token = provision_token() - local response, status = http_client.post(STUB_POST_URL, { access_token = token.access_token }, {host = "oauth2_3.com"}) - local body = cjson.decode(response) - assert.are.equal(200, status) - assert.falsy(body.postData.params.access_token) - end) - - it("should not hide credentials in the querystring", function() - local token = provision_token() - local response, status = http_client.get(STUB_GET_URL, { access_token = token.access_token }, {host = "oauth2.com"}) - local body = cjson.decode(response) - assert.are.equal(200, status) - assert.are.equal(token.access_token, body.queryString.access_token) - end) - - it("should hide credentials in the querystring", function() - local token = provision_token() - local response, status = http_client.get(STUB_GET_URL, { access_token = token.access_token }, {host = "oauth2_3.com"}) - local body = cjson.decode(response) - assert.are.equal(200, status) - assert.falsy(body.queryString.access_token) - end) - - it("should not hide credentials in the header", function() - local token = provision_token() - local response, status = http_client.get(STUB_GET_URL, {}, {host = "oauth2.com", authorization = "bearer "..token.access_token}) - local body = cjson.decode(response) - assert.are.equal(200, status) - assert.are.equal("bearer "..token.access_token, body.headers.authorization) - end) - - it("should hide credentials in the header", function() - local token = provision_token() - local response, status = http_client.get(STUB_GET_URL, {}, {host = "oauth2_3.com", authorization = "bearer "..token.access_token}) - local body = cjson.decode(response) - assert.are.equal(200, status) - assert.falsy(body.headers.authorization) - end) - - end) - -end)