diff --git a/kong/api/endpoints.lua b/kong/api/endpoints.lua index c40cbd6eecfd..a5d223f7f6e8 100644 --- a/kong/api/endpoints.lua +++ b/kong/api/endpoints.lua @@ -31,6 +31,7 @@ local ERRORS_HTTP_CODES = { [Errors.codes.INVALID_SIZE] = 400, [Errors.codes.INVALID_UNIQUE] = 400, [Errors.codes.INVALID_OPTIONS] = 400, + [Errors.codes.OPERATION_UNSUPPORTED] = 405, } @@ -113,8 +114,11 @@ local function handle_error(err_t) return app_helpers.yield_error(err_t) end - local body = utils.get_default_exit_body(status, err_t) - return kong.response.exit(status, body) + if err_t.code == Errors.codes.OPERATION_UNSUPPORTED then + return kong.response.exit(status, err_t) + end + + return kong.response.exit(status, utils.get_default_exit_body(status, err_t)) end diff --git a/kong/api/init.lua b/kong/api/init.lua index 72527ad964ef..ab0724c3c0f9 100644 --- a/kong/api/init.lua +++ b/kong/api/init.lua @@ -83,6 +83,11 @@ local function new_db_on_error(self) return kong.response.exit(404, err) end + if err.code == Errors.codes.OPERATION_UNSUPPORTED then + kong.log.err(err) + return kong.response.exit(405, err) + end + if err.code == Errors.codes.PRIMARY_KEY_VIOLATION or err.code == Errors.codes.UNIQUE_VIOLATION then diff --git a/kong/db/errors.lua b/kong/db/errors.lua index dd4cac114a86..25c1a06b7a2c 100644 --- a/kong/db/errors.lua +++ b/kong/db/errors.lua @@ -37,6 +37,7 @@ local ERRORS = { INVALID_SIZE = 9, -- page(size, offset) is invalid INVALID_UNIQUE = 10, -- unique field value is invalid INVALID_OPTIONS = 11, -- invalid options given + OPERATION_UNSUPPORTED = 12, -- operation is not supported with this strategy } @@ -55,6 +56,7 @@ local ERRORS_NAMES = { [ERRORS.INVALID_SIZE] = "invalid size", [ERRORS.INVALID_UNIQUE] = "invalid unique %s", [ERRORS.INVALID_OPTIONS] = "invalid options", + [ERRORS.OPERATION_UNSUPPORTED] = "operation unsupported", } @@ -394,4 +396,13 @@ function _M:invalid_options(errors) end +function _M:operation_unsupported(err) + if type(err) ~= "string" then + error("err must be a string", 2) + end + + return new_err_t(self, ERRORS.OPERATION_UNSUPPORTED, err) +end + + return _M diff --git a/kong/db/strategies/off/init.lua b/kong/db/strategies/off/init.lua index 9b6b34a77f18..d526cc26855a 100644 --- a/kong/db/strategies/off/init.lua +++ b/kong/db/strategies/off/init.lua @@ -1,6 +1,14 @@ local declarative_config = require("kong.db.schema.others.declarative_config") +local kong = kong +local fmt = string.format +local tostring = tostring +local tonumber = tonumber +local encode_base64 = ngx.encode_base64 +local decode_base64 = ngx.decode_base64 + + local off = {} @@ -9,7 +17,22 @@ _mt.__index = _mt local function page_for_key(self, key, size, offset) - offset = offset and tonumber(offset) or 1 + if offset then + local token = decode_base64(offset) + if not token then + return nil, self.errors:invalid_offset(offset, "bad base64 encoding") + end + + token = tonumber(token) + if not token then + return nil, self.errors:invalid_offset(offset, "invalid offset") + end + + offset = token + + else + offset = 1 + end if not kong.cache then return {} @@ -39,7 +62,7 @@ local function page_for_key(self, key, size, offset) end if offset then - return ret, nil, tostring(offset + size) + return ret, nil, encode_base64(tostring(offset + size), true) end return ret @@ -77,6 +100,20 @@ end function off.new(connector, schema, errors) + local unsupported = function(operation) + local err = fmt("cannot %s '%s' entities when not using a database", operation, schema.name) + return function() + return nil, errors:operation_unsupported(err) + end + end + + local unsupported_by = function(operation) + local err = fmt("cannot %s '%s' entities by '%s' when not using a database", operation, schema.name, '%s') + return function(_, field_name) + return nil, errors:operation_unsupported(fmt(err, field_name)) + end + end + local self = { connector = connector, -- instance of kong.db.strategies.off.connector schema = schema, @@ -84,6 +121,13 @@ function off.new(connector, schema, errors) page = page, select = select, select_by_field = select_by_field, + insert = unsupported("create"), + update = unsupported("update"), + upsert = unsupported("create or update"), + delete = unsupported("remove"), + update_by_field = unsupported_by("update"), + upsert_by_field = unsupported_by("create or update"), + delete_by_field = unsupported_by("remove"), truncate = function() return true end, } diff --git a/spec/02-integration/04-admin_api/14-off_spec.lua b/spec/02-integration/04-admin_api/14-off_spec.lua new file mode 100644 index 000000000000..d2747c7c3f03 --- /dev/null +++ b/spec/02-integration/04-admin_api/14-off_spec.lua @@ -0,0 +1,182 @@ +local cjson = require "cjson" +local utils = require "kong.tools.utils" +local helpers = require "spec.helpers" +local Errors = require "kong.db.errors" + + +local function it_content_types(title, fn) + local test_form_encoded = fn("application/x-www-form-urlencoded") + local test_multipart = fn("multipart/form-data") + local test_json = fn("application/json") + + it(title .. " with application/www-form-urlencoded", test_form_encoded) + it(title .. " with multipart/form-data", test_multipart) + it(title .. " with application/json", test_json) +end + +describe("Admin API #off", function() + local client + + lazy_setup(function() + assert(helpers.start_kong({ + database = "off", + })) + end) + + lazy_teardown(function() + helpers.stop_kong(nil, true) + end) + + before_each(function() + client = assert(helpers.admin_client()) + end) + + after_each(function() + if client then + client:close() + end + end) + + describe("/routes", function() + describe("POST", function() + it_content_types("doesn't allow to creates a route", function(content_type) + return function() + if content_type == "multipart/form-data" then + -- the client doesn't play well with this + return + end + + local res = client:post("/routes", { + body = { + protocols = { "http" }, + hosts = { "my.route.com" }, + service = { id = utils.uuid() }, + }, + headers = { ["Content-Type"] = content_type } + }) + local body = assert.res_status(405, res) + local json = cjson.decode(body) + assert.same({ + code = Errors.codes.OPERATION_UNSUPPORTED, + name = Errors.names[Errors.codes.OPERATION_UNSUPPORTED], + message = "cannot create 'routes' entities when not using a database", + }, json) + end + end) + + it_content_types("doesn't allow to creates a complex route", function(content_type) + return function() + if content_type == "multipart/form-data" then + -- the client doesn't play well with this + return + end + + local res = client:post("/routes", { + body = { + protocols = { "http" }, + methods = { "GET", "POST", "PATCH" }, + hosts = { "foo.api.com", "bar.api.com" }, + paths = { "/foo", "/bar" }, + service = { id = utils.uuid() }, + }, + headers = { ["Content-Type"] = content_type } + }) + + local body = assert.res_status(405, res) + local json = cjson.decode(body) + assert.same({ + code = Errors.codes.OPERATION_UNSUPPORTED, + name = Errors.names[Errors.codes.OPERATION_UNSUPPORTED], + message = "cannot create 'routes' entities when not using a database", + }, json) + end + end) + end) + + describe("GET", function() + describe("errors", function() + it("handles invalid offsets", function() + local res = client:get("/routes", { query = { offset = "x" } }) + local body = assert.res_status(400, res) + assert.same({ + code = Errors.codes.INVALID_OFFSET, + name = "invalid offset", + message = "'x' is not a valid offset: bad base64 encoding" + }, cjson.decode(body)) + + res = client:get("/routes", { query = { offset = "|potato|" } }) + body = assert.res_status(400, res) + + local json = cjson.decode(body) + json.message = nil + + assert.same({ + code = Errors.codes.INVALID_OFFSET, + name = "invalid offset", + }, json) + end) + end) + end) + + it("returns HTTP 405 on invalid method", function() + local methods = { "DELETE", "PUT", "PATCH", "POST" } + for i = 1, #methods do + local res = assert(client:send { + method = methods[i], + path = "/routes", + body = { + paths = { "/" }, + service = { id = utils.uuid() } + }, + headers = { + ["Content-Type"] = "application/json" + } + }) + local body = assert.response(res).has.status(405) + local json = cjson.decode(body) + if methods[i] == "POST" then + assert.same({ + code = Errors.codes.OPERATION_UNSUPPORTED, + name = Errors.names[Errors.codes.OPERATION_UNSUPPORTED], + message = "cannot create 'routes' entities when not using a database", + }, json) + + else + assert.same({ message = "Method not allowed" }, json) + end + end + end) + end) + + describe("/routes/{route}", function() + it("returns HTTP 405 on invalid method", function() + local methods = { "PUT", "POST" } + for i = 1, #methods do + local res = assert(client:send { + method = methods[i], + path = "/routes/" .. utils.uuid(), + body = { + paths = { "/" }, + service = { id = utils.uuid() } + }, + headers = { + ["Content-Type"] = "application/json" + } + }) + + local body = assert.response(res).has.status(405) + local json = cjson.decode(body) + if methods[i] ~= "POST" then + assert.same({ + code = Errors.codes.OPERATION_UNSUPPORTED, + name = Errors.names[Errors.codes.OPERATION_UNSUPPORTED], + message = "cannot create or update 'routes' entities when not using a database", + }, json) + + else + assert.same({ message = "Method not allowed" }, json) + end + end + end) + end) +end)