Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api) restrict data manipulation operations on 'off' db with errors #4308

Merged
merged 3 commits into from
Feb 15, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions kong/api/endpoints.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}


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


Expand Down
5 changes: 5 additions & 0 deletions kong/api/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions kong/db/errors.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
}


Expand All @@ -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",
}


Expand Down Expand Up @@ -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
48 changes: 46 additions & 2 deletions kong/db/strategies/off/init.lua
Original file line number Diff line number Diff line change
@@ -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 = {}


Expand All @@ -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 {}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -77,13 +100,34 @@ 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,
errors = 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,
}

Expand Down
182 changes: 182 additions & 0 deletions spec/02-integration/04-admin_api/14-off_spec.lua
Original file line number Diff line number Diff line change
@@ -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)