From a6dee10696e7fc23b305cf301ba41ecdd4a66683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Garc=C3=ADa=20Cota?= Date: Sat, 17 Mar 2018 01:58:06 +0100 Subject: [PATCH] feat(db) move ssl_cert schema to new db --- kong-0.13.1-0.rockspec | 6 +- kong/api/init.lua | 3 +- kong/api/routes/certificates.lua | 323 +---- kong/api/routes/snis.lua | 55 +- kong/dao/factory.lua | 2 - kong/dao/migrations/cassandra.lua | 115 +- kong/dao/migrations/helpers.lua | 6 +- kong/dao/migrations/postgres.lua | 92 +- kong/dao/schemas/ssl_certificates.lua | 15 - kong/dao/schemas/ssl_servers_names.lua | 14 - kong/db/dao/certificates.lua | 225 +++ kong/db/dao/init.lua | 49 +- kong/db/dao/snis.lua | 166 +++ kong/db/errors.lua | 16 +- kong/db/init.lua | 30 +- kong/db/schema/entities/certificates.lua | 15 + kong/db/schema/entities/snis.lua | 16 + kong/runloop/certificate.lua | 40 +- kong/runloop/handler.lua | 26 +- .../06-certificates_routes_spec.lua | 1274 ---------------- .../02-integration/05-proxy/05-ssl_spec.lua | 2 +- .../02-core_entities_invalidations_spec.lua | 8 +- .../01-helpers/01-blueprints_spec.lua | 14 +- .../03-dao/04-constraints_spec.lua | 4 +- .../06-certificates_routes_spec.lua | 1290 +++++------------ spec/02-integration/05-proxy/05-ssl_spec.lua | 2 +- .../02-core_entities_invalidations_spec.lua | 113 +- spec/fixtures/blueprints.lua | 9 +- 28 files changed, 1205 insertions(+), 2725 deletions(-) delete mode 100644 kong/dao/schemas/ssl_certificates.lua delete mode 100644 kong/dao/schemas/ssl_servers_names.lua create mode 100644 kong/db/dao/certificates.lua create mode 100644 kong/db/dao/snis.lua create mode 100644 kong/db/schema/entities/certificates.lua create mode 100644 kong/db/schema/entities/snis.lua delete mode 100644 spec-old-api/02-integration/04-admin_api/06-certificates_routes_spec.lua diff --git a/kong-0.13.1-0.rockspec b/kong-0.13.1-0.rockspec index bd8780fc7b89..d3723bbcc4df 100644 --- a/kong-0.13.1-0.rockspec +++ b/kong-0.13.1-0.rockspec @@ -112,8 +112,6 @@ build = { ["kong.dao.schemas.plugins"] = "kong/dao/schemas/plugins.lua", ["kong.dao.schemas.upstreams"] = "kong/dao/schemas/upstreams.lua", ["kong.dao.schemas.targets"] = "kong/dao/schemas/targets.lua", - ["kong.dao.schemas.ssl_certificates"] = "kong/dao/schemas/ssl_certificates.lua", - ["kong.dao.schemas.ssl_servers_names"] = "kong/dao/schemas/ssl_servers_names.lua", ["kong.dao.db"] = "kong/dao/db/init.lua", ["kong.dao.db.cassandra"] = "kong/dao/db/cassandra.lua", ["kong.dao.db.postgres"] = "kong/dao/db/postgres.lua", @@ -127,9 +125,13 @@ build = { ["kong.db"] = "kong/db/init.lua", ["kong.db.errors"] = "kong/db/errors.lua", ["kong.db.dao"] = "kong/db/dao/init.lua", + ["kong.db.dao.certificates"] = "kong/db/dao/certificates.lua", + ["kong.db.dao.snis"] = "kong/db/dao/snis.lua", ["kong.db.schema"] = "kong/db/schema/init.lua", ["kong.db.schema.entities.routes"] = "kong/db/schema/entities/routes.lua", ["kong.db.schema.entities.services"] = "kong/db/schema/entities/services.lua", + ["kong.db.schema.entities.certificates"] = "kong/db/schema/entities/certificates.lua", + ["kong.db.schema.entities.snis"] = "kong/db/schema/entities/snis.lua", ["kong.db.schema.entity"] = "kong/db/schema/entity.lua", ["kong.db.schema.metaschema"] = "kong/db/schema/metaschema.lua", ["kong.db.schema.typedefs"] = "kong/db/schema/typedefs.lua", diff --git a/kong/api/init.lua b/kong/api/init.lua index 7635cd3ff9e2..61b4f1a2e389 100644 --- a/kong/api/init.lua +++ b/kong/api/init.lua @@ -218,8 +218,7 @@ ngx.log(ngx.DEBUG, "Loading Admin API endpoints") -- Load core routes -for _, v in ipairs({"kong", "apis", "consumers", "plugins", "cache", - "certificates", "snis", "upstreams"}) do +for _, v in ipairs({"kong", "apis", "consumers", "plugins", "cache", "upstreams"}) do local routes = require("kong.api.routes." .. v) attach_routes(routes) end diff --git a/kong/api/routes/certificates.lua b/kong/api/routes/certificates.lua index 0f8e1bbb4442..7f8747dcbd9d 100644 --- a/kong/api/routes/certificates.lua +++ b/kong/api/routes/certificates.lua @@ -1,317 +1,44 @@ -local crud = require "kong.api.crud_helpers" -local utils = require "kong.tools.utils" -local cjson = require "cjson" +local endpoints = require "kong.api.endpoints" +local utils = require "kong.tools.utils" +local responses = require "kong.tools.responses" -local function create_certificate(self, dao_factory, helpers) - local snis - if type(self.params.snis) == "string" then - snis = utils.split(self.params.snis, ",") - end - - self.params.snis = nil - - if snis then - -- dont add the certificate or any snis if we have an SNI conflict - -- its fairly inefficient that we have to loop twice over the datastore - -- but no support for OR queries means we gotsta! - local snis_in_request = {} - - for _, sni in ipairs(snis) do - if snis_in_request[sni] then - return helpers.responses.send_HTTP_CONFLICT("duplicate SNI in " .. - "request: " .. sni) - end - - local cnt, err = dao_factory.ssl_servers_names:count { - name = sni, - } - if err then - return helpers.yield_error(err) - end - - if cnt > 0 then - -- Note: it could be that the SNI is not associated with any - -- certificate, but we don't handle this case. (for PostgreSQL - -- only, as C* requires a cert_id for its partition key). - return helpers.responses.send_HTTP_CONFLICT("SNI already exists: " .. - sni) - end - - snis_in_request[sni] = true - end - end - - local ssl_cert, err = dao_factory.ssl_certificates:insert(self.params) - if err then - return helpers.yield_error(err) - end - - ssl_cert.snis = setmetatable({}, cjson.empty_array_mt) - - -- insert SNIs if given - - if snis then - for i, sni in ipairs(snis) do - local ssl_server_name = { - name = sni, - ssl_certificate_id = ssl_cert.id, - } - - local row, err = dao_factory.ssl_servers_names:insert(ssl_server_name) - if err then - return helpers.yield_error(err) - end - - ssl_cert.snis[i] = row.name +local function get_cert_id_from_sni(self, db, helpers) + local id = self.params.certificates + if not utils.is_valid_uuid(id) then + local sni, _, err_t = db.snis:select_by_name(id) + if err_t then + return endpoints.handle_error(err_t) end - end - - return helpers.responses.send_HTTP_CREATED(ssl_cert) -end - - -local function update_certificate(self, dao_factory, helpers) - -- check if exists - local ssl_cert, err = dao_factory.ssl_certificates:find { - id = self.params.id - } - if err then - return helpers.yield_error(err) - end - - if not ssl_cert then - return helpers.responses.send_HTTP_NOT_FOUND() - end - - local snis - if type(self.params.snis) == "string" then - snis = utils.split(self.params.snis, ",") - - elseif self.params.snis == ngx.null then - snis = {} - end - - self.params.snis = nil - - local snis_in_request = {} -- check for duplicate snis in the request - local snis_in_db = {} -- avoid db insert if sni is already present in db - - -- if snis field present - -- 1. no duplicate snis should be present in the request - -- 2. check if any sni in the request is using a cert - -- other than the one being updated - - if snis then - for _, sni in ipairs(snis) do - if snis_in_request[sni] then - return helpers.responses.send_HTTP_CONFLICT("duplicate SNI in " .. - "request: " .. sni) - end - - local sni_in_db, err = dao_factory.ssl_servers_names:find { - name = sni, - } - if err then - return helpers.yield_error(err) - end - - if sni_in_db then - if sni_in_db.ssl_certificate_id ~= ssl_cert.id then - return helpers.responses.send_HTTP_CONFLICT( - "SNI '" .. sni .. "' already associated with existing " .. - "certificate (" .. sni_in_db.ssl_certificate_id .. ")" - ) - end - - snis_in_db[sni] = true - end - - snis_in_request[sni] = true - end - end - - local old_snis, err = dao_factory.ssl_servers_names:find_all { - ssl_certificate_id = ssl_cert.id, - } - if err then - return helpers.yield_error(err) - end - - -- update certificate if necessary - if self.params.key or self.params.cert then - self.params.created_at = ssl_cert.created_at - - ssl_cert, err = dao_factory.ssl_certificates:update(self.params, { - id = self.params.id, - }, { full = true }) - if err then - return helpers.yield_error(err) - end - end - - ssl_cert.snis = setmetatable({}, cjson.empty_array_mt) - if not snis then - for i = 1, #old_snis do - ssl_cert.snis[i] = old_snis[i].name + if not sni then + responses.send_HTTP_NOT_FOUND("SNI not found") end - return helpers.responses.send_HTTP_OK(ssl_cert) + self.params.certificates = sni.certificate.id end - - -- insert/delete SNIs into db if snis field was present in the request - for i, sni in ipairs(snis) do - if not snis_in_db[sni] then - local ssl_server_name = { - name = sni, - ssl_certificate_id = ssl_cert.id, - } - - local _, err = dao_factory.ssl_servers_names:insert(ssl_server_name) - if err then - return helpers.yield_error(err) - end - end - - ssl_cert.snis[i] = sni - end - - -- delete snis which should no longer use ssl_cert - for i = 1, #old_snis do - if not snis_in_request[old_snis[i].name] then - -- ignoring error - -- if we want to return an error here - -- to return 4xx here, the current transaction needs to be - -- rolled back else we risk an invalid state and confusing - -- the user - dao_factory.ssl_servers_names:delete({ - name = old_snis[i].name, - }) - end - end - - return helpers.responses.send_HTTP_OK(ssl_cert) end return { - ["/certificates/"] = { - POST = function(self, dao_factory, helpers) - create_certificate(self, dao_factory, helpers) - end, + ["/certificates/:certificates"] = { + before = get_cert_id_from_sni, + -- override to include the snis list when getting an individual certificate + GET = function(self, db, helpers) + local pk = { id = self.params.certificates } - GET = function(self, dao_factory, helpers) - local ssl_certificates, err = dao_factory.ssl_certificates:find_all() - if err then - return helpers.yield_error(err) + local cert, _, err_t = db.certificates:select_with_name_list(pk) + if err_t then + return endpoints.handle_error(err_t) end - for i = 1, #ssl_certificates do - local rows, err = dao_factory.ssl_servers_names:find_all { - ssl_certificate_id = ssl_certificates[i].id - } - if err then - return helpers.yield_error(err) - end - - ssl_certificates[i].snis = setmetatable({}, cjson.empty_array_mt) - - for j = 1, #rows do - ssl_certificates[i].snis[j] = rows[j].name - end - end - - return helpers.responses.send_HTTP_OK({ - data = #ssl_certificates > 0 and ssl_certificates or cjson.empty_array, - total = #ssl_certificates, - }) - end, - - - PUT = function(self, dao_factory, helpers) - -- no id present, behaviour should be same as POST - if not self.params.id then - create_certificate(self, dao_factory, helpers) - return -- avoid tail call - end - - -- id present in body - update_certificate(self, dao_factory, helpers) + return helpers.responses.send_HTTP_OK(cert) end, }, - - ["/certificates/:sni_or_uuid"] = { - before = function(self, dao_factory, helpers) - if utils.is_valid_uuid(self.params.sni_or_uuid) then - self.ssl_certificate_id = self.params.sni_or_uuid - - else - -- get requested SNI - - local row, err = dao_factory.ssl_servers_names:find { - name = self.params.sni_or_uuid - } - if err then - return helpers.yield_error(err) - end - - if not row then - return helpers.responses.send_HTTP_NOT_FOUND() - end - - -- cache certificate row id - - self.ssl_certificate_id = row.ssl_certificate_id - end - - self.params.sni_or_uuid = nil - end, - - - GET = function(self, dao_factory, helpers) - local row, err = dao_factory.ssl_certificates:find { - id = self.ssl_certificate_id - } - if err then - return helpers.yield_error(err) - end - - if not row then - return helpers.responses.send_HTTP_NOT_FOUND() - end - - -- add list of other SNIs for this certificate - - row.snis = setmetatable({}, cjson.empty_array_mt) - - local rows, err = dao_factory.ssl_servers_names:find_all { - ssl_certificate_id = self.ssl_certificate_id - } - if err then - return helpers.yield_error(err) - end - - for i = 1, #rows do - row.snis[i] = rows[i].name - end - - return helpers.responses.send_HTTP_OK(row) - end, - - - PATCH = function(self, dao_factory, helpers) - self.params.id = self.ssl_certificate_id - update_certificate(self, dao_factory, helpers) - end, - - - DELETE = function(self, dao_factory, helpers) - return crud.delete({ - id = self.ssl_certificate_id - }, dao_factory.ssl_certificates) - end, - } + ["/certificates/:certificates/snis"] = { + before = get_cert_id_from_sni, + }, } + diff --git a/kong/api/routes/snis.lua b/kong/api/routes/snis.lua index cce7e66ab961..69195c42d8aa 100644 --- a/kong/api/routes/snis.lua +++ b/kong/api/routes/snis.lua @@ -1,53 +1,8 @@ -local crud = require "kong.api.crud_helpers" - - return { - ["/snis/"] = { - GET = function(self, dao_factory) - crud.paginated_set(self, dao_factory.ssl_servers_names) - end, - - - PUT = function(self, dao_factory) - crud.put(self.params, dao_factory.ssl_servers_names) - end, - - - POST = function(self, dao_factory) - crud.post(self.params, dao_factory.ssl_servers_names) - end, + -- deactivate endpoint (use /certificates/sni instead) + ["/snis/:snis/certificate"] = { + before = function(self, db, helpers) + return helpers.responses.send_HTTP_NOT_FOUND() + end }, - - - ["/snis/:name"] = { - before = function(self, dao_factory, helpers) - local row, err = dao_factory.ssl_servers_names:find { - name = self.params.name - } - if err then - return helpers.yield_error(err) - end - - if not row then - return helpers.responses.send_HTTP_NOT_FOUND() - end - - self.sni = row - end, - - - GET = function(self, dao_factory, helpers) - return helpers.responses.send_HTTP_OK(self.sni) - end, - - - PATCH = function(self, dao_factory) - crud.patch(self.params, dao_factory.ssl_servers_names, self.sni) - end, - - - DELETE = function(self, dao_factory) - crud.delete(self.sni, dao_factory.ssl_servers_names) - end, - } } diff --git a/kong/dao/factory.lua b/kong/dao/factory.lua index 8e97b899dd3e..de480047c4a8 100644 --- a/kong/dao/factory.lua +++ b/kong/dao/factory.lua @@ -11,8 +11,6 @@ local CORE_MODELS = { "apis", "consumers", "plugins", - "ssl_certificates", - "ssl_servers_names", "upstreams", "targets", } diff --git a/kong/dao/migrations/cassandra.lua b/kong/dao/migrations/cassandra.lua index 6c86592e6e31..e5ac8d568f42 100644 --- a/kong/dao/migrations/cassandra.lua +++ b/kong/dao/migrations/cassandra.lua @@ -1,5 +1,8 @@ local log = require "kong.cmd.utils.log" +local cassandra = require "cassandra" +local utils = require "kong.tools.utils" +local migration_helpers = require "kong.dao.migrations.helpers" return { { @@ -240,7 +243,7 @@ return { { name = "2016-12-14-172100_move_ssl_certs_to_core", up = [[ - CREATE TABLE ssl_certificates( + CREATE TABLE IF NOT EXISTS ssl_certificates( id uuid PRIMARY KEY, cert text, key text , @@ -628,5 +631,115 @@ return { CREATE INDEX IF NOT EXISTS ON targets(target); ]], down = nil + }, + { + name = "2018-03-22-141700_create_new_ssl_tables", + up = [[ + CREATE TABLE IF NOT EXISTS certificates( + partition text, + id uuid, + cert text, + key text, + created_at timestamp, + PRIMARY KEY (partition, id) + ); + + CREATE TABLE IF NOT EXISTS snis( + partition text, + id uuid, + name text, + certificate_id uuid, + created_at timestamp, + PRIMARY KEY (partition, id) + ); + + CREATE INDEX IF NOT EXISTS snis_name_idx ON snis(name); + CREATE INDEX IF NOT EXISTS snis_certificate_id_idx ON snis(certificate_id); + ]], + down = nil + }, + { + name = "2018-03-26-234600_copy_records_to_new_ssl_tables", + up = function(_, _, dao) + local ssl_certificates_def = { + name = "ssl_certificates", + columns = { + id = "uuid", + cert = "text", + key = "text", + created_at = "timestamp", + }, + partition_keys = { "id" }, + } + + local certificates_def = { + name = "certificates", + columns = { + partition = "text", + id = "uuid", + cert = "text", + key = "text", + created_at = "timestamp", + }, + partition_keys = { "partition", "id" }, + } + + local _, err = migration_helpers.cassandra.copy_records(dao, + ssl_certificates_def, + certificates_def, { + partition = function() return cassandra.text("certificates") end, + id = "id", + cert = "cert", + key = "key", + created_at = "created_at", + }) + if err then + return err + end + + local ssl_servers_names_def = { + name = "ssl_servers_names", + columns = { + name = "text", + ssl_certificate_id = "uuid", + created_at = "timestamp", + }, + partition_keys = { "name", "ssl_certificate_id" }, + } + + local snis_def = { + name = "snis", + columns = { + partition = "text", + id = "uuid", + name = "text", + certificate_id = "uuid", + created_at = "timestamp", + }, + partition_keys = { "partition", "id" }, + } + + local _, err = migration_helpers.cassandra.copy_records(dao, + ssl_servers_names_def, + snis_def, { + partition = function() return cassandra.text("snis") end, + id = function() return cassandra.uuid(utils.uuid()) end, + name = "name", + certificate_id = "ssl_certificate_id", + created_at = "created_at", + }) + if err then + return err + end + end, + down = nil + }, + { name = "2018-03-27-002500_drop_old_ssl_tables", + up = [[ + DROP INDEX ssl_servers_names_ssl_certificate_id_idx; + DROP TABLE ssl_certificates; + DROP TABLE ssl_servers_names; + ]], + down = nil } } diff --git a/kong/dao/migrations/helpers.lua b/kong/dao/migrations/helpers.lua index 41cbe09f5643..14ec945f4700 100644 --- a/kong/dao/migrations/helpers.lua +++ b/kong/dao/migrations/helpers.lua @@ -3,13 +3,13 @@ local cassandra = require("cassandra") local utils = require "kong.tools.utils" -local _M = {} - - local fmt = string.format local table_concat = table.concat +local _M = {} + + -- Iterator to update plugin configurations. -- It works indepedent of the underlying datastore. -- @param dao the dao to use diff --git a/kong/dao/migrations/postgres.lua b/kong/dao/migrations/postgres.lua index 6ec70a85308b..24f918eeb45b 100644 --- a/kong/dao/migrations/postgres.lua +++ b/kong/dao/migrations/postgres.lua @@ -1,3 +1,6 @@ +local utils = require "kong.tools.utils" + + return { { name = "2015-01-12-175310_skeleton", @@ -199,14 +202,14 @@ return { { name = "2016-12-14-172100_move_ssl_certs_to_core", up = [[ - CREATE TABLE ssl_certificates( + CREATE TABLE IF NOT EXISTS ssl_certificates( id uuid PRIMARY KEY, cert text , key text , created_at timestamp without time zone default (CURRENT_TIMESTAMP(0) at time zone 'utc') ); - CREATE TABLE ssl_servers_names( + CREATE TABLE IF NOT EXISTS ssl_servers_names( name text PRIMARY KEY, ssl_certificate_id uuid REFERENCES ssl_certificates(id) ON DELETE CASCADE, created_at timestamp without time zone default (CURRENT_TIMESTAMP(0) at time zone 'utc') @@ -692,4 +695,89 @@ return { ]], down = nil }, + { + name = "2018-03-27-123400_prepare_certs_and_snis", + up = [[ + DO $$ + BEGIN + ALTER TABLE ssl_certificates RENAME TO certificates; + ALTER TABLE ssl_servers_names RENAME TO snis; + EXCEPTION WHEN duplicate_table THEN + -- Do nothing, accept existing state + END$$; + + DO $$ + BEGIN + ALTER TABLE snis RENAME COLUMN ssl_certificate_id TO certificate_id; + ALTER TABLE snis ADD COLUMN id uuid; + EXCEPTION WHEN undefined_column THEN + -- Do nothing, accept existing state + END$$; + + DO $$ + BEGIN + ALTER TABLE snis ALTER COLUMN created_at TYPE timestamp with time zone + USING created_at AT time zone 'UTC'; + ALTER TABLE certificates ALTER COLUMN created_at TYPE timestamp with time zone + USING created_at AT time zone 'UTC'; + EXCEPTION WHEN undefined_column THEN + -- Do nothing, accept existing state + END$$; + ]], + down = nil + }, + { + name = "2018-03-27-125400_fill_in_snis_ids", + up = function(_, _, dao) + local fmt = string.format + + local rows, err = dao.db:query([[ + SELECT * FROM snis; + ]]) + if err then + return err + end + local sql_buffer = { "BEGIN;" } + local len = #rows + for i = 1, len do + sql_buffer[i + 1] = fmt("UPDATE snis SET id = '%s' WHERE name = '%s';", + utils.uuid(), + rows[i].name) + end + sql_buffer[len + 2] = "COMMIT;" + + local _, err = dao.db:query(table.concat(sql_buffer)) + if err then + return err + end + end, + down = nil + }, + { + name = "2018-03-27-130400_make_ids_primary_keys_in_snis", + up = [[ + ALTER TABLE snis + DROP CONSTRAINT IF EXISTS ssl_servers_names_pkey; + + ALTER TABLE snis + DROP CONSTRAINT IF EXISTS ssl_servers_names_ssl_certificate_id_fkey; + + DO $$ + BEGIN + ALTER TABLE snis + ADD CONSTRAINT snis_name_unique UNIQUE(name); + + ALTER TABLE snis + ADD PRIMARY KEY (id); + + ALTER TABLE snis + ADD CONSTRAINT snis_certificate_id_fkey + FOREIGN KEY (certificate_id) + REFERENCES certificates; + EXCEPTION WHEN duplicate_table THEN + -- Do nothing, accept existing state + END$$; + ]], + down = nil + }, } diff --git a/kong/dao/schemas/ssl_certificates.lua b/kong/dao/schemas/ssl_certificates.lua deleted file mode 100644 index 4b87c092177f..000000000000 --- a/kong/dao/schemas/ssl_certificates.lua +++ /dev/null @@ -1,15 +0,0 @@ -return { - table = "ssl_certificates", - primary_key = { "id" }, - fields = { - id = { type = "id", dao_insert_value = true, required = true }, - cert = { type = "string", required = true }, - key = { type = "string", required = true }, - created_at = { - type = "timestamp", - immutable = true, - dao_insert_value = true, - required = true, - }, - }, -} diff --git a/kong/dao/schemas/ssl_servers_names.lua b/kong/dao/schemas/ssl_servers_names.lua deleted file mode 100644 index f3b6c7e636ab..000000000000 --- a/kong/dao/schemas/ssl_servers_names.lua +++ /dev/null @@ -1,14 +0,0 @@ -return { - table = "ssl_servers_names", - primary_key = { "name" }, - fields = { - name = { type = "text", required = true, unique = true }, - ssl_certificate_id = { type = "id", foreign = "ssl_certificates:id" }, - created_at = { - type = "timestamp", - immutable = true, - dao_insert_value = true, - required = true, - }, - }, -} diff --git a/kong/db/dao/certificates.lua b/kong/db/dao/certificates.lua new file mode 100644 index 000000000000..21398c00ca43 --- /dev/null +++ b/kong/db/dao/certificates.lua @@ -0,0 +1,225 @@ +local cjson = require "cjson" +local utils = require "kong.tools.utils" + + +-- Get an array of SNI names from either +-- an array(sort) or ngx.null(return {}) +-- Returns an error if the list has duplicates +-- Returns nil if input is falsy. +local function parse_name_list(input, errors) + local name_list + if type(input) == "table" then + name_list = utils.shallow_copy(input) + + elseif input == ngx.null then + name_list = {} + + else + return nil + end + + local found = {} + for _, name in ipairs(name_list) do + if found[name] then + local err_t = errors:schema_violation({ + snis = name .. " is duplicated", + }) + return nil, tostring(err_t), err_t + end + found[name] = true + end + + table.sort(name_list) + return setmetatable(name_list, cjson.empty_array_mt) +end + + +local _Certificates = {} + +-- Creates a certificate +-- If the provided cert has a field called "snis" it will be used to generate server +-- names associated to the cert, after being parsed by parse_name_list. +-- Returns a certificate with the snis sorted alphabetically. +function _Certificates:insert(cert) + local name_list, err, err_t = parse_name_list(cert.snis, self.errors) + if err then + return nil, err, err_t + end + + if name_list then + local ok, err, err_t = self.db.snis:check_list_is_new(name_list) + if not ok then + return nil, err, err_t + end + end + + cert.snis = nil + cert, err, err_t = self.super.insert(self, cert) + if not cert then + return nil, err, err_t + end + cert.snis = name_list or cjson.empty_array + + if name_list then + local ok, err, err_t = self.db.snis:insert_list({id = cert.id}, name_list) + if not ok then + return nil, err, err_t + end + end + + return cert +end + +-- Update override +-- If the cert has a "snis" attribute it will be used to update the SNIs +-- associated to the cert. +-- * If the cert had any names associated which are not on `snis`, they will be +-- removed. +-- * Any new certificates will be added to the db. +-- Returns an error if any of the new certificates where already assigned to a cert different +-- from the one identified by cert_pk +function _Certificates:update(cert_pk, cert) + local name_list, err, err_t = parse_name_list(cert.snis, self.errors) + if err then + return nil, err, err_t + end + + if name_list then + local ok, err, err_t = + self.db.snis:check_list_is_new(name_list, cert_pk.id) + if not ok then + return nil, err, err_t + end + end + + -- update certificate if necessary + if cert.key or cert.cert then + cert.snis = nil + cert, err, err_t = self.super.update(self, cert_pk, cert) + if err then + return nil, err, err_t + end + end + + if name_list then + cert.snis = name_list + + local ok, err, err_t = self.db.snis:update_list(cert_pk, name_list) + if not ok then + return nil, err, err_t + end + + else + cert.snis, err, err_t = self.db.snis:list_for_certificate(cert_pk) + if not cert.snis then + return nil, err, err_t + end + end + + return cert +end + +-- Upsert override +function _Certificates:upsert(cert_pk, cert) + local name_list, err, err_t = parse_name_list(cert.snis, self.errors) + if err then + return nil, err, err_t + end + + if name_list then + local ok, err, err_t = + self.db.snis:check_list_is_new(name_list, cert_pk.id) + if not ok then + return nil, err, err_t + end + end + + cert.snis = nil + cert, err, err_t = self.super.upsert(self, cert_pk, cert) + if err then + return nil, err, err_t + end + + if name_list then + cert.snis = name_list + + local ok, err, err_t = self.db.snis:update_list(cert_pk, name_list) + if not ok then + return nil, err, err_t + end + + else + cert.snis, err, err_t = self.db.snis:list_for_certificate(cert_pk) + if not cert.snis then + return nil, err, err_t + end + end + + return cert +end + + +-- Returns the certificate identified by cert_pk but adds the +-- `snis` pseudo attribute to it. It is an array of strings +-- representing the SNIs associated to the certificate. +function _Certificates:select_with_name_list(cert_pk) + local cert, err, err_t = self:select(cert_pk) + if err_t then + return nil, err, err_t + end + + if not cert then + local err_t = self.errors:not_found(cert_pk) + return nil, tostring(err_t), err_t + end + + cert.snis, err, err_t = self.db.snis:list_for_certificate(cert_pk) + if err_t then + return nil, err, err_t + end + + return cert +end + +-- Returns a page of certificates, each with the `snis` pseudo-attribute +-- associated to them. This method does N+1 queries, but for now we are limited +-- by the DAO's select options (we can't query for "all the SNIs for this +-- list of certificate ids" in one go). +function _Certificates:page(size, offset) + local certs, err, err_t, offset = self.super.page(self, size, offset) + if not certs then + return nil, err, err_t + end + + for i=1, #certs do + local cert = certs[i] + local snis, err, err_t = + self.db.snis:list_for_certificate({ id = cert.id }) + if not snis then + return nil, err, err_t + end + cert.snis = snis + end + + return certs, nil, nil, offset +end + +-- Overrides the default delete function by cascading-deleting all the SNIs +-- associated to the certificate +function _Certificates:delete(cert_pk) + local name_list, err, err_t = + self.db.snis:list_for_certificate(cert_pk) + if not name_list then + return nil, err, err_t + end + + local ok, err, err_t = self.db.snis:delete_list(name_list) + if not ok then + return nil, err, err_t + end + + return self.super.delete(self, cert_pk) +end + + +return _Certificates diff --git a/kong/db/dao/init.lua b/kong/db/dao/init.lua index 467b1df81150..84fac65341d2 100644 --- a/kong/db/dao/init.lua +++ b/kong/db/dao/init.lua @@ -29,14 +29,14 @@ local DAO = {} DAO.__index = DAO -local function generate_foreign_key_methods(self) - local schema = self.schema +local function generate_foreign_key_methods(schema) + local methods = {} for name, field in schema:each_field() do if field.type == "foreign" then local method_name = "for_" .. name - self[method_name] = function(self, foreign_key, size, offset) + methods[method_name] = function(self, foreign_key, size, offset) if type(foreign_key) ~= "table" then error("foreign_key must be a table", 2) end @@ -85,13 +85,13 @@ local function generate_foreign_key_methods(self) elseif field.unique then local function validate_unique_value(unique_value) - local ok, err = self.schema:validate_field(field, unique_value) + local ok, err = schema:validate_field(field, unique_value) if not ok then error("invalid argument '" .. name .. "' (" .. err .. ")", 3) end end - self["select_by_" .. name] = function(self, unique_value) + methods["select_by_" .. name] = function(self, unique_value) validate_unique_value(unique_value) local row, err_t = self.strategy:select_by_field(name, unique_value) @@ -106,7 +106,7 @@ local function generate_foreign_key_methods(self) return self:row_to_entity(row) end - self["update_by_" .. name] = function(self, unique_value, entity) + methods["update_by_" .. name] = function(self, unique_value, entity) validate_unique_value(unique_value) local entity_to_update, err = self.schema:process_auto_fields(entity, "update") @@ -137,7 +137,7 @@ local function generate_foreign_key_methods(self) return row end - self["upsert_by_" .. name] = function(self, unique_value, entity) + methods["upsert_by_" .. name] = function(self, unique_value, entity) validate_unique_value(unique_value) local entity_to_upsert, err = self.schema:process_auto_fields(entity, "upsert") @@ -168,28 +168,43 @@ local function generate_foreign_key_methods(self) return row end - self["delete_by_" .. name] = function(self, unique_value) + methods["delete_by_" .. name] = function(self, unique_value) validate_unique_value(unique_value) + local entity, err, err_t = self["select_by_" .. name](self, unique_value) + if err then + return nil, err, err_t + end + if not entity then + return true + end + local _, err_t = self.strategy:delete_by_field(name, unique_value) if err_t then return nil, tostring(err_t), err_t end - self:post_crud_event("delete") + self:post_crud_event("delete", entity) return true end end end + + return methods end -function _M.new(schema, strategy, errors) +function _M.new(db, schema, strategy, errors) + local fk_methods = generate_foreign_key_methods(schema) + local super = setmetatable(fk_methods, DAO) + local self = { + db = db, schema = schema, strategy = strategy, errors = errors, + super = super, } if schema.dao then @@ -199,9 +214,7 @@ function _M.new(schema, strategy, errors) end end - generate_foreign_key_methods(self) - - return setmetatable(self, DAO) + return setmetatable(self, { __index = super }) end @@ -427,12 +440,20 @@ function DAO:delete(primary_key) return nil, tostring(err_t), err_t end + local entity, err, err_t = self:select(primary_key) + if err then + return nil, err, err_t + end + if not entity then + return true + end + local _, err_t = self.strategy:delete(primary_key) if err_t then return nil, tostring(err_t), err_t end - self:post_crud_event("delete") + self:post_crud_event("delete", entity) return true end diff --git a/kong/db/dao/snis.lua b/kong/db/dao/snis.lua new file mode 100644 index 000000000000..1545a2f067d7 --- /dev/null +++ b/kong/db/dao/snis.lua @@ -0,0 +1,166 @@ +local cjson = require "cjson" +local Set = require "pl.Set" + + +local function invalidate_cache(self, old_entity, err, err_t) + if err then + return nil, err, err_t + end + if old_entity then + self:post_crud_event("update", old_entity) + end +end + + +local _SNIs = {} + + +-- Truthy if all the names on the list don't exist on the db or exist but are +-- associated to the given certificate +-- if the cert id is nil, all encountered snis will return an error +function _SNIs:check_list_is_new(name_list, valid_cert_id) + for i=1, #name_list do + local name = name_list[i] + local row, err, err_t = self:select_by_name(name) + if err then + return nil, err, err_t + end + if row and row.certificate.id ~= valid_cert_id then + local msg = name .. + " already associated with existing " .. + "certificate '" .. row.certificate.id .. "'" + local err_t = self.errors:schema_violation({ snis = msg }) + return nil, tostring(err_t), err_t + end + end + + return true +end + + +-- Creates one instance of SNI for each name in name_list +-- All created instances will be associated to the given certificate +function _SNIs:insert_list(cert_pk, name_list) + for _, name in ipairs(name_list) do + local _, err, err_t = self:insert({ + name = name, + certificate = cert_pk, + }) + if err then + return nil, err, err_t + end + end + + return true +end + + +-- Deletes all SNIs on the given name list +function _SNIs:delete_list(name_list) + local err_list = {} + local errors_len = 0 + local first_err_t = nil + for i = 1, #name_list do + local ok, err, err_t = self:delete_by_name(name_list[i]) + if not ok then + errors_len = errors_len + 1 + err_list[errors_len] = err + first_err_t = first_err_t or err_t + end + end + + if errors_len > 0 then + return nil, table.concat(err_list, ","), first_err_t + end + + return true +end + + +-- Returns the name list for a given certificate +function _SNIs:list_for_certificate(cert_pk) + local name_list = setmetatable({}, cjson.empty_array_mt) + local rows, err, err_t = self:for_certificate(cert_pk) + if err then + return nil, err, err_t + end + for i = 1, #rows do + name_list[i] = rows[i].name + end + + table.sort(name_list) + return name_list +end + + +-- Replaces the names of a given certificate +-- It does not try to insert SNIs which are already inserted +-- It does not try to delete SNIs which don't exist +function _SNIs:update_list(cert_pk, new_list) + -- Get the names currently associated to the certificate + local current_list, err, err_t = self:list_for_certificate(cert_pk) + if not current_list then + return nil, err, err_t + end + + local delete_list = Set.values(Set(current_list) - Set(new_list)) + local insert_list = Set.values(Set(new_list) - Set(current_list)) + + local ok, err, err_t = self:insert_list(cert_pk, insert_list) + if not ok then + return nil, err, err_t + end + + -- ignoring errors here + -- returning 4xx here risks invalid states and is confusing to the user + self:delete_list(delete_list) + + return true +end + + +-- invalidates the *old* name when updating it to a new name +function _SNIs:update(pk, entity) + local _, err, err_t = invalidate_cache(self, self:select(pk)) + if err then + return nil, err, err_t + end + + return self.super.update(self, pk, entity) +end + + +-- invalidates the *old* name when updating it to a new name +function _SNIs:update_by_name(name, entity) + local _, err, err_t = invalidate_cache(self, self:select_by_name(name)) + if err then + return nil, err, err_t + end + + return self.super.update_by_name(self, name, entity) +end + + +-- invalidates the *old* name when updating it to a new name +function _SNIs:upsert(pk, entity) + local _, err, err_t = invalidate_cache(self, self:select(pk)) + if err then + return nil, err, err_t + end + + return self.super.upsert(self, pk, entity) +end + + +-- invalidates the *old* name when updating it to a new name +function _SNIs:upsert_by_name(name, entity) + local _, err, err_t = invalidate_cache(self, self:select_by_name(name)) + if err then + return nil, err, err_t + end + + return self.super.upsert_by_name(self, name, entity) +end + + +return _SNIs diff --git a/kong/db/errors.lua b/kong/db/errors.lua index e28d7824325f..401ac4806232 100644 --- a/kong/db/errors.lua +++ b/kong/db/errors.lua @@ -24,14 +24,14 @@ end local ERRORS = { - INVALID_PRIMARY_KEY = 1, - SCHEMA_VIOLATION = 2, - PRIMARY_KEY_VIOLATION = 3, -- primary key already exists (HTTP 400) - FOREIGN_KEY_VIOLATION = 4, -- foreign entity does not exist (HTTP 400) - UNIQUE_VIOLATION = 5, -- unique key already exists (HTTP 409) - NOT_FOUND = 6, -- WHERE clause leads nowhere (HTTP 404) - INVALID_OFFSET = 7, -- page(size, offset) is invalid - DATABASE_ERROR = 8, -- connection refused or DB error (HTTP 500) + INVALID_PRIMARY_KEY = 1, + SCHEMA_VIOLATION = 2, + PRIMARY_KEY_VIOLATION = 3, -- primary key already exists (HTTP 400) + FOREIGN_KEY_VIOLATION = 4, -- foreign entity does not exist (HTTP 400) + UNIQUE_VIOLATION = 5, -- unique key already exists (HTTP 409) + NOT_FOUND = 6, -- WHERE clause leads nowhere (HTTP 404) + INVALID_OFFSET = 7, -- page(size, offset) is invalid + DATABASE_ERROR = 8, -- connection refused or DB error (HTTP 500) } diff --git a/kong/db/init.lua b/kong/db/init.lua index 3c00849d302e..d8d2cd4d9615 100644 --- a/kong/db/init.lua +++ b/kong/db/init.lua @@ -18,8 +18,10 @@ local setmetatable = setmetatable -- to schemas and entities since schemas will also be used -- independently from the DB module (Admin API for GUI) local CORE_ENTITIES = { - "services", "routes", + "services", + "certificates", + "snis", } @@ -38,6 +40,10 @@ function DB.new(kong_config, strategy) error("strategy must be a string", 2) end + -- load errors + + local errors = Errors.new(strategy) + local schemas = {} do @@ -49,20 +55,16 @@ function DB.new(kong_config, strategy) local entity_schema = require("kong.db.schema.entities." .. entity_name) -- validate core entities schema via metaschema - local ok, err = MetaSchema:validate(entity_schema) + local ok, err_t = MetaSchema:validate(entity_schema) if not ok then return nil, fmt("schema of entity '%s' is invalid: %s", entity_name, - err) + tostring(errors:schema_violation(err_t))) end schemas[entity_name] = Entity.new(entity_schema) end end - -- load errors - - local errors = Errors.new(strategy) - -- load strategy local connector, strategies, err = Strategies.new(kong_config, strategy, @@ -73,6 +75,13 @@ function DB.new(kong_config, strategy) local daos = {} + + local self = { + daos = daos, -- each of those has the connector singleton + strategies = strategies, + connector = connector, + } + do -- load DAOs @@ -82,17 +91,12 @@ function DB.new(kong_config, strategy) return nil, fmt("no strategy found for schema '%s'", schema.name) end - daos[schema.name] = DAO.new(schema, strategy, errors) + daos[schema.name] = DAO.new(self, schema, strategy, errors) end end -- we are 200 OK - local self = { - daos = daos, -- each of those has the connector singleton - strategies = strategies, - connector = connector, - } return setmetatable(self, DB) end diff --git a/kong/db/schema/entities/certificates.lua b/kong/db/schema/entities/certificates.lua new file mode 100644 index 000000000000..e2f8fb7abed4 --- /dev/null +++ b/kong/db/schema/entities/certificates.lua @@ -0,0 +1,15 @@ +local typedefs = require "kong.db.schema.typedefs" + +return { + name = "certificates", + primary_key = { "id" }, + dao = "kong.db.dao.certificates", + + fields = { + { id = typedefs.uuid, }, + { created_at = { type = "integer", timestamp = true, auto = true }, }, + { cert = { type = "string", required = true}, }, + { key = { type = "string", required = true}, }, + }, + +} diff --git a/kong/db/schema/entities/snis.lua b/kong/db/schema/entities/snis.lua new file mode 100644 index 000000000000..caaef91bc76c --- /dev/null +++ b/kong/db/schema/entities/snis.lua @@ -0,0 +1,16 @@ +local typedefs = require "kong.db.schema.typedefs" + +return { + name = "snis", + primary_key = { "id" }, + endpoint_key = "name", + dao = "kong.db.dao.snis", + + fields = { + { id = typedefs.uuid, }, + { name = { type = "string", required = true, unique = true }, }, + { created_at = { type = "integer", timestamp = true, auto = true }, }, + { certificate = { type = "foreign", reference = "certificates", required = true }, }, + }, + +} diff --git a/kong/runloop/certificate.lua b/kong/runloop/certificate.lua index f087fd067066..4515d09ced49 100644 --- a/kong/runloop/certificate.lua +++ b/kong/runloop/certificate.lua @@ -15,62 +15,58 @@ end local _M = {} -local function find_certificate(sni) - local row, err = singletons.dao.ssl_servers_names:find { - name = sni - } +local function find_certificate(sni_name) + local row, err = singletons.db.snis:select_by_name(sni_name) if err then return nil, err end if not row then - log(DEBUG, "no server name registered for client-provided SNI: '", - sni, "'") + log(DEBUG, "no SNI registered for client-provided name: '", + sni_name, "'") return true end - -- fetch SSL certificate for this SNI + -- fetch SSL certificate for this sni - local ssl_certificate, err = singletons.dao.ssl_certificates:find { - id = row.ssl_certificate_id - } + local certificate, err = singletons.db.certificates:select(row.certificate) if err then return nil, err end - if not ssl_certificate then - return nil, "no SSL certificate configured for server name: " .. sni + if not certificate then + return nil, "no SSL certificate configured for sni: " .. sni_name end return { - cert = ssl_certificate.cert, - key = ssl_certificate.key, + cert = certificate.cert, + key = certificate.key, } end function _M.execute() - -- retrieve SNI or raw server IP + -- retrieve sni or raw server IP - local sni, err = ssl.server_name() + local sn, err = ssl.server_name() if err then - log(ERR, "could not retrieve Server Name Indication: ", err) + log(ERR, "could not retrieve SNI: ", err) return ngx.exit(ngx.ERROR) end - if not sni then - log(DEBUG, "no Server Name Indication provided by client, serving ", + if not sn then + log(DEBUG, "no SNI provided by client, serving ", "default proxy SSL certificate") -- use fallback certificate return end local lru = singletons.cache.mlcache.lru - local pem_cache_key = "pem_ssl_certificates:" .. sni - local parsed_cache_key = "parsed_ssl_certificates:" .. sni + local pem_cache_key = "pem_ssl_certificates:" .. sn + local parsed_cache_key = "parsed_ssl_certificates:" .. sn local pem_cert_and_key, err = singletons.cache:get(pem_cache_key, nil, - find_certificate, sni) + find_certificate, sn) if not pem_cert_and_key then log(ERR, err) return ngx.exit(ngx.ERROR) diff --git a/kong/runloop/handler.lua b/kong/runloop/handler.lua index 7d44bd62c477..ddc017489779 100644 --- a/kong/runloop/handler.lua +++ b/kong/runloop/handler.lua @@ -154,7 +154,7 @@ return { reports.init_worker() -- initialize local local_events hooks - + local db = singletons.db local dao = singletons.dao local cache = singletons.cache local worker_events = singletons.worker_events @@ -255,32 +255,32 @@ return { worker_events.register(function(data) log(DEBUG, "[events] SNI updated, invalidating cached certificates") - local sni = data.entity + local sn = data.entity - cache:invalidate("pem_ssl_certificates:" .. sni.name) - cache:invalidate("parsed_ssl_certificates:" .. sni.name) - end, "crud", "ssl_servers_names") + cache:invalidate("pem_ssl_certificates:" .. sn.name) + cache:invalidate("parsed_ssl_certificates:" .. sn.name) + end, "crud", "snis") worker_events.register(function(data) log(DEBUG, "[events] SSL cert updated, invalidating cached certificates") local certificate = data.entity - local rows, err = dao.ssl_servers_names:find_all { - ssl_certificate_id = certificate.id - } + local rows, err = db.snis:for_certificate({ + id = certificate.id + }) if not rows then - log(ERR, "[events] could not find associated SNIs for certificate: ", + log(ERR, "[events] could not find associated snis for certificate: ", err) end for i = 1, #rows do - local sni = rows[i] + local sn = rows[i] - cache:invalidate("pem_ssl_certificates:" .. sni.name) - cache:invalidate("parsed_ssl_certificates:" .. sni.name) + cache:invalidate("pem_ssl_certificates:" .. sn.name) + cache:invalidate("parsed_ssl_certificates:" .. sn.name) end - end, "crud", "ssl_certificates") + end, "crud", "certificates") -- target updates diff --git a/spec-old-api/02-integration/04-admin_api/06-certificates_routes_spec.lua b/spec-old-api/02-integration/04-admin_api/06-certificates_routes_spec.lua deleted file mode 100644 index b301048071ab..000000000000 --- a/spec-old-api/02-integration/04-admin_api/06-certificates_routes_spec.lua +++ /dev/null @@ -1,1274 +0,0 @@ -local ssl_fixtures = require "spec-old-api.fixtures.ssl" -local dao_helpers = require "spec.02-integration.03-dao.helpers" -local DAOFactory = require "kong.dao.factory" -local helpers = require "spec.helpers" -local cjson = require "cjson" -local utils = require "kong.tools.utils" - - -local function it_content_types(title, fn) - local test_form_encoded = fn("application/x-www-form-urlencoded") - local test_json = fn("application/json") - it(title .. " with application/www-form-urlencoded", test_form_encoded) - it(title .. " with application/json", test_json) -end - - -dao_helpers.for_each_dao(function(kong_config) - -describe("Admin API: #" .. kong_config.database, function() - local client - local dao - - before_each(function() - client = assert(helpers.admin_client()) - end) - - setup(function() - dao = assert(DAOFactory.new(kong_config)) - assert(dao:run_migrations()) - - assert(helpers.start_kong({ - database = kong_config.database - })) - end) - - teardown(function() - if client then - client:close() - end - - helpers.stop_kong() - end) - - describe("/certificates", function() - before_each(function() - dao:truncate_tables() - local res = assert(client:send { - method = "POST", - path = "/certificates", - body = { - cert = ssl_fixtures.cert, - key = ssl_fixtures.key, - snis = "foo.com,bar.com", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, - }) - - assert.res_status(201, res) - end) - - describe("GET", function() - it("retrieves all certificates", function() - local res = assert(client:send { - method = "GET", - path = "/certificates", - }) - - local body = assert.res_status(200, res) - local json = cjson.decode(body) - assert.equal(1, json.total) - assert.equal(1, #json.data) - assert.is_string(json.data[1].cert) - assert.is_string(json.data[1].key) - assert.same({ "foo.com", "bar.com" }, json.data[1].snis) - end) - end) - - describe("POST", function() - it("returns a conflict when duplicates snis are present in the request", function() - local res = assert(client:send { - method = "POST", - path = "/certificates", - body = { - cert = ssl_fixtures.cert, - key = ssl_fixtures.key, - snis = "foobar.com,baz.com,foobar.com", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, - }) - - local body = assert.res_status(409, res) - local json = cjson.decode(body) - assert.equals("duplicate SNI in request: foobar.com", json.message) - - -- make sure we dont add any snis - res = assert(client:send { - method = "GET", - path = "/snis", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, #json.data) - assert.equal(2, json.total) - - -- make sure we didnt add the certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(1, #json.data) - assert.equal(1, json.total) - end) - - it("returns a conflict when a pre-existing sni is detected", function() - local res = assert(client:send { - method = "POST", - path = "/certificates", - body = { - cert = ssl_fixtures.cert, - key = ssl_fixtures.key, - snis = "foo.com", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, - }) - - local body = assert.res_status(409, res) - local json = cjson.decode(body) - assert.equals("SNI already exists: foo.com", json.message) - - -- make sure we only have two snis - res = assert(client:send { - method = "GET", - path = "/snis", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, #json.data) - assert.equal(2, json.total) - assert.equal("foo.com", json.data[1].name) - assert.equal("bar.com", json.data[2].name) - - -- make sure we only have one certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(1, json.total) - assert.equal(1, #json.data) - assert.is_string(json.data[1].cert) - assert.is_string(json.data[1].key) - assert.same({ "foo.com", "bar.com" }, json.data[1].snis) - end) - - it_content_types("creates a certificate", function(content_type) - return function() - local res = assert(client:send { - method = "POST", - path = "/certificates", - body = { - cert = ssl_fixtures.cert, - key = ssl_fixtures.key, - snis = "foobar.com,baz.com", - }, - headers = { ["Content-Type"] = content_type }, - }) - - local body = assert.res_status(201, res) - local json = cjson.decode(body) - assert.is_string(json.cert) - assert.is_string(json.key) - assert.same({ "foobar.com", "baz.com" }, json.snis) - end - end) - - it_content_types("returns snis as [] when none is set", function(content_type) - return function() - local res = assert(client:send { - method = "POST", - path = "/certificates", - body = { - cert = ssl_fixtures.cert, - key = ssl_fixtures.key, - }, - headers = { ["Content-Type"] = content_type }, - }) - - local body = assert.res_status(201, res) - local json = cjson.decode(body) - assert.is_string(json.cert) - assert.is_string(json.key) - assert.matches('"snis":[]', body, nil, true) - end - end) - end) - - describe("PUT", function() - local cert_foo - local cert_bar - - before_each(function() - dao:truncate_tables() - - local res = assert(client:send { - method = "POST", - path = "/certificates", - body = { - cert = ssl_fixtures.cert, - key = ssl_fixtures.key, - snis = "foo.com", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, - }) - local body = assert.res_status(201, res) - cert_foo = cjson.decode(body) - - local res = assert(client:send { - method = "POST", - path = "/certificates", - body = { - cert = ssl_fixtures.cert, - key = ssl_fixtures.key, - snis = "bar.com", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, - }) - local body = assert.res_status(201, res) - cert_bar = cjson.decode(body) - end) - - it("creates a certificate if ID is not present in the body", function() - local res = assert(client:send { - method = "PUT", - path = "/certificates", - body = { - cert = ssl_fixtures.cert, - key = ssl_fixtures.key, - snis = "baz.com", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, - }) - - local body = assert.res_status(201, res) - local json = cjson.decode(body) - - assert.is_string(json.cert) - assert.is_string(json.key) - assert.same({ "baz.com" }, json.snis) - - -- make sure we added an sni - res = assert(client:send { - method = "GET", - path = "/snis", - }) - - body = assert.res_status(200, res) - local json = cjson.decode(body) - assert.equal(3, #json.data) - assert.equal(3, json.total) - - -- make sure we added our certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) - - local body = assert.res_status(200, res) - local json = cjson.decode(body) - assert.equal(3, #json.data) - assert.equal(3, json.total) - end) - - it("returns 404 for a random non-existing id", function() - local res = assert(client:send { - method = "PUT", - path = "/certificates", - body = { - id = utils.uuid(), - cert = ssl_fixtures.cert, - key = ssl_fixtures.key, - snis = "baz.com", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, - }) - - assert.res_status(404, res) - - -- make sure we did not add any sni - res = assert(client:send { - method = "GET", - path = "/snis", - }) - - local body = assert.res_status(200, res) - local json = cjson.decode(body) - assert.equal(2, #json.data) - assert.equal(2, json.total) - - -- make sure we did not add any certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, json.total) - assert.equal(2, #json.data) - end) - - it("returns Bad Request if only certificate is specified", function() - local res = assert(client:send { - method = "PUT", - path = "/certificates", - body = { - id = cert_foo.id, - cert = "cert_foo", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, - }) - - local body = assert.res_status(400, res) - local json = cjson.decode(body) - assert.equals("key is required", json.key) - - -- make sure we did not add any sni - res = assert(client:send { - method = "GET", - path = "/snis", - }) - - local body = assert.res_status(200, res) - local json = cjson.decode(body) - assert.equal(2, #json.data) - assert.equal(2, json.total) - - -- make sure we did not add any certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, json.total) - assert.equal(2, #json.data) - end) - - it("returns Bad Request if only key is specified", function() - local res = assert(client:send { - method = "PUT", - path = "/certificates", - body = { - id = cert_foo.id, - key = "key_foo", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, - }) - - local body = assert.res_status(400, res) - local json = cjson.decode(body) - assert.equals("cert is required", json.cert) - - -- make sure we did not add any sni - res = assert(client:send { - method = "GET", - path = "/snis", - }) - - local body = assert.res_status(200, res) - local json = cjson.decode(body) - assert.equal(2, #json.data) - assert.equal(2, json.total) - - -- make sure we did not add any certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, json.total) - assert.equal(2, #json.data) - end) - - it("updates snis associated with a certificate", function() - local res = assert(client:send { - method = "PUT", - path = "/certificates", - body = { - id = cert_foo.id, - snis = "baz.com", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, - }) - - local body = assert.res_status(200, res) - local json = cjson.decode(body) - - assert.same({ "baz.com" }, json.snis) - - -- make sure number of snis don't change - -- since we delete foo.com and added baz.com - res = assert(client:send { - method = "GET", - path = "/snis", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, #json.data) - assert.equal(2, json.total) - local sni_names = {} - table.insert(sni_names, json.data[1].name) - table.insert(sni_names, json.data[2].name) - assert.are.same({ "baz.com", "bar.com" }, sni_names) - - -- make sure we did not add any certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, json.total) - assert.equal(2, #json.data) - end) - - it("updates only the certificate if no snis are specified", function() - local res = assert(client:send { - method = "PUT", - path = "/certificates", - body = { - id = cert_bar.id, - cert = "bar_cert", - key = "bar_key", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, - }) - - local body = assert.res_status(200, res) - local json = cjson.decode(body) - - -- make sure certificate got updated and sni remains the same - assert.same({ "bar.com" }, json.snis) - assert.same("bar_cert", json.cert) - assert.same("bar_key", json.key) - - -- make sure the certificate got updated in DB - res = assert(client:send { - method = "GET", - path = "/certificates/" .. cert_bar.id, - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal("bar_cert", json.cert) - assert.equal("bar_key", json.key) - - -- make sure number of snis don't change - res = assert(client:send { - method = "GET", - path = "/snis", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, #json.data) - assert.equal(2, json.total) - - -- make sure we did not add any certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, json.total) - assert.equal(2, #json.data) - end) - - it("returns a conflict when duplicates snis are present in the request", function() - local res = assert(client:send { - method = "PUT", - path = "/certificates", - body = { - id = cert_bar.id, - snis = "baz.com,baz.com", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, - }) - - local body = assert.res_status(409, res) - local json = cjson.decode(body) - - assert.equals("duplicate SNI in request: baz.com", json.message) - - -- make sure number of snis don't change - res = assert(client:send { - method = "GET", - path = "/snis", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, #json.data) - assert.equal(2, json.total) - - -- make sure we did not add any certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, json.total) - assert.equal(2, #json.data) - end) - - it("returns a conflict when a pre-existing sni present in " .. - "the request is associated with another certificate", function() - local res = assert(client:send { - method = "PUT", - path = "/certificates", - body = { - id = cert_bar.id, - snis = "foo.com,baz.com", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, - }) - - local body = assert.res_status(409, res) - local json = cjson.decode(body) - - assert.equals("SNI 'foo.com' already associated with " .. - "existing certificate (" .. cert_foo.id .. ")", - json.message) - - -- make sure number of snis don't change - res = assert(client:send { - method = "GET", - path = "/snis", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, #json.data) - assert.equal(2, json.total) - - -- make sure we did not add any certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, json.total) - assert.equal(2, #json.data) - end) - - it("deletes all snis from a certificate if snis field is JSON null", function() - -- Note: we currently do not support unsetting a field with - -- form-urlencoded requests. This depends on upcoming work - -- to the Admin API. We here follow the road taken by: - -- https://github.com/Kong/kong/pull/2700 - local res = assert(client:send { - method = "PUT", - path = "/certificates", - body = { - snis = ngx.null, - id = cert_bar.id, - }, - headers = { ["Content-Type"] = "application/json" }, - }) - - local body = assert.res_status(200, res) - local json = cjson.decode(body) - - assert.equal(0, #json.snis) - assert.matches('"snis":[]', body, nil, true) - - -- make sure the sni was deleted - res = assert(client:send { - method = "GET", - path = "/snis", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(1, #json.data) - assert.equal(1, json.total) - assert.equal("foo.com", json.data[1].name) - - -- make sure we did not add any certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, json.total) - assert.equal(2, #json.data) - end) - end) - end) - - describe("/certificates/:sni_or_uuid", function() - before_each(function() - dao:truncate_tables() - local res = assert(client:send { - method = "POST", - path = "/certificates", - body = { - cert = ssl_fixtures.cert, - key = ssl_fixtures.key, - snis = "foo.com,bar.com", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, - }) - - assert.res_status(201, res) - end) - - describe("GET", function() - it("retrieves a certificate by SNI", function() - local res1 = assert(client:send { - method = "GET", - path = "/certificates/foo.com", - }) - - local body1 = assert.res_status(200, res1) - local json1 = cjson.decode(body1) - - local res2 = assert(client:send { - method = "GET", - path = "/certificates/bar.com", - }) - - local body2 = assert.res_status(200, res2) - local json2 = cjson.decode(body2) - - assert.is_string(json1.cert) - assert.is_string(json1.key) - assert.same({ "foo.com", "bar.com" }, json1.snis) - assert.same(json1, json2) - end) - end) - - describe("PATCH", function() - local cert_foo - local cert_bar - - before_each(function() - dao:truncate_tables() - - local res = assert(client:send { - method = "POST", - path = "/certificates", - body = { - cert = ssl_fixtures.cert, - key = ssl_fixtures.key, - snis = "foo.com", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, - }) - local body = assert.res_status(201, res) - cert_foo = cjson.decode(body) - - local res = assert(client:send { - method = "POST", - path = "/certificates", - body = { - cert = ssl_fixtures.cert, - key = ssl_fixtures.key, - snis = "bar.com", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, - }) - local body = assert.res_status(201, res) - cert_bar = cjson.decode(body) - end) - - it_content_types("updates a certificate by SNI", function(content_type) - return function() - local res = assert(client:send { - method = "PATCH", - path = "/certificates/foo.com", - body = { - cert = "foo_cert", - key = "foo_key", - }, - headers = { ["Content-Type"] = content_type } - }) - - local body = assert.res_status(200, res) - local json = cjson.decode(body) - - assert.equal("foo_cert", json.cert) - end - end) - - it("returns 404 for a random non-existing id", function() - local res = assert(client:send { - method = "PATCH", - path = "/certificates/" .. utils.uuid(), - body = { - cert = ssl_fixtures.cert, - key = ssl_fixtures.key, - snis = "baz.com", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, - }) - - assert.res_status(404, res) - - -- make sure we did not add any sni - res = assert(client:send { - method = "GET", - path = "/snis", - }) - - local body = assert.res_status(200, res) - local json = cjson.decode(body) - assert.equal(2, #json.data) - assert.equal(2, json.total) - - -- make sure we did not add any certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, json.total) - assert.equal(2, #json.data) - end) - - it("returns Bad Request if only certificate is specified", function() - local res = assert(client:send { - method = "PATCH", - path = "/certificates/" .. cert_foo.id, - body = { - cert = "cert_foo", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, - }) - - local body = assert.res_status(400, res) - local json = cjson.decode(body) - assert.equals("key is required", json.key) - - -- make sure we did not add any sni - res = assert(client:send { - method = "GET", - path = "/snis", - }) - - local body = assert.res_status(200, res) - local json = cjson.decode(body) - assert.equal(2, #json.data) - assert.equal(2, json.total) - - -- make sure we did not add any certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, json.total) - assert.equal(2, #json.data) - end) - - it("returns Bad Request if only key is specified", function() - local res = assert(client:send { - method = "PATCH", - path = "/certificates/" .. cert_foo.id, - body = { - key = "key_foo", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, - }) - - local body = assert.res_status(400, res) - local json = cjson.decode(body) - assert.equals("cert is required", json.cert) - - -- make sure we did not add any sni - res = assert(client:send { - method = "GET", - path = "/snis", - }) - - local body = assert.res_status(200, res) - local json = cjson.decode(body) - assert.equal(2, #json.data) - assert.equal(2, json.total) - - -- make sure we did not add any certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, json.total) - assert.equal(2, #json.data) - end) - - it("updates snis associated with a certificate", function() - local res = assert(client:send { - method = "PATCH", - path = "/certificates/" .. cert_foo.id, - body = { - snis = "baz.com", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, - }) - - local body = assert.res_status(200, res) - local json = cjson.decode(body) - - assert.same({ "baz.com" }, json.snis) - - -- make sure number of snis don't change - -- since we delete foo.com and added baz.com - res = assert(client:send { - method = "GET", - path = "/snis", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, #json.data) - assert.equal(2, json.total) - local sni_names = {} - table.insert(sni_names, json.data[1].name) - table.insert(sni_names, json.data[2].name) - assert.are.same( { "baz.com", "bar.com" } , sni_names) - - -- make sure we did not add any certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, json.total) - assert.equal(2, #json.data) - end) - - it("updates only the certificate if no snis are specified", function() - local res = assert(client:send { - method = "PATCH", - path = "/certificates/" .. cert_bar.id, - body = { - cert = "bar_cert", - key = "bar_key", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, - }) - - local body = assert.res_status(200, res) - local json = cjson.decode(body) - - -- make sure certificate got updated and sni remains the same - assert.same({ "bar.com" }, json.snis) - assert.same("bar_cert", json.cert) - assert.same("bar_key", json.key) - - -- make sure the certificate got updated in DB - res = assert(client:send { - method = "GET", - path = "/certificates/" .. cert_bar.id, - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal("bar_cert", json.cert) - assert.equal("bar_key", json.key) - - -- make sure number of snis don't change - res = assert(client:send { - method = "GET", - path = "/snis", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, #json.data) - assert.equal(2, json.total) - - -- make sure we did not add any certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, json.total) - assert.equal(2, #json.data) - end) - - it("returns a conflict when duplicates snis are present in the request", function() - local res = assert(client:send { - method = "PATCH", - path = "/certificates/" .. cert_bar.id, - body = { - snis = "baz.com,baz.com", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, - }) - - local body = assert.res_status(409, res) - local json = cjson.decode(body) - - assert.equals("duplicate SNI in request: baz.com", json.message) - - -- make sure number of snis don't change - res = assert(client:send { - method = "GET", - path = "/snis", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, #json.data) - assert.equal(2, json.total) - - -- make sure we did not add any certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, json.total) - assert.equal(2, #json.data) - end) - - it("returns a conflict when a pre-existing sni present in " .. - "the request is associated with another certificate", function() - local res = assert(client:send { - method = "PATCH", - path = "/certificates/" .. cert_bar.id, - body = { - snis = "foo.com,baz.com", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, - }) - - local body = assert.res_status(409, res) - local json = cjson.decode(body) - - assert.equals("SNI 'foo.com' already associated with " .. - "existing certificate (" .. cert_foo.id .. ")", - json.message) - - -- make sure number of snis don't change - res = assert(client:send { - method = "GET", - path = "/snis", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, #json.data) - assert.equal(2, json.total) - - -- make sure we did not add any certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, json.total) - assert.equal(2, #json.data) - end) - - it("deletes all snis from a certificate if snis field is JSON null", function() - -- Note: we currently do not support unsetting a field with - -- form-urlencoded requests. This depends on upcoming work - -- to the Admin API. We here follow the road taken by: - -- https://github.com/Kong/kong/pull/2700 - local res = assert(client:send { - method = "PATCH", - path = "/certificates/" .. cert_bar.id, - body = { - snis = ngx.null, - }, - headers = { ["Content-Type"] = "application/json" }, - }) - - local body = assert.res_status(200, res) - local json = cjson.decode(body) - - assert.equal(0, #json.snis) - assert.matches('"snis":[]', body, nil, true) - - -- make sure the sni was deleted - res = assert(client:send { - method = "GET", - path = "/snis", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(1, #json.data) - assert.equal(1, json.total) - assert.equal("foo.com", json.data[1].name) - - -- make sure we did not add any certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, json.total) - assert.equal(2, #json.data) - end) - end) - - describe("DELETE", function() - it("deletes a certificate and all related SNIs", function() - local res = assert(client:send { - method = "DELETE", - path = "/certificates/foo.com", - }) - - assert.res_status(204, res) - - res = assert(client:send { - method = "GET", - path = "/certificates/foo.com", - }) - - assert.res_status(404, res) - - res = assert(client:send { - method = "GET", - path = "/certificates/bar.com", - }) - - assert.res_status(404, res) - end) - - it("deletes a certificate by id", function() - local res = assert(client:send { - method = "POST", - path = "/certificates", - body = { - cert = "foo", - key = "bar", - }, - headers = { ["Content-Type"] = "application/json" } - }) - - local body = assert.res_status(201, res) - local json = cjson.decode(body) - - res = assert(client:send { - method = "DELETE", - path = "/certificates/" .. json.id, - }) - - assert.res_status(204, res) - end) - end) - end) - - - describe("/snis", function() - local ssl_certificate - - before_each(function() - dao:truncate_tables() - ssl_certificate = assert(dao.ssl_certificates:insert { - cert = ssl_fixtures.cert, - key = ssl_fixtures.key, - }) - assert(dao.ssl_servers_names:insert { - name = "foo.com", - ssl_certificate_id = ssl_certificate.id, - }) - end) - - describe("POST", function() - before_each(function() - dao:truncate_tables() - - ssl_certificate = assert(dao.ssl_certificates:insert { - cert = ssl_fixtures.cert, - key = ssl_fixtures.key, - }) - end) - - describe("errors", function() - it("certificate doesn't exist", function() - local res = assert(client:send { - method = "POST", - path = "/snis", - body = { - name = "bar.com", - ssl_certificate_id = "585e4c16-c656-11e6-8db9-5f512d8a12cd", - }, - headers = { ["Content-Type"] = "application/json" }, - }) - - local body = assert.res_status(404, res) - local json = cjson.decode(body) - assert.same({ ssl_certificate_id = "does not exist with value " - .. "'585e4c16-c656-11e6-8db9-5f512d8a12cd'" }, json) - end) - end) - - it_content_types("creates a SNI", function(content_type) - return function() - local res = assert(client:send { - method = "POST", - path = "/snis", - body = { - name = "foo.com", - ssl_certificate_id = ssl_certificate.id, - }, - headers = { ["Content-Type"] = content_type }, - }) - - local body = assert.res_status(201, res) - local json = cjson.decode(body) - assert.equal("foo.com", json.name) - assert.equal(ssl_certificate.id, json.ssl_certificate_id) - end - end) - - it("returns a conflict when an SNI already exists", function() - assert(dao.ssl_servers_names:insert { - name = "foo.com", - ssl_certificate_id = ssl_certificate.id, - }) - - local res = assert(client:send { - method = "POST", - path = "/snis", - body = { - name = "foo.com", - ssl_certificate_id = ssl_certificate.id, - }, - headers = { ["Content-Type"] = "application/json" }, - }) - - local body = assert.res_status(409, res) - local json = cjson.decode(body) - assert.equals("already exists with value 'foo.com'", json.name) - end) - end) - - describe("GET", function() - it("retrieves a SNI", function() - local res = assert(client:send { - method = "GET", - path = "/snis", - }) - - local body = assert.res_status(200, res) - local json = cjson.decode(body) - assert.equal(1, #json.data) - assert.equal(1, json.total) - assert.equal("foo.com", json.data[1].name) - assert.equal(ssl_certificate.id, json.data[1].ssl_certificate_id) - end) - end) - end) - - describe("/snis/:name", function() - local ssl_certificate - - before_each(function() - dao:truncate_tables() - ssl_certificate = assert(dao.ssl_certificates:insert { - cert = ssl_fixtures.cert, - key = ssl_fixtures.key, - }) - assert(dao.ssl_servers_names:insert { - name = "foo.com", - ssl_certificate_id = ssl_certificate.id, - }) - end) - - describe("GET", function() - it("retrieves a SNI", function() - local res = assert(client:send { - mathod = "GET", - path = "/snis/foo.com", - }) - - local body = assert.res_status(200, res) - local json = cjson.decode(body) - assert.equal("foo.com", json.name) - assert.equal(ssl_certificate.id, json.ssl_certificate_id) - end) - end) - - describe("PATCH", function() - do - local test = it - if kong_config.database == "cassandra" then - test = pending - end - - test("updates a SNI", function() - -- SKIP: this test fails with Cassandra because the PRIMARY KEY - -- used by the C* table is a composite of (name, - -- ssl_certificate_id), and hence, we cannot update the - -- ssl_certificate_id field because it is in the `SET` part of the - -- query built by the DAO, but in C*, one cannot change a value - -- from the clustering key. - local ssl_certificate_2 = assert(dao.ssl_certificates:insert { - cert = "foo", - key = "bar", - }) - - local res = assert(client:send { - method = "PATCH", - path = "/snis/foo.com", - body = { - ssl_certificate_id = ssl_certificate_2.id, - }, - headers = { ["Content-Type"] = "application/json" }, - }) - - local body = assert.res_status(200, res) - local json = cjson.decode(body) - assert.equal(ssl_certificate_2.id, json.ssl_certificate_id) - end) - end - end) - - describe("DELETE", function() - it("deletes a SNI", function() - local res = assert(client:send { - method = "DELETE", - path = "/snis/foo.com", - }) - - assert.res_status(204, res) - end) - end) - end) -end) - -end) diff --git a/spec-old-api/02-integration/05-proxy/05-ssl_spec.lua b/spec-old-api/02-integration/05-proxy/05-ssl_spec.lua index b648323461a0..690dd17bbf98 100644 --- a/spec-old-api/02-integration/05-proxy/05-ssl_spec.lua +++ b/spec-old-api/02-integration/05-proxy/05-ssl_spec.lua @@ -79,7 +79,7 @@ describe("SSL", function() body = { cert = ssl_fixtures.cert, key = ssl_fixtures.key, - snis = "example.com,ssl1.com", + snis = { "example.com", "ssl1.com" }, }, headers = { ["Content-Type"] = "application/json" }, }) diff --git a/spec-old-api/02-integration/06-invalidations/02-core_entities_invalidations_spec.lua b/spec-old-api/02-integration/06-invalidations/02-core_entities_invalidations_spec.lua index 4e74d1164ae2..3977a465b234 100644 --- a/spec-old-api/02-integration/06-invalidations/02-core_entities_invalidations_spec.lua +++ b/spec-old-api/02-integration/06-invalidations/02-core_entities_invalidations_spec.lua @@ -215,7 +215,7 @@ describe("core entities are invalidated with db: #" .. strategy, function() -- ssl_certificates ------------------- - describe("#o ssl_certificates / SNIs", function() + describe("ssl_certificates / snis", function() local function get_cert(port, sni) local pl_utils = require "pl.utils" @@ -251,7 +251,7 @@ describe("core entities are invalidated with db: #" .. strategy, function() body = { cert = ssl_fixtures.cert, key = ssl_fixtures.key, - snis = "ssl-example.com", + snis = { "ssl-example.com" }, }, headers = { ["Content-Type"] = "application/json", @@ -288,7 +288,7 @@ describe("core entities are invalidated with db: #" .. strategy, function() body = { cert = ssl_fixtures.cert, key = ssl_fixtures.key, - snis = "new-ssl-example.com", + snis = { "new-ssl-example.com" }, }, headers = { ["Content-Type"] = "application/json", @@ -344,7 +344,7 @@ describe("core entities are invalidated with db: #" .. strategy, function() end) pending("on SNI update", function() - -- Pending: currently, SNIs cannot be updated: + -- Pending: currently, snis cannot be updated: -- - A PATCH updating the name property would not work, since -- the URI path expects the current name, and so does the -- query fetchign the row to be updated diff --git a/spec/02-integration/01-helpers/01-blueprints_spec.lua b/spec/02-integration/01-helpers/01-blueprints_spec.lua index dd01b0afaec1..1b5880443364 100644 --- a/spec/02-integration/01-helpers/01-blueprints_spec.lua +++ b/spec/02-integration/01-helpers/01-blueprints_spec.lua @@ -107,16 +107,20 @@ dao_helpers.for_each_dao(function(kong_config) assert.same({ "email", "profile" }, p.config.scopes) end) - it("inserts ssl certificates", function() - local c = bp.ssl_certificates:insert() + it("inserts certificates", function() + local c = bp.certificates:insert() assert.equal("string", type(c.cert)) assert.equal("string", type(c.key)) end) - it("inserts ssl server names", function() - local c = bp.ssl_certificates:insert() - local s = bp.ssl_servers_names:insert({ ssl_certificate_id = c.id }) + it("inserts snis", function() + local c = bp.certificates:insert() + local s = bp.snis:insert({ certificate = c }) assert.equal("string", type(s.name)) + + local s2 = bp.snis:insert() + assert.equal("string", type(s2.name)) + assert.equal("string", type(s2.certificate.id)) end) it("inserts consumers", function() diff --git a/spec/02-integration/03-dao/04-constraints_spec.lua b/spec/02-integration/03-dao/04-constraints_spec.lua index 511bfa5f9543..3d51f63af6ae 100644 --- a/spec/02-integration/03-dao/04-constraints_spec.lua +++ b/spec/02-integration/03-dao/04-constraints_spec.lua @@ -315,7 +315,7 @@ dao_helpers.for_each_dao(function(kong_config) } assert.is_nil(err_t) assert.is_nil(err) - assert.is_true(ok) + assert.is_truthy(ok) -- no more Service local api, err = db.services:select { @@ -364,7 +364,7 @@ dao_helpers.for_each_dao(function(kong_config) } assert.is_nil(err_t) assert.is_nil(err) - assert.is_true(ok) + assert.is_truthy(ok) -- no more Route local api, err = db.routes:select { diff --git a/spec/02-integration/04-admin_api/06-certificates_routes_spec.lua b/spec/02-integration/04-admin_api/06-certificates_routes_spec.lua index a3cdca937621..5b6f5d9cfc84 100644 --- a/spec/02-integration/04-admin_api/06-certificates_routes_spec.lua +++ b/spec/02-integration/04-admin_api/06-certificates_routes_spec.lua @@ -1,9 +1,8 @@ local ssl_fixtures = require "spec.fixtures.ssl" -local dao_helpers = require "spec.02-integration.03-dao.helpers" -local DAOFactory = require "kong.dao.factory" local helpers = require "spec.helpers" local cjson = require "cjson" local utils = require "kong.tools.utils" +local Errors = require "kong.db.errors" local function it_content_types(title, fn) @@ -14,11 +13,33 @@ local function it_content_types(title, fn) end -dao_helpers.for_each_dao(function(kong_config) +local function get_snis_lists(certs) + local lists = {} + for i=1, #certs do + lists[i] = certs[i].snis + end -describe("Admin API: #" .. kong_config.database, function() + table.sort(lists, function(a,b) + if not a[1] then + return true + end + if not b[1] then + return false + end + + return a[1] < b[1] + end) + + return lists +end + + +for _, strategy in helpers.each_strategy() do + +describe("Admin API: #" .. strategy, function() local client - local dao + + local bp, db, dao before_each(function() client = assert(helpers.admin_client()) @@ -31,11 +52,11 @@ describe("Admin API: #" .. kong_config.database, function() end) setup(function() - dao = assert(DAOFactory.new(kong_config)) + bp, db, dao = helpers.get_db_utils(strategy) assert(dao:run_migrations()) assert(helpers.start_kong({ - database = kong_config.database + database = strategy, })) end) @@ -45,131 +66,80 @@ describe("Admin API: #" .. kong_config.database, function() describe("/certificates", function() before_each(function() - dao:truncate_tables() - local res = assert(client:send { - method = "POST", - path = "/certificates", + assert(db:truncate()) + local res = client:post("/certificates", { body = { cert = ssl_fixtures.cert, key = ssl_fixtures.key, - snis = "foo.com,bar.com", + snis = { "foo.com", "bar.com" }, }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, + headers = { ["Content-Type"] = "application/json" }, }) - assert.res_status(201, res) end) describe("GET", function() it("retrieves all certificates", function() - local res = assert(client:send { - method = "GET", - path = "/certificates", - }) - + local res = client:get("/certificates") local body = assert.res_status(200, res) local json = cjson.decode(body) - assert.equal(1, json.total) assert.equal(1, #json.data) assert.is_string(json.data[1].cert) assert.is_string(json.data[1].key) - assert.same({ "foo.com", "bar.com" }, json.data[1].snis) + assert.same({ "bar.com", "foo.com" }, json.data[1].snis) end) end) describe("POST", function() - it("returns a conflict when duplicates snis are present in the request", function() - local res = assert(client:send { - method = "POST", - path = "/certificates", + it("returns a conflict when duplicated snis are present in the request", function() + local res = client:post("/certificates", { body = { cert = ssl_fixtures.cert, key = ssl_fixtures.key, - snis = "foobar.com,baz.com,foobar.com", + snis = { "foobar.com", "baz.com", "foobar.com" }, }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, + headers = { ["Content-Type"] = "application/json" }, }) - - local body = assert.res_status(409, res) + local body = assert.res_status(400, res) local json = cjson.decode(body) - assert.equals("duplicate SNI in request: foobar.com", json.message) - - -- make sure we dont add any snis - res = assert(client:send { - method = "GET", - path = "/snis", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, #json.data) - assert.equal(2, json.total) - - -- make sure we didnt add the certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) + assert.equals("schema violation (snis: foobar.com is duplicated)", json.message) + -- make sure we didnt add the certificate, or any snis + res = client:get("/certificates") body = assert.res_status(200, res) json = cjson.decode(body) assert.equal(1, #json.data) - assert.equal(1, json.total) + assert.same({ "bar.com", "foo.com" }, json.data[1].snis) end) it("returns a conflict when a pre-existing sni is detected", function() - local res = assert(client:send { - method = "POST", - path = "/certificates", + local res = client:post("/certificates", { body = { cert = ssl_fixtures.cert, key = ssl_fixtures.key, - snis = "foo.com", + snis = { "foo.com" }, }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, + headers = { ["Content-Type"] = "application/json" }, }) - - local body = assert.res_status(409, res) + local body = assert.res_status(400, res) local json = cjson.decode(body) - assert.equals("SNI already exists: foo.com", json.message) - - -- make sure we only have two snis - res = assert(client:send { - method = "GET", - path = "/snis", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, #json.data) - assert.equal(2, json.total) - assert.equal("foo.com", json.data[1].name) - assert.equal("bar.com", json.data[2].name) - - -- make sure we only have one certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) + assert.matches("snis: foo.com already associated with existing certificate", json.message) + -- make sure we only have one certificate, with two snis + res = client:get("/certificates") body = assert.res_status(200, res) json = cjson.decode(body) - assert.equal(1, json.total) assert.equal(1, #json.data) - assert.is_string(json.data[1].cert) - assert.is_string(json.data[1].key) - assert.same({ "foo.com", "bar.com" }, json.data[1].snis) + assert.same({ "bar.com", "foo.com" }, json.data[1].snis) end) - it_content_types("creates a certificate", function(content_type) + it_content_types("creates a certificate and returns it with the snis pseudo-property", function(content_type) return function() - local res = assert(client:send { - method = "POST", - path = "/certificates", + local res = client:post("/certificates", { body = { cert = ssl_fixtures.cert, key = ssl_fixtures.key, - snis = "foobar.com,baz.com", + snis = { "foobar.com", "baz.com" }, }, headers = { ["Content-Type"] = content_type }, }) @@ -178,15 +148,13 @@ describe("Admin API: #" .. kong_config.database, function() local json = cjson.decode(body) assert.is_string(json.cert) assert.is_string(json.key) - assert.same({ "foobar.com", "baz.com" }, json.snis) + assert.same({ "baz.com", "foobar.com" }, json.snis) end end) it_content_types("returns snis as [] when none is set", function(content_type) return function() - local res = assert(client:send { - method = "POST", - path = "/certificates", + local res = client:post("/certificates", { body = { cert = ssl_fixtures.cert, key = ssl_fixtures.key, @@ -202,478 +170,119 @@ describe("Admin API: #" .. kong_config.database, function() end end) end) + end) - describe("PUT", function() - local cert_foo - local cert_bar - - before_each(function() - dao:truncate_tables() - - local res = assert(client:send { - method = "POST", - path = "/certificates", - body = { - cert = ssl_fixtures.cert, - key = ssl_fixtures.key, - snis = "foo.com", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, - }) - local body = assert.res_status(201, res) - cert_foo = cjson.decode(body) - - local res = assert(client:send { - method = "POST", - path = "/certificates", - body = { - cert = ssl_fixtures.cert, - key = ssl_fixtures.key, - snis = "bar.com", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, - }) - local body = assert.res_status(201, res) - cert_bar = cjson.decode(body) - end) - - it("creates a certificate if ID is not present in the body", function() - local res = assert(client:send { - method = "PUT", - path = "/certificates", - body = { - cert = ssl_fixtures.cert, - key = ssl_fixtures.key, - snis = "baz.com", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, - }) - - local body = assert.res_status(201, res) - local json = cjson.decode(body) - - assert.is_string(json.cert) - assert.is_string(json.key) - assert.same({ "baz.com" }, json.snis) - - -- make sure we added an sni - res = assert(client:send { - method = "GET", - path = "/snis", - }) - - body = assert.res_status(200, res) - local json = cjson.decode(body) - assert.equal(3, #json.data) - assert.equal(3, json.total) - - -- make sure we added our certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) - - local body = assert.res_status(200, res) - local json = cjson.decode(body) - assert.equal(3, #json.data) - assert.equal(3, json.total) - end) - - it("returns 404 for a random non-existing id", function() - local res = assert(client:send { - method = "PUT", - path = "/certificates", - body = { - id = utils.uuid(), - cert = ssl_fixtures.cert, - key = ssl_fixtures.key, - snis = "baz.com", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, - }) - - assert.res_status(404, res) - - -- make sure we did not add any sni - res = assert(client:send { - method = "GET", - path = "/snis", - }) - - local body = assert.res_status(200, res) - local json = cjson.decode(body) - assert.equal(2, #json.data) - assert.equal(2, json.total) - - -- make sure we did not add any certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, json.total) - assert.equal(2, #json.data) - end) - - it("returns Bad Request if only certificate is specified", function() - local res = assert(client:send { - method = "PUT", - path = "/certificates", - body = { - id = cert_foo.id, - cert = "cert_foo", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, - }) - - local body = assert.res_status(400, res) - local json = cjson.decode(body) - assert.equals("key is required", json.key) - - -- make sure we did not add any sni - res = assert(client:send { - method = "GET", - path = "/snis", - }) - - local body = assert.res_status(200, res) - local json = cjson.decode(body) - assert.equal(2, #json.data) - assert.equal(2, json.total) - - -- make sure we did not add any certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, json.total) - assert.equal(2, #json.data) - end) - - it("returns Bad Request if only key is specified", function() - local res = assert(client:send { - method = "PUT", - path = "/certificates", - body = { - id = cert_foo.id, - key = "key_foo", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, - }) - - local body = assert.res_status(400, res) - local json = cjson.decode(body) - assert.equals("cert is required", json.cert) - - -- make sure we did not add any sni - res = assert(client:send { - method = "GET", - path = "/snis", - }) - - local body = assert.res_status(200, res) - local json = cjson.decode(body) - assert.equal(2, #json.data) - assert.equal(2, json.total) - - -- make sure we did not add any certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, json.total) - assert.equal(2, #json.data) - end) - - it("updates snis associated with a certificate", function() - local res = assert(client:send { - method = "PUT", - path = "/certificates", - body = { - id = cert_foo.id, - snis = "baz.com", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, - }) - - local body = assert.res_status(200, res) - local json = cjson.decode(body) + describe("/certificates/cert_id_or_sni", function() + local certificate - assert.same({ "baz.com" }, json.snis) + before_each(function() + assert(db:truncate()) + local res = client:post("/certificates", { + body = { + cert = ssl_fixtures.cert, + key = ssl_fixtures.key, + snis = { "foo.com", "bar.com" }, + }, + headers = { ["Content-Type"] = "application/json" }, + }) - -- make sure number of snis don't change - -- since we delete foo.com and added baz.com - res = assert(client:send { - method = "GET", - path = "/snis", - }) + local body = assert.res_status(201, res) + certificate = cjson.decode(body) + end) - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, #json.data) - assert.equal(2, json.total) - local sni_names = {} - table.insert(sni_names, json.data[1].name) - table.insert(sni_names, json.data[2].name) - assert.are.same({ "baz.com", "bar.com" }, sni_names) - - -- make sure we did not add any certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) + describe("GET", function() + it("retrieves a certificate by id", function() + local res1 = client:get("/certificates/" .. certificate.id) + local body1 = assert.res_status(200, res1) + local json1 = cjson.decode(body1) - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, json.total) - assert.equal(2, #json.data) + assert.is_string(json1.cert) + assert.is_string(json1.key) + assert.same({ "bar.com", "foo.com" }, json1.snis) end) - it("updates only the certificate if no snis are specified", function() - local res = assert(client:send { - method = "PUT", - path = "/certificates", - body = { - id = cert_bar.id, - cert = "bar_cert", - key = "bar_key", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, - }) - - local body = assert.res_status(200, res) - local json = cjson.decode(body) - - -- make sure certificate got updated and sni remains the same - assert.same({ "bar.com" }, json.snis) - assert.same("bar_cert", json.cert) - assert.same("bar_key", json.key) - - -- make sure the certificate got updated in DB - res = assert(client:send { - method = "GET", - path = "/certificates/" .. cert_bar.id, - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal("bar_cert", json.cert) - assert.equal("bar_key", json.key) - - -- make sure number of snis don't change - res = assert(client:send { - method = "GET", - path = "/snis", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, #json.data) - assert.equal(2, json.total) + it("retrieves a certificate by sni", function() + local res1 = client:get("/certificates/foo.com") + local body1 = assert.res_status(200, res1) + local json1 = cjson.decode(body1) - -- make sure we did not add any certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) + local res2 = client:get("/certificates/bar.com") + local body2 = assert.res_status(200, res2) + local json2 = cjson.decode(body2) - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, json.total) - assert.equal(2, #json.data) + assert.is_string(json1.cert) + assert.is_string(json1.key) + assert.same({ "bar.com", "foo.com" }, json1.snis) + assert.same(json1, json2) end) - it("returns a conflict when duplicates snis are present in the request", function() - local res = assert(client:send { - method = "PUT", - path = "/certificates", - body = { - id = cert_bar.id, - snis = "baz.com,baz.com", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, - }) - - local body = assert.res_status(409, res) - local json = cjson.decode(body) - - assert.equals("duplicate SNI in request: baz.com", json.message) - - -- make sure number of snis don't change - res = assert(client:send { - method = "GET", - path = "/snis", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, #json.data) - assert.equal(2, json.total) - - -- make sure we did not add any certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, json.total) - assert.equal(2, #json.data) + it("returns 404 for a random non-existing uuid", function() + local res = client:get("/certificates/" .. utils.uuid()) + assert.res_status(404, res) end) - it("returns a conflict when a pre-existing sni present in " .. - "the request is associated with another certificate", function() - local res = assert(client:send { - method = "PUT", - path = "/certificates", - body = { - id = cert_bar.id, - snis = "foo.com,baz.com", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, - }) - - local body = assert.res_status(409, res) - local json = cjson.decode(body) - - assert.equals("SNI 'foo.com' already associated with " .. - "existing certificate (" .. cert_foo.id .. ")", - json.message) - - -- make sure number of snis don't change - res = assert(client:send { - method = "GET", - path = "/snis", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, #json.data) - assert.equal(2, json.total) - - -- make sure we did not add any certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, json.total) - assert.equal(2, #json.data) + it("returns 404 for a random non-existing sni", function() + local res = client:get("/certificates/doesntexist.com") + assert.res_status(404, res) end) + end) - it("deletes all snis from a certificate if snis field is JSON null", function() - -- Note: we currently do not support unsetting a field with - -- form-urlencoded requests. This depends on upcoming work - -- to the Admin API. We here follow the road taken by: - -- https://github.com/Kong/kong/pull/2700 - local res = assert(client:send { - method = "PUT", - path = "/certificates", - body = { - snis = ngx.null, - id = cert_bar.id, + describe("PUT", function() + it("creates if not found", function() + local id = utils.uuid() + local res = client:put("/certificates/" .. id, { + body = { + cert = "created_cert", + key = "created_key", + snis = { "pandoras-box.com" }, }, headers = { ["Content-Type"] = "application/json" }, }) - local body = assert.res_status(200, res) - local json = cjson.decode(body) - - assert.equal(0, #json.snis) - assert.matches('"snis":[]', body, nil, true) - - -- make sure the sni was deleted - res = assert(client:send { - method = "GET", - path = "/snis", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(1, #json.data) - assert.equal(1, json.total) - assert.equal("foo.com", json.data[1].name) - - -- make sure we did not add any certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, json.total) - assert.equal(2, #json.data) - end) - end) - end) - - describe("/certificates/:sni_or_uuid", function() - before_each(function() - dao:truncate_tables() - local res = assert(client:send { - method = "POST", - path = "/certificates", - body = { - cert = ssl_fixtures.cert, - key = ssl_fixtures.key, - snis = "foo.com,bar.com", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, - }) - - assert.res_status(201, res) - end) - - describe("GET", function() - it("retrieves a certificate by SNI", function() - local res1 = assert(client:send { - method = "GET", - path = "/certificates/foo.com", - }) - - local body1 = assert.res_status(200, res1) - local json1 = cjson.decode(body1) - - local res2 = assert(client:send { - method = "GET", - path = "/certificates/bar.com", - }) + local json = cjson.decode(body) + assert.same("created_cert", json.cert) - local body2 = assert.res_status(200, res2) - local json2 = cjson.decode(body2) + assert.same({ "pandoras-box.com" }, json.snis) + json.snis = nil - assert.is_string(json1.cert) - assert.is_string(json1.key) - assert.same({ "foo.com", "bar.com" }, json1.snis) - assert.same(json1, json2) + local in_db = assert(db.certificates:select({ id = id })) + assert.same(json, in_db) end) - it("returns 404 for a random non-existing uuid", function() - local res = assert(client:send { - method = "GET", - path = "/certificates/" .. utils.uuid(), + it("updates if found", function() + local res = client:put("/certificates/" .. certificate.id, { + body = { cert = "updated_cert", key = "updated_key" }, + headers = { ["Content-Type"] = "application/json" }, }) - assert.res_status(404, res) + + local body = assert.res_status(200, res) + local json = cjson.decode(body) + assert.same("updated_cert", json.cert) + assert.same("updated_key", json.key) + assert.same({"bar.com", "foo.com"}, json.snis) + + json.snis = nil + + local in_db = assert(db.certificates:select({ id = certificate.id })) + assert.same(json, in_db) end) - it("returns 404 for a random non-existing SNI", function() - local res = assert(client:send { - method = "GET", - path = "/certificates/doesntexist.com", + it("handles invalid input", function(content_type) + -- Missing params + local res = client:put("/certificates/" .. utils.uuid(), { + body = {}, + headers = { ["Content-Type"] = content_type } }) - assert.res_status(404, res) + local body = assert.res_status(400, res) + assert.same({ + code = Errors.codes.SCHEMA_VIOLATION, + name = "schema violation", + message = "2 schema violations (cert: required field missing; key: required field missing)", + fields = { + cert = "required field missing", + key = "required field missing", + } + }, cjson.decode(body)) end) end) @@ -682,40 +291,34 @@ describe("Admin API: #" .. kong_config.database, function() local cert_bar before_each(function() - dao:truncate_tables() + assert(db:truncate()) - local res = assert(client:send { - method = "POST", - path = "/certificates", + local res = client:post("/certificates", { body = { cert = ssl_fixtures.cert, key = ssl_fixtures.key, - snis = "foo.com", + snis = { "foo.com" }, }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, + headers = { ["Content-Type"] = "application/json" }, }) local body = assert.res_status(201, res) cert_foo = cjson.decode(body) - local res = assert(client:send { - method = "POST", - path = "/certificates", + local res = client:post("/certificates", { body = { cert = ssl_fixtures.cert, key = ssl_fixtures.key, - snis = "bar.com", + snis = { "bar.com" }, }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, + headers = { ["Content-Type"] = "application/json" }, }) local body = assert.res_status(201, res) cert_bar = cjson.decode(body) end) - it_content_types("updates a certificate by SNI", function(content_type) + it_content_types("updates a certificate by cert id", function(content_type) return function() - local res = assert(client:send { - method = "PATCH", - path = "/certificates/foo.com", + local res = client:patch("/certificates/" .. cert_foo.id, { body = { cert = "foo_cert", key = "foo_key", @@ -730,164 +333,64 @@ describe("Admin API: #" .. kong_config.database, function() end end) - it("returns 404 for a random non-existing id", function() - local res = assert(client:send { - method = "PATCH", - path = "/certificates/" .. utils.uuid(), - body = { - cert = ssl_fixtures.cert, - key = ssl_fixtures.key, - snis = "baz.com", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, - }) - - assert.res_status(404, res) - - -- make sure we did not add any sni - res = assert(client:send { - method = "GET", - path = "/snis", - }) - - local body = assert.res_status(200, res) - local json = cjson.decode(body) - assert.equal(2, #json.data) - assert.equal(2, json.total) - - -- make sure we did not add any certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, json.total) - assert.equal(2, #json.data) - end) - - it("returns Bad Request if only certificate is specified", function() - local res = assert(client:send { - method = "PATCH", - path = "/certificates/" .. cert_foo.id, - body = { - cert = "cert_foo", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, - }) - - local body = assert.res_status(400, res) - local json = cjson.decode(body) - assert.equals("key is required", json.key) - - -- make sure we did not add any sni - res = assert(client:send { - method = "GET", - path = "/snis", - }) - - local body = assert.res_status(200, res) - local json = cjson.decode(body) - assert.equal(2, #json.data) - assert.equal(2, json.total) + it_content_types("updates a certificate by sni", function(content_type) + return function() + local res = client:patch("/certificates/foo.com", { + body = { + cert = "foo_cert", + key = "foo_key", + }, + headers = { ["Content-Type"] = content_type } + }) - -- make sure we did not add any certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) + local body = assert.res_status(200, res) + local json = cjson.decode(body) - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, json.total) - assert.equal(2, #json.data) + assert.equal("foo_cert", json.cert) + end end) - it("returns Bad Request if only key is specified", function() - local res = assert(client:send { - method = "PATCH", - path = "/certificates/" .. cert_foo.id, + it("returns 404 for a random non-existing id", function() + local res = client:patch("/certificates/" .. utils.uuid(), { body = { - key = "key_foo", + cert = ssl_fixtures.cert, + key = ssl_fixtures.key, + snis = { "baz.com" }, }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, + headers = { ["Content-Type"] = "application/json" }, }) - local body = assert.res_status(400, res) - local json = cjson.decode(body) - assert.equals("cert is required", json.cert) - - -- make sure we did not add any sni - res = assert(client:send { - method = "GET", - path = "/snis", - }) + assert.res_status(404, res) + -- make sure we did not add any certificate or sni + res = client:get("/certificates") local body = assert.res_status(200, res) local json = cjson.decode(body) assert.equal(2, #json.data) - assert.equal(2, json.total) - - -- make sure we did not add any certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, json.total) - assert.equal(2, #json.data) + assert.same({ { "bar.com" }, { "foo.com" } }, get_snis_lists(json.data)) end) it("updates snis associated with a certificate", function() - local res = assert(client:send { - method = "PATCH", - path = "/certificates/" .. cert_foo.id, - body = { - snis = "baz.com", - }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, + local res = client:patch("/certificates/" .. cert_foo.id, { + body = { snis = { "baz.com" }, }, + headers = { ["Content-Type"] = "application/json" }, }) local body = assert.res_status(200, res) local json = cjson.decode(body) - assert.same({ "baz.com" }, json.snis) - -- make sure number of snis don't change - -- since we delete foo.com and added baz.com - res = assert(client:send { - method = "GET", - path = "/snis", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, #json.data) - assert.equal(2, json.total) - local sni_names = {} - table.insert(sni_names, json.data[1].name) - table.insert(sni_names, json.data[2].name) - assert.are.same( { "baz.com", "bar.com" } , sni_names) - - -- make sure we did not add any certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) - + -- make sure we did not add any certificate, and that the snis + -- are correct + res = client:get("/certificates") body = assert.res_status(200, res) json = cjson.decode(body) - assert.equal(2, json.total) assert.equal(2, #json.data) + assert.same({ { "bar.com" }, { "baz.com" } }, get_snis_lists(json.data)) end) it("updates only the certificate if no snis are specified", function() - local res = assert(client:send { - method = "PATCH", - path = "/certificates/" .. cert_bar.id, + local res = client:patch( "/certificates/" .. cert_bar.id, { body = { cert = "bar_cert", key = "bar_key", @@ -904,116 +407,61 @@ describe("Admin API: #" .. kong_config.database, function() assert.same("bar_key", json.key) -- make sure the certificate got updated in DB - res = assert(client:send { - method = "GET", - path = "/certificates/" .. cert_bar.id, - }) - + res = client:get("/certificates/" .. cert_bar.id) body = assert.res_status(200, res) json = cjson.decode(body) assert.equal("bar_cert", json.cert) assert.equal("bar_key", json.key) - -- make sure number of snis don't change - res = assert(client:send { - method = "GET", - path = "/snis", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, #json.data) - assert.equal(2, json.total) - - -- make sure we did not add any certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) - + -- make sure we did not add any certificate or sni + res = client:get("/certificates") body = assert.res_status(200, res) json = cjson.decode(body) - assert.equal(2, json.total) - assert.equal(2, #json.data) + assert.same({ { "bar.com" }, { "foo.com" } }, get_snis_lists(json.data)) end) - it("returns a conflict when duplicates snis are present in the request", function() - local res = assert(client:send { - method = "PATCH", - path = "/certificates/" .. cert_bar.id, + it("returns a conflict when duplicated snis are present in the request", function() + local res = client:patch("/certificates/" .. cert_bar.id, { body = { - snis = "baz.com,baz.com", + snis = { "baz.com", "baz.com" }, }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, + headers = { ["Content-Type"] = "application/json" }, }) - - local body = assert.res_status(409, res) + local body = assert.res_status(400, res) local json = cjson.decode(body) - assert.equals("duplicate SNI in request: baz.com", json.message) - - -- make sure number of snis don't change - res = assert(client:send { - method = "GET", - path = "/snis", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, #json.data) - assert.equal(2, json.total) - - -- make sure we did not add any certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) + assert.equals("schema violation (snis: baz.com is duplicated)", json.message) + -- make sure we did not change certificates or snis + res = client:get("/certificates") body = assert.res_status(200, res) json = cjson.decode(body) - assert.equal(2, json.total) assert.equal(2, #json.data) + assert.same({ { "bar.com" }, { "foo.com" } }, get_snis_lists(json.data)) end) it("returns a conflict when a pre-existing sni present in " .. "the request is associated with another certificate", function() - local res = assert(client:send { - method = "PATCH", - path = "/certificates/" .. cert_bar.id, + local res = client:patch("/certificates/" .. cert_bar.id, { body = { - snis = "foo.com,baz.com", + snis = { "foo.com", "baz.com" }, }, - headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }, + headers = { ["Content-Type"] = "application/json" }, }) - local body = assert.res_status(409, res) + local body = assert.res_status(400, res) local json = cjson.decode(body) - assert.equals("SNI 'foo.com' already associated with " .. - "existing certificate (" .. cert_foo.id .. ")", + assert.equals("schema violation (snis: foo.com already associated with " .. + "existing certificate '" .. cert_foo.id .. "')", json.message) - -- make sure number of snis don't change - res = assert(client:send { - method = "GET", - path = "/snis", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(2, #json.data) - assert.equal(2, json.total) - - -- make sure we did not add any certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) - + -- make sure we did not add any certificate or sni + res = client:get("/certificates") body = assert.res_status(200, res) json = cjson.decode(body) - assert.equal(2, json.total) assert.equal(2, #json.data) + assert.same({ { "bar.com" }, { "foo.com" } }, get_snis_lists(json.data)) end) it("deletes all snis from a certificate if snis field is JSON null", function() @@ -1021,147 +469,95 @@ describe("Admin API: #" .. kong_config.database, function() -- form-urlencoded requests. This depends on upcoming work -- to the Admin API. We here follow the road taken by: -- https://github.com/Kong/kong/pull/2700 - local res = assert(client:send { - method = "PATCH", - path = "/certificates/" .. cert_bar.id, + local res = client:patch("/certificates/" .. cert_bar.id, { body = { snis = ngx.null, }, headers = { ["Content-Type"] = "application/json" }, }) - local body = assert.res_status(200, res) local json = cjson.decode(body) assert.equal(0, #json.snis) assert.matches('"snis":[]', body, nil, true) - -- make sure the sni was deleted - res = assert(client:send { - method = "GET", - path = "/snis", - }) - - body = assert.res_status(200, res) - json = cjson.decode(body) - assert.equal(1, #json.data) - assert.equal(1, json.total) - assert.equal("foo.com", json.data[1].name) - - -- make sure we did not add any certificate - res = assert(client:send { - method = "GET", - path = "/certificates", - }) - + -- make sure we did not add any certificate and the sni was deleted + res = client:get("/certificates") body = assert.res_status(200, res) json = cjson.decode(body) - assert.equal(2, json.total) assert.equal(2, #json.data) + assert.same({ {}, { "foo.com" } }, get_snis_lists(json.data)) end) end) describe("DELETE", function() - it("deletes a certificate and all related SNIs", function() - local res = assert(client:send { - method = "DELETE", - path = "/certificates/foo.com", - }) - + it("deletes a certificate and all related snis", function() + local res = client:delete("/certificates/foo.com") assert.res_status(204, res) - res = assert(client:send { - method = "GET", - path = "/certificates/foo.com", - }) - - assert.res_status(404, res) - - res = assert(client:send { - method = "GET", - path = "/certificates/bar.com", - }) - - assert.res_status(404, res) + res = client:get("/certificates") + local body = assert.res_status(200, res) + local json = cjson.decode(body) + assert.equal(0, #json.data) end) it("deletes a certificate by id", function() - local res = assert(client:send { - method = "POST", - path = "/certificates", + local res = client:post("/certificates", { body = { cert = "foo", key = "bar", }, headers = { ["Content-Type"] = "application/json" } }) - local body = assert.res_status(201, res) local json = cjson.decode(body) - res = assert(client:send { - method = "DELETE", - path = "/certificates/" .. json.id, - }) - + local res = client:delete("/certificates/" .. json.id) assert.res_status(204, res) + + res = client:get("/certificates") + local body = assert.res_status(200, res) + local json = cjson.decode(body) + assert.equal(1, #json.data) end) end) end) - describe("/snis", function() - local ssl_certificate - - before_each(function() - dao:truncate_tables() - ssl_certificate = assert(dao.ssl_certificates:insert { - cert = ssl_fixtures.cert, - key = ssl_fixtures.key, - }) - assert(dao.ssl_servers_names:insert { - name = "foo.com", - ssl_certificate_id = ssl_certificate.id, - }) - end) - + describe("/certificates/:certificate/snis", function() describe("POST", function() + + local certificate before_each(function() - dao:truncate_tables() + assert(db:truncate()) - ssl_certificate = assert(dao.ssl_certificates:insert { - cert = ssl_fixtures.cert, - key = ssl_fixtures.key, + certificate = bp.certificates:insert() + bp.snis:insert({ + name = "ttt.com", + certificate = { id = certificate.id } }) end) describe("errors", function() it("certificate doesn't exist", function() - local res = assert(client:send { - method = "POST", - path = "/snis", - body = { - name = "bar.com", - ssl_certificate_id = "585e4c16-c656-11e6-8db9-5f512d8a12cd", + local res = client:post("/certificates/585e4c16-c656-11e6-8db9-5f512d8a12cd/snis", { + body = { + name = "bar.com", }, headers = { ["Content-Type"] = "application/json" }, }) local body = assert.res_status(404, res) local json = cjson.decode(body) - assert.same({ ssl_certificate_id = "does not exist with value " - .. "'585e4c16-c656-11e6-8db9-5f512d8a12cd'" }, json) + assert.same("Not found", json.message) end) end) - it_content_types("creates a SNI", function(content_type) + it_content_types("creates a sni using a certificate id", function(content_type) return function() - local res = assert(client:send { - method = "POST", - path = "/snis", - body = { - name = "foo.com", - ssl_certificate_id = ssl_certificate.id, + local res = client:post("/certificates/" .. certificate.id .. "/snis", { + body = { + name = "foo.com", }, headers = { ["Content-Type"] = content_type }, }) @@ -1169,124 +565,184 @@ describe("Admin API: #" .. kong_config.database, function() local body = assert.res_status(201, res) local json = cjson.decode(body) assert.equal("foo.com", json.name) - assert.equal(ssl_certificate.id, json.ssl_certificate_id) + assert.equal(certificate.id, json.certificate.id) end end) - it("returns a conflict when an SNI already exists", function() - assert(dao.ssl_servers_names:insert { - name = "foo.com", - ssl_certificate_id = ssl_certificate.id, - }) - - local res = assert(client:send { - method = "POST", - path = "/snis", - body = { - name = "foo.com", - ssl_certificate_id = ssl_certificate.id, + it_content_types("creates a sni using a sni to id the certificate", function(content_type) + return function() + local res = client:post("/certificates/ttt.com/snis", { + body = { + name = "foo.com", }, - headers = { ["Content-Type"] = "application/json" }, + headers = { ["Content-Type"] = content_type }, }) - local body = assert.res_status(409, res) + local body = assert.res_status(201, res) local json = cjson.decode(body) - assert.equals("already exists with value 'foo.com'", json.name) + assert.equal("foo.com", json.name) + assert.equal(certificate.id, json.certificate.id) + end end) - end) - describe("GET", function() - it("retrieves a SNI", function() - local res = assert(client:send { - method = "GET", - path = "/snis", + it("returns a conflict when an sni already exists", function() + bp.snis:insert { + name = "foo.com", + certificate = certificate, + } + + local res = client:post("/certificates/" .. certificate.id .. "/snis", { + body = { + name = "foo.com", + }, + headers = { ["Content-Type"] = "application/json" }, }) + local body = assert.res_status(409, res) + local json = cjson.decode(body) + assert.equals("unique constraint violation", json.name) + end) + end) + + describe("GET", function() + it("retrieves a list of snis", function() + assert(db:truncate()) + local certificate = bp.certificates:insert() + bp.snis:insert { + name = "foo.com", + certificate = certificate, + } + + local res = client:get("/certificates/" .. certificate.id .. "/snis") local body = assert.res_status(200, res) local json = cjson.decode(body) assert.equal(1, #json.data) - assert.equal(1, json.total) assert.equal("foo.com", json.data[1].name) - assert.equal(ssl_certificate.id, json.data[1].ssl_certificate_id) + assert.equal(certificate.id, json.data[1].certificate.id) end) end) end) describe("/snis/:name", function() - local ssl_certificate + local certificate, sni before_each(function() - dao:truncate_tables() - ssl_certificate = assert(dao.ssl_certificates:insert { - cert = ssl_fixtures.cert, - key = ssl_fixtures.key, - }) - assert(dao.ssl_servers_names:insert { - name = "foo.com", - ssl_certificate_id = ssl_certificate.id, - }) + assert(db:truncate()) + certificate = bp.certificates:insert() + sni = bp.snis:insert { + name = "foo.com", + certificate = certificate, + } end) describe("GET", function() - it("retrieves a SNI", function() - local res = assert(client:send { - mathod = "GET", - path = "/snis/foo.com", + it("retrieves a sni using the name", function() + local res = client:get("/snis/foo.com") + local body = assert.res_status(200, res) + local json = cjson.decode(body) + assert.equal("foo.com", json.name) + assert.equal(certificate.id, json.certificate.id) + end) + it("retrieves a sni using the id", function() + local res = client:get("/snis/" .. sni.id) + local body = assert.res_status(200, res) + local json = cjson.decode(body) + assert.equal("foo.com", json.name) + assert.equal(certificate.id, json.certificate.id) + end) + end) + + describe("PUT", function() + it("creates if not found", function() + local id = utils.uuid() + local res = client:put("/snis/" .. id, { + body = { + certificate = { id = certificate.id }, + name = "created.com", + }, + headers = { ["Content-Type"] = "application/json" }, + }) + local body = assert.res_status(200, res) + local json = cjson.decode(body) + assert.same("created.com", json.name) + + local in_db = assert(db.snis:select({ id = id })) + assert.same(json, in_db) + end) + + it("updates if found", function() + local res = client:put("/snis/" .. sni.id, { + body = { + name = "updated.com", + certificate = { id = certificate.id }, + }, + headers = { ["Content-Type"] = "application/json" }, }) local body = assert.res_status(200, res) local json = cjson.decode(body) - assert.equal("foo.com", json.name) - assert.equal(ssl_certificate.id, json.ssl_certificate_id) + assert.same("updated.com", json.name) + + local in_db = assert(db.snis:select({ id = sni.id })) + assert.same(json, in_db) + end) + + it("handles invalid input", function() + -- Missing params + local res = client:put("/snis/" .. utils.uuid(), { + body = {}, + headers = { ["Content-Type"] = "application/json" } + }) + local body = assert.res_status(400, res) + assert.same({ + code = Errors.codes.SCHEMA_VIOLATION, + name = "schema violation", + message = "2 schema violations (certificate: required field missing; name: required field missing)", + fields = { + certificate = "required field missing", + name = "required field missing", + } + }, cjson.decode(body)) end) end) describe("PATCH", function() - do - local test = it - if kong_config.database == "cassandra" then - test = pending - end + it("updates a sni", function() + local certificate_2 = bp.certificates:insert { + cert = "foo", + key = "bar", + } - test("updates a SNI", function() - -- SKIP: this test fails with Cassandra because the PRIMARY KEY - -- used by the C* table is a composite of (name, - -- ssl_certificate_id), and hence, we cannot update the - -- ssl_certificate_id field because it is in the `SET` part of the - -- query built by the DAO, but in C*, one cannot change a value - -- from the clustering key. - local ssl_certificate_2 = assert(dao.ssl_certificates:insert { - cert = "foo", - key = "bar", - }) + local res = client:patch("/snis/foo.com", { + body = { + certificate = { id = certificate_2.id }, + }, + headers = { ["Content-Type"] = "application/json" }, + }) - local res = assert(client:send { - method = "PATCH", - path = "/snis/foo.com", - body = { - ssl_certificate_id = ssl_certificate_2.id, - }, - headers = { ["Content-Type"] = "application/json" }, - }) + local body = assert.res_status(200, res) + local json = cjson.decode(body) + assert.equal(certificate_2.id, json.certificate.id) - local body = assert.res_status(200, res) - local json = cjson.decode(body) - assert.equal(ssl_certificate_2.id, json.ssl_certificate_id) - end) - end + local res = client:get("/certificates/" .. certificate.id) + local body = assert.res_status(200, res) + local json = cjson.decode(body) + assert.same({}, json.snis) + + local res = client:get("/certificates/" .. certificate_2.id) + local body = assert.res_status(200, res) + local json = cjson.decode(body) + assert.same({ "foo.com" }, json.snis) + end) end) describe("DELETE", function() - it("deletes a SNI", function() - local res = assert(client:send { - method = "DELETE", - path = "/snis/foo.com", - }) - + it("deletes a sni", function() + local res = client:delete("/snis/foo.com") assert.res_status(204, res) end) end) end) end) -end) +end diff --git a/spec/02-integration/05-proxy/05-ssl_spec.lua b/spec/02-integration/05-proxy/05-ssl_spec.lua index 84a6bdf023ab..0aca03743801 100644 --- a/spec/02-integration/05-proxy/05-ssl_spec.lua +++ b/spec/02-integration/05-proxy/05-ssl_spec.lua @@ -99,7 +99,7 @@ for _, strategy in helpers.each_strategy() do body = { cert = ssl_fixtures.cert, key = ssl_fixtures.key, - snis = "example.com,ssl1.com", + snis = { "example.com", "ssl1.com" }, }, headers = { ["Content-Type"] = "application/json" }, }) diff --git a/spec/02-integration/06-invalidations/02-core_entities_invalidations_spec.lua b/spec/02-integration/06-invalidations/02-core_entities_invalidations_spec.lua index ebc0fb9cbfce..f9b7259cf753 100644 --- a/spec/02-integration/06-invalidations/02-core_entities_invalidations_spec.lua +++ b/spec/02-integration/06-invalidations/02-core_entities_invalidations_spec.lua @@ -373,9 +373,9 @@ for _, strategy in helpers.each_strategy() do -- ssl_certificates ------------------- - describe("ssl_certificates / SNIs", function() + describe("ssl_certificates / snis", function() - local function get_cert(port, sni) + local function get_cert(port, sn) local pl_utils = require "pl.utils" local cmd = [[ @@ -385,7 +385,7 @@ for _, strategy in helpers.each_strategy() do -servername %s \ ]] - local _, _, stderr = pl_utils.executeex(string.format(cmd, port, sni)) + local _, _, stderr = pl_utils.executeex(string.format(cmd, port, sn)) return stderr end @@ -402,18 +402,14 @@ for _, strategy in helpers.each_strategy() do assert.matches("CN=localhost", cert_2, nil, true) end) - it("on certificate+SNI create", function() - local admin_res = assert(admin_client_1:send { - method = "POST", - path = "/certificates", + it("on certificate+sni create", function() + local admin_res = admin_client_1:post("/certificates", { body = { cert = ssl_fixtures.cert, key = ssl_fixtures.key, - snis = "ssl-example.com", + snis = { "ssl-example.com" }, }, - headers = { - ["Content-Type"] = "application/json", - } + headers = { ["Content-Type"] = "application/json" } }) assert.res_status(201, admin_res) @@ -430,23 +426,18 @@ for _, strategy in helpers.each_strategy() do end) it("on certificate delete+re-creation", function() - -- TODO: PATCH/PUT update are currently not possible + -- TODO: PATCH update are currently not possible -- with the admin API because snis have their name as their -- primary key and the DAO has limited support for such updates. - local admin_res = assert(admin_client_1:send { - method = "DELETE", - path = "/certificates/ssl-example.com", - }) + local admin_res = admin_client_1:delete("/certificates/ssl-example.com") assert.res_status(204, admin_res) - local admin_res = assert(admin_client_1:send { - method = "POST", - path = "/certificates", + local admin_res = admin_client_1:post("/certificates", { body = { cert = ssl_fixtures.cert, key = ssl_fixtures.key, - snis = "new-ssl-example.com", + snis = { "new-ssl-example.com" }, }, headers = { ["Content-Type"] = "application/json", @@ -474,7 +465,7 @@ for _, strategy in helpers.each_strategy() do it("on certificate update", function() -- update our certificate *without* updating the - -- attached SNI + -- attached sni local admin_res = assert(admin_client_1:send { method = "PATCH", @@ -501,63 +492,69 @@ for _, strategy in helpers.each_strategy() do assert.matches("CN=ssl-alt.com", cert_2, nil, true) end) - pending("on SNI update", function() - -- Pending: currently, SNIs cannot be updated: - -- - A PATCH updating the name property would not work, since - -- the URI path expects the current name, and so does the - -- query fetchign the row to be updated - -- - -- - -- - -- update our SNI but leave certificate untouched + it("on sni update via id", function() + local admin_res = admin_client_1:get("/snis") + local body = assert.res_status(200, admin_res) + local sni = assert(cjson.decode(body).data[1]) - local admin_res = assert(admin_client_1:send { - method = "PATCH", - path = "/snis/new-ssl-example.com", - body = { - name = "updated-sni.com", - }, - headers = { - ["Content-Type"] = "application/json", - }, + local admin_res = admin_client_1:patch("/snis/" .. sni.id, { + body = { name = "updated-sn-via-id.com" }, + headers = { ["Content-Type"] = "application/json" }, }) assert.res_status(200, admin_res) - -- no need to wait for workers propagation (lua-resty-worker-events) - -- because our test instance only has 1 worker + local cert_1_old = get_cert(8443, "new-ssl-example.com") + assert.matches("CN=localhost", cert_1_old, nil, true) + + local cert_1_new = get_cert(8443, "updated-sn-via-id.com") + assert.matches("CN=ssl-alt.com", cert_1_new, nil, true) - local cert_1_old_sni = get_cert(8443, "new-ssl-example.com") - assert.matches("CN=localhost", cert_1_old_sni, nil, true) + wait_for_propagation() + + local cert_2_old = get_cert(9443, "new-ssl-example.com") + assert.matches("CN=localhost", cert_2_old, nil, true) - local cert_1_new_sni = get_cert(8443, "updated-sni.com") - assert.matches("CN=updated-sni.com", cert_1_new_sni, nil, true) + local cert_2_new = get_cert(9443, "updated-sn-via-id.com") + assert.matches("CN=ssl-alt.com", cert_2_new, nil, true) + end) + + it("on sni update via name", function() + local admin_res = admin_client_1:patch("/snis/updated-sn-via-id.com", { + body = { name = "updated-sn.com" }, + headers = { ["Content-Type"] = "application/json" }, + }) + assert.res_status(200, admin_res) + + local cert_1_old = get_cert(8443, "updated-sn-via-id.com") + assert.matches("CN=localhost", cert_1_old, nil, true) + + local cert_1_new = get_cert(8443, "updated-sn.com") + assert.matches("CN=ssl-alt.com", cert_1_new, nil, true) + + wait_for_propagation() + + local cert_2_old = get_cert(9443, "updated-sn-via-id.com") + assert.matches("CN=localhost", cert_2_old, nil, true) + + local cert_2_new = get_cert(9443, "updated-sn.com") + assert.matches("CN=ssl-alt.com", cert_2_new, nil, true) end) it("on certificate delete", function() -- delete our certificate - local admin_res = assert(admin_client_1:send { - method = "GET", - path = "/certificates/new-ssl-example.com", - }) - local body = assert.res_status(200, admin_res) - local cert = cjson.decode(body) - - admin_res = assert(admin_client_1:send { - method = "DELETE", - path = "/certificates/" .. cert.id - }) + local admin_res = admin_client_1:delete("/certificates/updated-sn.com") assert.res_status(204, admin_res) -- no need to wait for workers propagation (lua-resty-worker-events) -- because our test instance only has 1 worker - local cert_1 = get_cert(8443, "new-ssl-example.com") + local cert_1 = get_cert(8443, "updated-sn.com") assert.matches("CN=localhost", cert_1, nil, true) wait_for_propagation() - local cert_2 = get_cert(9443, "new-ssl-example.com") + local cert_2 = get_cert(9443, "updated-sn.com") assert.matches("CN=localhost", cert_2, nil, true) end) end) diff --git a/spec/fixtures/blueprints.lua b/spec/fixtures/blueprints.lua index 726bd7919860..8c07e9dfbef7 100644 --- a/spec/fixtures/blueprints.lua +++ b/spec/fixtures/blueprints.lua @@ -65,14 +65,15 @@ local _M = {} function _M.new(dao, db) local res = {} - local ssl_name_seq = new_sequence("ssl-server-%d") - res.ssl_servers_names = new_blueprint(dao.ssl_servers_names, function() + local sni_seq = new_sequence("server-name-%d") + res.snis = new_blueprint(db.snis, function(overrides) return { - name = ssl_name_seq:next(), + name = sni_seq:next(), + certificate = overrides.certificate or res.certificates:insert(), } end) - res.ssl_certificates = new_blueprint(dao.ssl_certificates, function() + res.certificates = new_blueprint(db.certificates, function() return { cert = ssl_fixtures.cert, key = ssl_fixtures.key,