diff --git a/CHANGELOG.md b/CHANGELOG.md index 43ac44d3cfe9..fc7dcec9c7f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,18 +1,40 @@ ## [Unreleased][unreleased] +## [0.8.0] - 2016/04/13 + +This release includes support for PostgreSQL as Kong's primary datastore! + ### Breaking changes - Remove support for the long deprecated `/consumers/:consumer/keyauth/` and `/consumers/:consumer/basicauth/` routes (deprecated in `0.5.0`). The new routes (available since `0.5.0` too) use the real name of the plugin: `/consumers/:consumer/key-auth` and `/consumers/:consumer/basic-auth`. ### Added -- Support for PostgreSQL as Kong's primary datastore. [#331](https://github.com/Mashape/kong/issues/331) [#1054](https://github.com/Mashape/kong/issues/1054) +- Support for PostgreSQL 9.4+ as Kong's primary datastore. [#331](https://github.com/Mashape/kong/issues/331) [#1054](https://github.com/Mashape/kong/issues/1054) - Configurable Cassandra reading/writing consistency. [#1026](https://github.com/Mashape/kong/pull/1026) -- Admin API: oncluding pending and running timers count in the response to `/`. [#992](https://github.com/Mashape/kong/pull/992) +- Admin API: including pending and running timers count in the response to `/`. [#992](https://github.com/Mashape/kong/pull/992) - Plugins - **New correlation-id plugin**: assign unique identifiers to the requests processed by Kong. Courtesy of [@opyate](https://github.com/opyate). [#1094](https://github.com/Mashape/kong/pull/1094) - - JWT - - Add support for RS256 signed tokens thanks to [@kdstew](https://github.com/kdstew)! [#1053](https://github.com/Mashape/kong/pull/1053) + - LDAP: add support for LDAP authentication. [#1133](https://github.com/Mashape/kong/pull/1133) + - StatsD: add support for StatsD logging. [#1142](https://github.com/Mashape/kong/pull/1142) + - JWT: add support for RS256 signed tokens thanks to [@kdstew](https://github.com/kdstew)! [#1053](https://github.com/Mashape/kong/pull/1053) + - ACL: appends `X-Consumer-Groups` to the request, so the upstream service can check what groups the consumer belongs to. [#1154](https://github.com/Mashape/kong/pull/1154) + - Galileo (mashape-analytics): increase batch sending timeout to 30s. [#1091](https://github.com/Mashape/kong/pull/1091) +- Added `ttl_on_failure` option in the cluster configuration, to configure the TTL of failed nodes. [#1125](https://github.com/Mashape/kong/pull/1125) + +### Fixed + +- Introduce a new `port` option when connecting to your Cassandra cluster instead of using the CQL default (9042). [#1139](https://github.com/Mashape/kong/issues/1139) +- Plugins + - Request/Response Transformer: add missing migrations for upgrades from ` <= 0.5.x`. [#1064](https://github.com/Mashape/kong/issues/1064) + - OAuth2 + - Error responses comply to RFC 6749. [#1017](https://github.com/Mashape/kong/issues/1017) + - Handle multipart requests. [#1067](https://github.com/Mashape/kong/issues/1067) + - Make access_tokens correctly expire. [#1089](https://github.com/Mashape/kong/issues/1089) + +> **internal** +> - replace globals with singleton pattern thanks to [@mars](https://github.com/mars). +> - fixed resolution mismatches when using deep paths in the path resolver thanks to [siddharthkchatterjee](https://github.com/siddharthkchatterjee) ## [0.7.0] - 2016/02/24 @@ -521,7 +543,8 @@ First version running with Cassandra. - CLI `bin/kong` script. - Database migrations (using `db.lua`). -[unreleased]: https://github.com/mashape/kong/compare/0.7.0...next +[unreleased]: https://github.com/mashape/kong/compare/0.8.0...next +[0.8.0]: https://github.com/mashape/kong/compare/0.7.0...0.8.0 [0.7.0]: https://github.com/mashape/kong/compare/0.6.1...0.7.0 [0.6.1]: https://github.com/mashape/kong/compare/0.6.0...0.6.1 [0.6.0]: https://github.com/mashape/kong/compare/0.5.4...0.6.0 diff --git a/Makefile b/Makefile index 0957c616c782..6e9ccb7de870 100644 --- a/Makefile +++ b/Makefile @@ -22,8 +22,8 @@ dev: install echo $$rock already installed, skipping ; \ fi \ done; - bin/kong config -c kong.yml -e TEST - bin/kong config -c kong.yml -e DEVELOPMENT + bin/kong config -c kong.yml -e TEST -s TEST + bin/kong config -c kong.yml -e DEVELOPMENT -s DEVELOPMENT bin/kong migrations -c $(DEVELOPMENT_CONF) up clean: diff --git a/UPGRADE.md b/UPGRADE.md index b864734c552b..1a98c72edc56 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -22,6 +22,46 @@ $ kong reload [-c configuration_file] **Reminder**: `kong reload` leverages the Nginx `reload` signal and seamlessly starts new workers taking over the old ones until they all have been terminated. This will guarantee you no drop in your current incoming traffic. +## Upgrade to `0.8.x` + +No important breaking changes for this release, just be careful to not use the long deprecated routes `/consumers/:consumer/keyauth/` and `/consumers/:consumer/basicauth/` as instructed in the Changelog. As always, also make sure to check the configuration file for new properties (this release allows you to configure the read/write consistency of Cassandra). + +Let's talk about **PostgreSQL**. To use it instead of Cassandra, follow those steps: + +* Get your hands on a 9.4+ server (being compatible with Postgres 9.4 allows you to use [Amazon RDS](https://aws.amazon.com/rds/)) +* Create a database, (maybe a user too?), let's say `kong` +* Update your Kong configuration: + +```yaml +# as always, be careful about your YAML formatting +database: postgres +postgres: + host: "127.0.0.1" + port: 5432 + user: kong + password: kong + database: kong +``` + +As usual, migrations should run from kong start, but as a reminder and just in case, here are some tips: + +Reset the database with (careful, you'll lose all data): +``` +$ kong migrations reset --config kong.yml +``` + +Run the migrations manually with: +``` +$ kong migrations up --config kong.yml +``` + +If needed, list your migrations for debug purposes with: +``` +$ kong migrations list --config kong.yml +``` + +**Note**: This release does not provide a mean to migrate from Cassandra to PostgreSQL. Additionally, we recommend that you **do not** use `kong reload` if you switch your cluster from Cassandra to PostgreSQL. Instead, we recommend that you migrate by spawning a new cluster and gradually redirect your traffic before decomissioning your old nodes. + ## Upgrade to `0.7.x` If you are running a source installation, you will need to upgrade OpenResty to its `1.9.7.*` version. The good news is that this family of releases does not need to patch the NGINX core anymore to enable SSL support. If you install Kong from one of the distribution packages, they already include the appropriate OpenResty, simply download and install the appropriate package for your platform. diff --git a/kong-0.7.0-0.rockspec b/kong-0.8.0-0.rockspec similarity index 95% rename from kong-0.7.0-0.rockspec rename to kong-0.8.0-0.rockspec index 1bdcb48c9897..44008405153d 100644 --- a/kong-0.7.0-0.rockspec +++ b/kong-0.8.0-0.rockspec @@ -1,9 +1,9 @@ package = "kong" -version = "0.7.0-0" +version = "0.8.0-0" supported_platforms = {"linux", "macosx"} source = { url = "git://github.com/Mashape/kong", - tag = "0.7.0" + tag = "0.8.0" } description = { summary = "Kong is a scalable and customizable API Management Layer built on top of Nginx.", @@ -20,7 +20,7 @@ dependencies = { "yaml ~> 1.1.2-1", "lapis ~> 1.3.1-1", "stringy ~> 0.4-1", - "lua-cassandra ~> 0.5.0", + "lua-cassandra ~> 0.5.1", "pgmoon ~> 1.4.0", "multipart ~> 0.3-2", "lua-path ~> 0.2.3-1", @@ -34,7 +34,8 @@ dependencies = { "lrexlib-pcre ~> 2.7.2-1", "lua-llthreads2 ~> 0.1.3-1", "luacrypto >= 0.3.2-1", - "luasyslog >= 1.0.0-2" + "luasyslog >= 1.0.0-2", + "lua_pack ~> 1.0.4-0" } build = { type = "builtin", @@ -242,6 +243,12 @@ build = { ["kong.plugins.hmac-auth.api"] = "kong/plugins/hmac-auth/api.lua", ["kong.plugins.hmac-auth.daos"] = "kong/plugins/hmac-auth/daos.lua", + ["kong.plugins.ldap-auth.handler"] = "kong/plugins/ldap-auth/handler.lua", + ["kong.plugins.ldap-auth.access"] = "kong/plugins/ldap-auth/access.lua", + ["kong.plugins.ldap-auth.schema"] = "kong/plugins/ldap-auth/schema.lua", + ["kong.plugins.ldap-auth.ldap"] = "kong/plugins/ldap-auth/ldap.lua", + ["kong.plugins.ldap-auth.asn1"] = "kong/plugins/ldap-auth/asn1.lua", + ["kong.plugins.syslog.handler"] = "kong/plugins/syslog/handler.lua", ["kong.plugins.syslog.schema"] = "kong/plugins/syslog/schema.lua", @@ -250,7 +257,11 @@ build = { ["kong.plugins.datadog.handler"] = "kong/plugins/datadog/handler.lua", ["kong.plugins.datadog.schema"] = "kong/plugins/datadog/schema.lua", - ["kong.plugins.datadog.statsd_logger"] = "kong/plugins/datadog/statsd_logger.lua" + ["kong.plugins.datadog.statsd_logger"] = "kong/plugins/datadog/statsd_logger.lua", + + ["kong.plugins.statsd.handler"] = "kong/plugins/statsd/handler.lua", + ["kong.plugins.statsd.schema"] = "kong/plugins/statsd/schema.lua", + ["kong.plugins.statsd.statsd_logger"] = "kong/plugins/statsd/statsd_logger.lua" }, install = { conf = { "kong.yml" }, diff --git a/kong.yml b/kong.yml index 01bf213d6b70..15dd99f83565 100644 --- a/kong.yml +++ b/kong.yml @@ -87,6 +87,12 @@ ## Key for encrypting network traffic within Kong. Must be a base64-encoded 16-byte key. # encrypt: "foo" + ###### + ## The TTL (time to live), in seconds, of a node in the cluster when it stops sending healthcheck pings, maybe + ## because of a failure. If the node is not able to send a new healthcheck before the expiration, then new nodes + ## in the cluster will stop attempting to connect to it on startup. + # ttl_on_failure: 3600 + ###### ## Specify which database to use. Only "cassandra" is currently available. # database: cassandra @@ -94,10 +100,17 @@ ###### ## PostgreSQL configuration # postgres: -# host: "127.0.0.1" -# port: 5432 -# user: kong -# database: kong + # host: "127.0.0.1" + # port: 5432 + + ###### + ## Name of the database used by Kong. Will be created if it does not exist. + # database: kong + + ##### + ## User authentication settings + # user: "" + # password: "" ###### ## Cassandra configuration (keyspace, authentication, client-to-node encryption) @@ -107,6 +120,10 @@ # contact_points: # - "127.0.0.1:9042" + ## Port on which your cluster's peers (other than your contact_points) + ## are listening on. + # port: 9042 + ###### ## Name of the keyspace used by Kong. Will be created if it does not exist. # keyspace: kong diff --git a/kong/cli/services/serf.lua b/kong/cli/services/serf.lua index 8af73580ae32..5db39e99562e 100644 --- a/kong/cli/services/serf.lua +++ b/kong/cli/services/serf.lua @@ -160,7 +160,7 @@ function Serf:_add_node() local _, err = self._dao_factory.nodes:insert({ name = name, cluster_listening_address = stringy.strip(addr) - }, {ttl = 3600}) + }, {ttl = self._configuration.cluster.ttl_on_failure}) if err then return false, err end diff --git a/kong/constants.lua b/kong/constants.lua index 4b8c101159c0..c8b6fd0f75a8 100644 --- a/kong/constants.lua +++ b/kong/constants.lua @@ -13,7 +13,7 @@ return { "http-log", "key-auth", "hmac-auth", "basic-auth", "ip-restriction", "mashape-analytics", "request-transformer", "response-transformer", "request-size-limiting", "rate-limiting", "response-ratelimiting", "syslog", - "loggly", "datadog", "runscope" + "loggly", "datadog", "runscope", "ldap-auth", "statsd" }, -- Non standard headers, specific to Kong HEADERS = { @@ -23,7 +23,10 @@ return { CONSUMER_ID = "X-Consumer-ID", CONSUMER_CUSTOM_ID = "X-Consumer-Custom-ID", CONSUMER_USERNAME = "X-Consumer-Username", - CREDENTIAL_USERNAME = "X-Credential-Username" + CREDENTIAL_USERNAME = "X-Credential-Username", + RATELIMIT_LIMIT = "X-RateLimit-Limit", + RATELIMIT_REMAINING = "X-RateLimit-Remaining", + CONSUMER_GROUPS = "X-Consumer-Groups" }, RATELIMIT = { PERIODS = { diff --git a/kong/core/cluster.lua b/kong/core/cluster.lua index dd35d3c80636..a87918af9833 100644 --- a/kong/core/cluster.lua +++ b/kong/core/cluster.lua @@ -79,7 +79,7 @@ local function send_keepalive(premature) ngx.log(ngx.ERR, tostring(err)) elseif #nodes == 1 then local node = nodes[1] - local _, err = singletons.dao.nodes:update(node, node) + local _, err = singletons.dao.nodes:update(node, node, {ttl=singletons.configuration.cluster.ttl_on_failure}) if err then ngx.log(ngx.ERR, tostring(err)) end diff --git a/kong/core/reports.lua b/kong/core/reports.lua index 0332fdaa830d..205b25e3ebba 100644 --- a/kong/core/reports.lua +++ b/kong/core/reports.lua @@ -1,6 +1,7 @@ local syslog = require "kong.tools.syslog" local cache = require "kong.tools.database_cache" local utils = require "kong.tools.utils" +local singletons = require "kong.singletons" local unique_str = utils.random_string() local enabled = false @@ -29,7 +30,7 @@ local function send_ping(premature) if elapsed and elapsed == 0 then local reqs = cache.get(cache.requests_key()) if not reqs then reqs = 0 end - syslog.log({signal = "ping", requests = reqs, unique_id = unique_str}) + syslog.log({signal = "ping", requests = reqs, unique_id = unique_str, database = singletons.configuration.database}) cache.incr(cache.requests_key(), -reqs) -- Reset counter end create_timer(INTERVAL, send_ping) diff --git a/kong/dao/cassandra_db.lua b/kong/dao/cassandra_db.lua index 5fdee4c8d64e..a393f9f47918 100644 --- a/kong/dao/cassandra_db.lua +++ b/kong/dao/cassandra_db.lua @@ -26,6 +26,9 @@ function CassandraDB:new(options) prepared_shm = "cassandra_prepared", contact_points = options.contact_points, keyspace = options.keyspace, + protocol_options = { + default_port = options.port + }, query_options = { prepare = true }, @@ -298,7 +301,7 @@ function CassandraDB:count(table_name, tbl, schema) end end -function CassandraDB:update(table_name, schema, constraints, filter_keys, values, nils, full, options) +function CassandraDB:update(table_name, schema, constraints, filter_keys, values, nils, full, model, options) -- must check unique constaints manually too local err = check_unique_constraints(self, table_name, constraints, values, filter_keys, true) if err then @@ -309,6 +312,30 @@ function CassandraDB:update(table_name, schema, constraints, filter_keys, values return nil, err end + -- Cassandra TTL on update is per-column and not per-row, and TTLs cannot be updated on primary keys. + -- Not only that, but TTL on other rows can only be incremented, and not decremented. Because of all + -- of these limitations, the only way to make this happen is to do an upsert operation. + -- This implementation can be changed once Cassandra closes this issue: https://issues.apache.org/jira/browse/CASSANDRA-9312 + if options and options.ttl then + if schema.primary_key and #schema.primary_key == 1 and filter_keys[schema.primary_key[1]] then + local row, err = self:find(table_name, schema, filter_keys) + if err then + return nil, err + elseif row then + for k, v in pairs(row) do + if not values[k] then + model[k] = v -- Populate the model to be used later for the insert + end + end + + -- Insert without any contraint check, since the check has already been executed + return self:insert(table_name, schema, model, {unique={}, foreign={}}, options) + end + else + return nil, "Cannot update TTL on entities that have more than one primary_key" + end + end + local sets, args, where = {}, {} for col, value in pairs(values) do local field = schema.fields[col] diff --git a/kong/dao/dao.lua b/kong/dao/dao.lua index c6fa37a7d6c7..bbd39dbf7e18 100644 --- a/kong/dao/dao.lua +++ b/kong/dao/dao.lua @@ -265,7 +265,7 @@ function DAO:update(tbl, filter_keys, options) fix(old, values, self.schema) end - local res, err = self.db:update(self.table, self.schema, self.constraints, primary_keys, values, nils, full_update, options) + local res, err = self.db:update(self.table, self.schema, self.constraints, primary_keys, values, nils, full_update, model, options) if err then return nil, err elseif res then diff --git a/kong/dao/migrations/postgres.lua b/kong/dao/migrations/postgres.lua index 336e8e0f9b76..ae88af9b82ea 100644 --- a/kong/dao/migrations/postgres.lua +++ b/kong/dao/migrations/postgres.lua @@ -1,12 +1,14 @@ return { { name = "2015-01-12-175310_skeleton", - up = [[ - CREATE TABLE IF NOT EXISTS schema_migrations( - id text PRIMARY KEY, - migrations varchar(100)[] - ); - ]], + up = function(db, properties) + return db:queries [[ + CREATE TABLE IF NOT EXISTS schema_migrations( + id text PRIMARY KEY, + migrations varchar(100)[] + ); + ]] + end, down = [[ DROP TABLE schema_migrations; ]] diff --git a/kong/dao/postgres_db.lua b/kong/dao/postgres_db.lua index 95fd912b9b65..6c9487b46154 100644 --- a/kong/dao/postgres_db.lua +++ b/kong/dao/postgres_db.lua @@ -1,6 +1,7 @@ local BaseDB = require "kong.dao.base_db" local Errors = require "kong.dao.errors" local uuid = require "lua_uuid" +local utils = require "kong.tools.utils" local TTL_CLEANUP_INTERVAL = 60 -- 1 minute @@ -120,16 +121,17 @@ end -- Querying -function PostgresDB:query(...) - PostgresDB.super.query(self, ...) +function PostgresDB:query(query) + PostgresDB.super.query(self, query) - local pg = pgmoon.new(self:_get_conn_options()) + local conn_opts = self:_get_conn_options() + local pg = pgmoon.new(conn_opts) local ok, err = pg:connect() if not ok then return nil, Errors.db(err) end - local res, err = pg:query(...) + local res, err = pg:query(query) if ngx and ngx.get_phase() ~= "init" then pg:keepalive() else @@ -222,12 +224,22 @@ function PostgresDB:serialize_timestamps(tbl, schema) end function PostgresDB:ttl(tbl, table_name, schema, ttl) - if not schema.primary_key or #schema.primary_key > 1 then + if not schema.primary_key or #schema.primary_key ~= 1 then return false, "Cannot set a TTL if the entity has no primary key, or has more than one primary key" end local primary_key_type = self:retrieve_primary_key_type(schema, table_name) - local expire_at = tbl.created_at + (ttl * 1000) + + -- Get current server time + local query = "SELECT extract(epoch from now() at time zone 'utc')::bigint*1000 as timestamp;" + local res, err = self:query(query) + if err then + return false, err + end + + -- The expiration is always based on the current time + local expire_at = res[1].timestamp + (ttl * 1000) + local query = string.format("SELECT upsert_ttl('%s', %s, '%s', '%s', to_timestamp(%d/1000) at time zone 'UTC')", tbl[schema.primary_key[1]], primary_key_type == "uuid" and "'"..tbl[schema.primary_key[1]].."'" or "NULL", schema.primary_key[1], table_name, expire_at) @@ -358,7 +370,7 @@ function PostgresDB:count(table_name, tbl, schema) end end -function PostgresDB:update(table_name, schema, _, filter_keys, values, nils, full, options) +function PostgresDB:update(table_name, schema, _, filter_keys, values, nils, full, _, options) local args = {} local values, err = self:serialize_timestamps(values, schema) if err then @@ -417,7 +429,12 @@ end -- Migrations function PostgresDB:queries(queries) - return select(2, self:query(queries)) + if utils.strip(queries) ~= "" then + local err = select(2, self:query(queries)) + if err then + return err + end + end end function PostgresDB:drop_table(table_name) diff --git a/kong/meta.lua b/kong/meta.lua index 5427ff0e5e2a..e7f1a1721b3b 100644 --- a/kong/meta.lua +++ b/kong/meta.lua @@ -1,6 +1,6 @@ local version = setmetatable({ major = 0, - minor = 7, + minor = 8, patch = 0, --pre_release = "alpha" }, { diff --git a/kong/plugins/acl/handler.lua b/kong/plugins/acl/handler.lua index e995a3ca11b8..c0fe36c8f095 100644 --- a/kong/plugins/acl/handler.lua +++ b/kong/plugins/acl/handler.lua @@ -3,6 +3,11 @@ local BasePlugin = require "kong.plugins.base_plugin" local cache = require "kong.tools.database_cache" local responses = require "kong.tools.responses" local utils = require "kong.tools.utils" +local constants = require "kong.constants" + +local table_insert = table.insert +local table_concat = table.concat +local ipairs = ipairs local ACLHandler = BasePlugin:extend() @@ -62,6 +67,13 @@ function ACLHandler:access(conf) if block then return responses.send_HTTP_FORBIDDEN("You cannot consume this service") end + + -- Prepare header + local str_acls = {} + for _, v in ipairs(acls) do + table_insert(str_acls, v.group) + end + ngx.req.set_header(constants.HEADERS.CONSUMER_GROUPS, table_concat(str_acls, ", ")) end return ACLHandler diff --git a/kong/plugins/ldap-auth/access.lua b/kong/plugins/ldap-auth/access.lua new file mode 100644 index 000000000000..4a75c8aa197b --- /dev/null +++ b/kong/plugins/ldap-auth/access.lua @@ -0,0 +1,114 @@ +local responses = require "kong.tools.responses" +local constants = require "kong.constants" +local cache = require "kong.tools.database_cache" +local base64 = require "base64" +local ldap = require "kong.plugins.ldap-auth.ldap" + +local match = string.match +local ngx_log = ngx.log +local request = ngx.req +local ngx_error = ngx.ERR +local ngx_debug = ngx.DEBUG +local ngx_socket_tcp = ngx.socket.tcp +local tostring = tostring + +local AUTHORIZATION = "authorization" +local PROXY_AUTHORIZATION = "proxy-authorization" + +local _M = {} + +local function retrieve_credentials(authorization_header_value, conf) + local username, password + if authorization_header_value then + local cred = match(authorization_header_value, "%s*[ldap|LDAP]%s+(.*)") + + if cred ~= nil then + local decoded_cred = base64.decode(cred) + username, password = match(decoded_cred, "(.+):(.+)") + end + end + return username, password +end + +local function ldap_authenticate(given_username, given_password, conf) + local is_authenticated + local error, suppressed_err, ok + local who = conf.attribute.."="..given_username..","..conf.base_dn + + local sock = ngx_socket_tcp() + sock:settimeout(conf.timeout) + ok, error = sock:connect(conf.ldap_host, conf.ldap_port) + if not ok then + ngx_log(ngx_error, "[ldap-auth] failed to connect to "..conf.ldap_host..":"..tostring(conf.ldap_port)..": ", error) + return responses.send_HTTP_INTERNAL_SERVER_ERROR(error) + end + + if conf.start_tls then + local success, error = ldap.start_tls(sock) + if not success then + return false, error + end + local _, error = sock:sslhandshake(true, conf.ldap_host, conf.verify_ldap_host) + if error ~= nil then + return false, "failed to do SSL handshake with "..conf.ldap_host..":"..tostring(conf.ldap_port)..": ".. error + end + end + + is_authenticated, error = ldap.bind_request(sock, who, given_password) + + ok, suppressed_err = sock:setkeepalive(conf.keepalive) + if not ok then + ngx_log(ngx_error, "[ldap-auth] failed to keepalive to "..conf.ldap_host..":"..tostring(conf.ldap_port)..": ", suppressed_err) + end + return is_authenticated, error +end + +local function authenticate(conf, given_credentials) + local given_username, given_password = retrieve_credentials(given_credentials) + if given_username == nil then + return false + end + + local credential = cache.get_or_set(cache.ldap_credential_key(given_username), function() + ngx_log(ngx_debug, "[ldap-auth] authenticating user against LDAP server: "..conf.ldap_host..":"..conf.ldap_port) + + local ok, err = ldap_authenticate(given_username, given_password, conf) + if err ~= nil then ngx_log(ngx_error, err) end + if not ok then + return nil + end + return {username = given_username, password = given_password} + end, conf.cache_ttl) + + return credential and credential.password == given_password, credential +end + +function _M.execute(conf) + local authorization_value = request.get_headers()[AUTHORIZATION] + local proxy_authorization_value = request.get_headers()[PROXY_AUTHORIZATION] + + -- If both headers are missing, return 401 + if not (authorization_value or proxy_authorization_value) then + ngx.header["WWW-Authenticate"] = 'LDAP realm="kong"' + return responses.send_HTTP_UNAUTHORIZED() + end + + local is_authorized, credential = authenticate(conf, proxy_authorization_value) + if not is_authorized then + is_authorized, credential = authenticate(conf, authorization_value) + end + + if not is_authorized then + return responses.send_HTTP_FORBIDDEN("Invalid authentication credentials") + end + + if conf.hide_credentials then + request.clear_header(AUTHORIZATION) + request.clear_header(PROXY_AUTHORIZATION) + end + + request.set_header(constants.HEADERS.CREDENTIAL_USERNAME, credential.username) + ngx.ctx.authenticated_credential = credential +end + +return _M diff --git a/kong/plugins/ldap-auth/asn1.lua b/kong/plugins/ldap-auth/asn1.lua new file mode 100644 index 000000000000..a9b5a6ed863a --- /dev/null +++ b/kong/plugins/ldap-auth/asn1.lua @@ -0,0 +1,360 @@ +require "lua_pack" + +local bpack = string.pack +local bunpack = string.unpack +local math = math +local bit = bit +local setmetatable = setmetatable +local table = table +local string_reverse = string.reverse +local string_char = string.char + +local _M = {} + +_M.BERCLASS = { + Universal = 0, + Application = 64, + ContextSpecific = 128, + Private = 192 +} + +_M.ASN1Decoder = { + + new = function(self,o) + o = o or {} + setmetatable(o, self) + self.__index = self + o:registerBaseDecoders() + return o + end, + + decode = function(self, encStr, pos) + local etype, elen + local newpos = pos + newpos, etype = bunpack(encStr, "X1", newpos) + newpos, elen = self.decodeLength(encStr, newpos) + if self.decoder[etype] then + return self.decoder[etype](self, encStr, elen, newpos) + else + return newpos, nil + end + end, + + setStopOnError = function(self, val) + self.stoponerror = val + end, + + registerBaseDecoders = function(self) + self.decoder = {} + + self.decoder["0A"] = function(self, encStr, elen, pos) + return self.decodeInt(encStr, elen, pos) + end + + self.decoder["8A"] = function(self, encStr, elen, pos) + return bunpack(encStr, "A" .. elen, pos) + end + + self.decoder["31"] = function(self, encStr, elen, pos) + return pos, nil + end + + -- Boolean + self.decoder["01"] = function(self, encStr, elen, pos) + local val = bunpack(encStr, "X", pos) + if val ~= "FF" then + return pos, true + else + return pos, false + end + end + + -- Integer + self.decoder["02"] = function(self, encStr, elen, pos) + return self.decodeInt(encStr, elen, pos) + end + + -- Octet String + self.decoder["04"] = function(self, encStr, elen, pos) + return bunpack(encStr, "A" .. elen, pos) + end + + -- Null + self.decoder["05"] = function(self, encStr, elen, pos) + return pos, false + end + + -- Object Identifier + self.decoder["06"] = function(self, encStr, elen, pos) + return self:decodeOID(encStr, elen, pos) + end + + -- Context specific tags + self.decoder["30"] = function(self, encStr, elen, pos) + return self:decodeSeq(encStr, elen, pos) + end + end, + + registerTagDecoders = function(self, tagDecoders) + self:registerBaseDecoders() + for k, v in pairs(tagDecoders) do + self.decoder[k] = v + end + end, + + decodeLength = function(encStr, pos) + local elen + pos, elen = bunpack(encStr, 'C', pos) + if (elen > 128) then + elen = elen - 128 + local elenCalc = 0 + local elenNext + for i = 1, elen do + elenCalc = elenCalc * 256 + pos, elenNext = bunpack(encStr, 'C', pos) + elenCalc = elenCalc + elenNext + end + elen = elenCalc + end + return pos, elen + end, + + decodeSeq = function(self, encStr, len, pos) + local seq = {} + local sPos = 1 + local sStr + pos, sStr = bunpack(encStr, "A" .. len, pos) + while (sPos < len) do + local newSeq + sPos, newSeq = self:decode(sStr, sPos) + if (not(newSeq) and self.stoponerror) then break end + table.insert(seq, newSeq) + end + return pos, seq + end, + + decode_oid_component = function(encStr, pos) + local octet + local n = 0 + + repeat + pos, octet = bunpack(encStr, "b", pos) + n = n * 128 + bit.band(0x7F, octet) + until octet < 128 + + return pos, n + end, + + decodeOID = function(self, encStr, len, pos) + local last + local oid = {} + local octet + + last = pos + len - 1 + if pos <= last then + oid._snmp = '06' + pos, octet = bunpack(encStr, "C", pos) + oid[2] = math.fmod(octet, 40) + octet = octet - oid[2] + oid[1] = octet/40 + end + + while pos <= last do + local c + pos, c = self.decode_oid_component(encStr, pos) + oid[#oid + 1] = c + end + + return pos, oid + end, + + decodeInt = function(encStr, len, pos) + local hexStr + pos, hexStr = bunpack(encStr, "X" .. len, pos) + local value = tonumber(hexStr, 16) + if (value >= math.pow(256, len)/2) then + value = value - math.pow(256, len) + end + return pos, value + end +} + +_M.ASN1Encoder = { + + new = function(self) + local o = {} + setmetatable(o, self) + self.__index = self + o:registerBaseEncoders() + return o + end, + + encodeSeq = function(self, seqData) + return bpack('XAA' , '30', self.encodeLength(#seqData), seqData) + end, + + encode = function(self, val) + local vtype = type(val) + + if self.encoder[vtype] then + return self.encoder[vtype](self,val) + end + end, + + registerTagEncoders = function(self, tagEncoders) + self:registerBaseEncoders() + for k, v in pairs(tagEncoders) do + self.encoder[k] = v + end + end, + + registerBaseEncoders = function(self) + self.encoder = {} + + self.encoder['table'] = function(self, val) + if (val._ldap == '0A') then + local ival = self.encodeInt(val[1]) + local len = self.encodeLength(#ival) + return bpack('XAA', '0A', len, ival) + end + if (val._ldaptype) then + local len + if val[1] == nil or #val[1] == 0 then + return bpack('XC', val._ldaptype, 0) + else + len = self.encodeLength(#val[1]) + return bpack('XAA', val._ldaptype, len, val[1]) + end + end + + local encVal = "" + for _, v in ipairs(val) do + encVal = encVal .. self.encode(v) -- todo: buffer? + end + local tableType = "\x30" + if (val["_snmp"]) then + tableType = bpack("X", val["_snmp"]) + end + return bpack('AAA', tableType, self.encodeLength(#encVal), encVal) + end + + -- Boolean encoder + self.encoder['boolean'] = function(self, val) + if val then + return bpack('X','01 01 FF') + else + return bpack('X', '01 01 00') + end + end + + -- Integer encoder + self.encoder['number'] = function(self, val) + local ival = self.encodeInt(val) + local len = self.encodeLength(#ival) + return bpack('XAA', '02', len, ival) + end + + -- Octet String encoder + self.encoder['string'] = function(self, val) + local len = self.encodeLength(#val) + return bpack('XAA', '04', len, val) + end + + -- Null encoder + self.encoder['nil'] = function(self, val) + return bpack('X', '05 00') + end + + end, + + encode_oid_component = function(n) + local parts = {} + parts[1] = string_char(bit.mod(n, 128)) + while n >= 128 do + n = bit.rshift(n, 7) + parts[#parts + 1] = string_char(bit.mod(n, 128) + 0x80) + end + return string_reverse(table.concat(parts)) + end, + + encodeInt = function(val) + local lsb = 0 + if val > 0 then + local valStr = "" + while (val > 0) do + lsb = math.fmod(val, 256) + valStr = valStr .. bpack('C', lsb) + val = math.floor(val/256) + end + if lsb > 127 then + valStr = valStr .. "\0" + end + + return string_reverse(valStr) + elseif val < 0 then + local i = 1 + local tcval = val + 256 + while tcval <= 127 do + tcval = tcval + (math.pow(256, i) * 255) + i = i+1 + end + local valStr = "" + while (tcval > 0) do + lsb = math.fmod(tcval, 256) + valStr = valStr .. bpack("C", lsb) + tcval = math.floor(tcval/256) + end + return string_reverse(valStr) + else -- val == 0 + return bpack("x") + end + end, + + encodeLength = function(len) + if len < 128 then + return string_char(len) + else + local parts = {} + + while len > 0 do + parts[#parts + 1] = string_char(bit.mod(len, 256)) + len = bit.rshift(len, 8) + end + + return string_char(#parts + 0x80) .. string_reverse(table.concat(parts)) + end + end +} + +function _M.BERtoInt(class, constructed, number) + local asn1_type = class + number + + if constructed == true then + asn1_type = asn1_type + 32 + end + + return asn1_type +end + +function _M.intToBER(i) + local ber = {} + if bit.band(i, _M.BERCLASS.Application) == _M.BERCLASS.Application then + ber.class = _M.BERCLASS.Application + elseif bit.band(i, _M.BERCLASS.ContextSpecific) == _M.BERCLASS.ContextSpecific then + ber.class = _M.BERCLASS.ContextSpecific + elseif bit.band(i, _M.BERCLASS.Private) == _M.BERCLASS.Private then + ber.class = _M.BERCLASS.Private + else + ber.class = _M.BERCLASS.Universal + end + if bit.band(i, 32) == 32 then + ber.constructed = true + ber.number = i - ber.class - 32 + else + ber.primitive = true + ber.number = i - ber.class + end + return ber +end + +return _M diff --git a/kong/plugins/ldap-auth/handler.lua b/kong/plugins/ldap-auth/handler.lua new file mode 100644 index 000000000000..62129d982f5f --- /dev/null +++ b/kong/plugins/ldap-auth/handler.lua @@ -0,0 +1,17 @@ +local access = require "kong.plugins.ldap-auth.access" +local BasePlugin = require "kong.plugins.base_plugin" + +local LdapAuthHandler = BasePlugin:extend() + +function LdapAuthHandler:new() + LdapAuthHandler.super.new(self, "ldap-auth") +end + +function LdapAuthHandler:access(conf) + LdapAuthHandler.super.access(self) + access.execute(conf) +end + +LdapAuthHandler.PRIORITY = 1000 + +return LdapAuthHandler diff --git a/kong/plugins/ldap-auth/ldap.lua b/kong/plugins/ldap-auth/ldap.lua new file mode 100644 index 000000000000..2fe8cf463621 --- /dev/null +++ b/kong/plugins/ldap-auth/ldap.lua @@ -0,0 +1,144 @@ +local asn1 = require "kong.plugins.ldap-auth.asn1" +local bunpack = string.unpack + +local string_format = string.format + +local _M = {} + +local ldapMessageId = 1 + +local ERROR_MSG = { + [1] = "Initialization of LDAP library failed.", + [4] = "Size limit exceeded.", + [13] = "Confidentiality required", + [32] = "No such object", + [34] = "Invalid DN", + [49] = "The supplied credential is invalid." +} + +local APPNO = { + BindRequest = 0, + BindResponse = 1, + UnbindRequest = 2, + ExtendedRequest = 23, + ExtendedResponse = 24 +} + +local function encodeLDAPOp(encoder, appno, isConstructed, data) + local asn1_type = asn1.BERtoInt(asn1.BERCLASS.Application, isConstructed, appno) + return encoder:encode({ _ldaptype = string_format("%X", asn1_type), data }) +end + +local function claculate_payload_length(encStr, pos, socket) + local elen + pos, elen = bunpack(encStr, 'C', pos) + if (elen > 128) then + elen = elen - 128 + local elenCalc = 0 + local elenNext + for i = 1, elen do + elenCalc = elenCalc * 256 + encStr = encStr..socket:receive(1) + pos, elenNext = bunpack(encStr, 'C', pos) + elenCalc = elenCalc + elenNext + end + elen = elenCalc + end + return pos, elen +end + +function _M.bind_request(socket, username, password) + local encoder = asn1.ASN1Encoder:new() + local decoder = asn1.ASN1Decoder:new() + + local ldapAuth = encoder:encode({ _ldaptype = 80, password }) + local bindReq = encoder:encode(3) .. encoder:encode(username) .. ldapAuth + local ldapMsg = encoder:encode(ldapMessageId) .. encodeLDAPOp(encoder, APPNO.BindRequest, true, bindReq) + local packet + local pos, packet_len, tmp, _ + local response = {} + + packet = encoder:encodeSeq(ldapMsg) + ldapMessageId = ldapMessageId +1 + socket:send(packet) + packet = socket:receive(2) + _, packet_len = claculate_payload_length(packet, 2, socket) + + packet = socket:receive(packet_len) + pos, response.messageID = decoder:decode(packet, 1) + pos, tmp = bunpack(packet, "C", pos) + pos = decoder.decodeLength(packet, pos) + response.protocolOp = asn1.intToBER(tmp) + + if response.protocolOp.number ~= APPNO.BindResponse then + return false, string_format("Received incorrect Op in packet: %d, expected %d", response.protocolOp.number, APPNO.BindResponse) + end + + pos, response.resultCode = decoder:decode(packet, pos) + + if (response.resultCode ~= 0) then + local error_msg + pos, response.matchedDN = decoder:decode(packet, pos) + _, response.errorMessage = decoder:decode(packet, pos) + error_msg = ERROR_MSG[response.resultCode] + return false, string_format("\n Error: %s\n Details: %s", + error_msg or "Unknown error occurred (code: " .. response.resultCode .. + ")", response.errorMessage or "") + else + return true + end +end + + +function _M.unbind_request(socket) + local ldapMsg, packet + local encoder = asn1.ASN1Encoder:new() + + ldapMessageId = ldapMessageId +1 + ldapMsg = encoder:encode(ldapMessageId) .. encodeLDAPOp(encoder, APPNO.UnbindRequest, false, nil) + packet = encoder:encodeSeq(ldapMsg) + socket:send(packet) + return true, "" +end + +function _M.start_tls(socket) + + local ldapMsg, pos, packet, packet_len, tmp, _ + local response = {} + local encoder = asn1.ASN1Encoder:new() + local decoder = asn1.ASN1Decoder:new() + + local method_name = encoder:encode({_ldaptype = 80, "1.3.6.1.4.1.1466.20037"}) + ldapMessageId = ldapMessageId +1 + ldapMsg = encoder:encode(ldapMessageId) .. encodeLDAPOp(encoder, APPNO.ExtendedRequest, true, method_name) + packet = encoder:encodeSeq(ldapMsg) + socket:send(packet) + packet = socket:receive(2) + _, packet_len = claculate_payload_length(packet, 2, socket) + + packet = socket:receive(packet_len) + pos, response.messageID = decoder:decode(packet, 1) + pos, tmp = bunpack(packet, "C", pos) + pos = decoder.decodeLength(packet, pos) + response.protocolOp = asn1.intToBER(tmp) + + if response.protocolOp.number ~= APPNO.ExtendedResponse then + return false, string_format("Received incorrect Op in packet: %d, expected %d", response.protocolOp.number, APPNO.ExtendedResponse) + end + + pos, response.resultCode = decoder:decode(packet, pos) + + if (response.resultCode ~= 0) then + local error_msg + pos, response.matchedDN = decoder:decode(packet, pos) + _, response.errorMessage = decoder:decode(packet, pos) + error_msg = ERROR_MSG[response.resultCode] + return false, string_format("\n Error: %s\n Details: %s", + error_msg or "Unknown error occurred (code: " .. response.resultCode .. + ")", response.errorMessage or "") + else + return true + end +end + +return _M; diff --git a/kong/plugins/ldap-auth/schema.lua b/kong/plugins/ldap-auth/schema.lua new file mode 100644 index 000000000000..940477d95f6b --- /dev/null +++ b/kong/plugins/ldap-auth/schema.lua @@ -0,0 +1,14 @@ +return { +fields = { + ldap_host = {required = true, type = "string"}, + ldap_port = {required = true, type = "number"}, + start_tls = {required = true, type = "boolean", default = false}, + verify_ldap_host = {required = true, type = "boolean", default = false}, + base_dn = {required = true, type = "string"}, + attribute = {required = true, type = "string"}, + cache_ttl = {required = true, type = "number", default = 60}, + hide_credentials = {type = "boolean", default = false}, + timeout = {type = "number", default = 10000}, + keepalive = {type = "number", default = 60000}, + } +} diff --git a/kong/plugins/statsd/handler.lua b/kong/plugins/statsd/handler.lua new file mode 100644 index 000000000000..63d4519adad4 --- /dev/null +++ b/kong/plugins/statsd/handler.lua @@ -0,0 +1,84 @@ +local BasePlugin = require "kong.plugins.base_plugin" +local basic_serializer = require "kong.plugins.log-serializers.basic" +local statsd_logger = require "kong.plugins.statsd.statsd_logger" + +local StatsdHandler = BasePlugin:extend() + +StatsdHandler.PRIORITY = 1 + +local ngx_log = ngx.log +local ngx_timer_at = ngx.timer.at +local string_gsub = string.gsub +local pairs = pairs +local NGX_ERR = ngx.ERR + +local gauges = { + request_size = function (api_name, message, logger) + local stat = api_name..".request.size" + logger:gauge(stat, message.request.size, 1) + end, + response_size = function (api_name, message, logger) + local stat = api_name..".response.size" + logger:gauge(stat, message.response.size, 1) + end, + status_count = function (api_name, message, logger) + local stat = api_name..".request.status."..message.response.status + logger:counter(stat, 1, 1) + end, + latency = function (api_name, message, logger) + local stat = api_name..".latency" + logger:gauge(stat, message.latencies.request, 1) + end, + request_count = function (api_name, message, logger) + local stat = api_name..".request.count" + logger:counter(stat, 1, 1) + end, + unique_users = function (api_name, message, logger) + if message.authenticated_entity ~= nil and message.authenticated_entity.consumer_id ~= nil then + local stat = api_name..".user.uniques" + logger:set(stat, message.authenticated_entity.consumer_id) + end + end, + request_per_user = function (api_name, message, logger) + if message.authenticated_entity ~= nil and message.authenticated_entity.consumer_id ~= nil then + local stat = api_name.."."..string_gsub(message.authenticated_entity.consumer_id, "-", "_")..".request.count" + logger:counter(stat, 1, 1) + end + end +} + +local function log(premature, conf, message) + if premature then return end + + local logger, err = statsd_logger:new(conf) + if err then + ngx_log(NGX_ERR, "failed to create Statsd logger: ", err) + return + end + + local api_name = string_gsub(message.api.name, "%.", "_") + for _, metric in pairs(conf.metrics) do + local gauge = gauges[metric] + if gauge ~= nil then + gauge(api_name, message, logger) + end + end + + logger:close_socket() +end + +function StatsdHandler:new() + StatsdHandler.super.new(self, "statsd") +end + +function StatsdHandler:log(conf) + StatsdHandler.super.log(self) + local message = basic_serializer.serialize(ngx) + + local ok, err = ngx_timer_at(0, log, conf, message) + if not ok then + ngx_log(NGX_ERR, "failed to create timer: ", err) + end +end + +return StatsdHandler diff --git a/kong/plugins/statsd/schema.lua b/kong/plugins/statsd/schema.lua new file mode 100644 index 000000000000..742447e9a534 --- /dev/null +++ b/kong/plugins/statsd/schema.lua @@ -0,0 +1,8 @@ +return { + fields = { + host = {required = true, type = "string", default = "localhost"}, + port = {required = true, type = "number", default = 8125}, + metrics = {required = true, type = "array", enum = {"request_count", "latency", "request_size", "status_count", "response_size", "unique_users", "request_per_user"}, default = {"request_count", "latency", "request_size", "status_count", "response_size", "unique_users", "request_per_user"}}, + timeout = {type = "number", default = 10000} + } +} diff --git a/kong/plugins/statsd/statsd_logger.lua b/kong/plugins/statsd/statsd_logger.lua new file mode 100644 index 000000000000..7d59c7b7f4c2 --- /dev/null +++ b/kong/plugins/statsd/statsd_logger.lua @@ -0,0 +1,86 @@ +local ngx_socket_udp = ngx.socket.udp +local ngx_log = ngx.log +local table_concat = table.concat +local setmetatable = setmetatable +local NGX_ERR = ngx.ERR +local NGX_DEBUG = ngx.DEBUG + +local statsd_mt = {} +statsd_mt.__index = statsd_mt + +function statsd_mt:new(conf) + local sock = ngx_socket_udp() + sock:settimeout(conf.timeout) + local _, err = sock:setpeername(conf.host, conf.port) + if err then + return nil, "failed to connect to "..conf.host..":"..tostring(conf.port)..": "..err + end + + local statsd = { + host = conf.host, + port = conf.port, + socket = sock + } + return setmetatable(statsd, statsd_mt) +end + +function statsd_mt:create_statsd_message(stat, delta, kind, sample_rate) + local rate = "" + if sample_rate and sample_rate ~= 1 then + rate = "|@"..sample_rate + end + + local message = { + "kong.", + stat, + ":", + delta, + "|", + kind, + rate + } + return table_concat(message, "") +end + +function statsd_mt:close_socket() + local ok, err = self.socket:close() + if not ok then + ngx_log(NGX_ERR, "failed to close connection from "..self.host..":"..tostring(self.port)..": ", err) + return + end +end + +function statsd_mt:send_statsd(stat, delta, kind, sample_rate) + local udp_message = self:create_statsd_message(stat, delta, kind, sample_rate) + ngx_log(NGX_DEBUG, "Sending data to statsd server: "..udp_message) + local ok, err = self.socket:send(udp_message) + if not ok then + ngx_log(NGX_ERR, "failed to send data to "..self.host..":"..tostring(self.port)..": ", err) + end +end + +function statsd_mt:gauge(stat, value, sample_rate) + return self:send_statsd(stat, value, "g", sample_rate) +end + +function statsd_mt:counter(stat, value, sample_rate) + return self:send_statsd(stat, value, "c", sample_rate) +end + +function statsd_mt:timer(stat, ms) + return self:send_statsd(stat, ms, "ms") +end + +function statsd_mt:histogram(stat, value) + return self:send_statsd(stat, value, "h") +end + +function statsd_mt:meter(stat, value) + return self:send_statsd(stat, value, "m") +end + +function statsd_mt:set(stat, value) + return self:send_statsd(stat, value, "s") +end + +return statsd_mt diff --git a/kong/tools/config_defaults.lua b/kong/tools/config_defaults.lua index bf7f4fa9628d..6620a07fc5c1 100644 --- a/kong/tools/config_defaults.lua +++ b/kong/tools/config_defaults.lua @@ -30,7 +30,8 @@ return { ["auto-join"] = {type = "boolean", default = true}, ["advertise"] = {type = "string", nullable = true}, ["encrypt"] = {type = "string", nullable = true}, - ["profile"] = {type = "string", default = "wan", enum = {"wan", "lan", "local"}} + ["profile"] = {type = "string", default = "wan", enum = {"wan", "lan", "local"}}, + ["ttl_on_failure"] = {type = "number", default = 3600} } }, ["database"] = {type = "string", default = "cassandra", enum = {"cassandra", "postgres"}}, @@ -41,13 +42,14 @@ return { ["port"] = {type = "number", default = 5432}, ["user"] = {type = "string", default = "kong"}, ["database"] = {type = "string", default = "kong"}, - ["password"] = {type = "string", default = "kong"} + ["password"] = {type = "string", nullable = true} } }, ["cassandra"] = { type = "table", content = { ["contact_points"] = {type = "array", default = {"127.0.0.1:9042"}}, + ["port"] = {type = "number", default = 9042}, ["keyspace"] = {type = "string", default = "kong"}, ["timeout"] = {type = "number", default = 5000}, ["replication_strategy"] = {type = "string", default = "SimpleStrategy", enum = {"SimpleStrategy", "NetworkTopologyStrategy"}}, @@ -55,7 +57,7 @@ return { ["data_centers"] = {type = "table", default = {}}, ["username"] = {type = "string", nullable = true}, ["password"] = {type = "string", nullable = true}, - ["consistency"] = {type = "string", default = "ONE", enum = {"ANY", "ONE", "TWO", "THREE", "QUORUM", "ALL", "LOCAL_QUORUM", + ["consistency"] = {type = "string", default = "ONE", enum = {"ANY", "ONE", "TWO", "THREE", "QUORUM", "ALL", "LOCAL_QUORUM", "EACH_QUORUM", "SERIAL", "LOCAL_SERIAL", "LOCAL_ONE"}}, ["ssl"] = { type = "table", diff --git a/kong/tools/database_cache.lua b/kong/tools/database_cache.lua index 01e94e558210..7e77d238e3da 100644 --- a/kong/tools/database_cache.lua +++ b/kong/tools/database_cache.lua @@ -16,7 +16,8 @@ local CACHE_KEYS = { REQUESTS = "requests", AUTOJOIN_RETRIES = "autojoin_retries", TIMERS = "timers", - ALL_APIS_BY_DIC = "ALL_APIS_BY_DIC" + ALL_APIS_BY_DIC = "ALL_APIS_BY_DIC", + LDAP_CREDENTIAL = "ldap_credentials" } local _M = {} @@ -102,6 +103,10 @@ function _M.jwtauth_credential_key(secret) return CACHE_KEYS.JWTAUTH_CREDENTIAL..":"..secret end +function _M.ldap_credential_key(username) + return CACHE_KEYS.LDAP_CREDENTIAL.."/"..username +end + function _M.acls_key(consumer_id) return CACHE_KEYS.ACLS..":"..consumer_id end diff --git a/spec/integration/01-dao/07-options_spec.lua b/spec/integration/01-dao/07-options_spec.lua index 41f6b54e6d7b..db4ad949fb0f 100644 --- a/spec/integration/01-dao/07-options_spec.lua +++ b/spec/integration/01-dao/07-options_spec.lua @@ -40,11 +40,11 @@ helpers.for_each_dao(function(db_type, default_options, TYPES) assert.falsy(row) end) - it("on update", function() + it("on update - increase ttl", function() local api, err = factory.apis:insert({ name = "mockbin", request_host = "mockbin.com", upstream_url = "http://mockbin.com" - }, {ttl = 5}) + }, {ttl = 3}) assert.falsy(err) -- Retrieval @@ -54,10 +54,12 @@ helpers.for_each_dao(function(db_type, default_options, TYPES) assert.falsy(err) assert.truthy(row) + os.execute("sleep 2") + -- Updating the TTL to a higher value - factory.apis:update({name = "mockbin2"}, {id = api.id}, {ttl = 10}) + factory.apis:update({name = "mockbin2"}, {id = api.id}, {ttl = 3}) - os.execute("sleep 5") + os.execute("sleep 2") row, err = factory.apis:find { id = api.id @@ -65,12 +67,41 @@ helpers.for_each_dao(function(db_type, default_options, TYPES) assert.falsy(err) assert.truthy(row) - os.execute("sleep 5") + os.execute("sleep 2") + -- It has now finally expired row, err = factory.apis:find { id = api.id } + assert.falsy(err) + assert.falsy(row) + end) + + it("on update - decrease ttl", function() + local api, err = factory.apis:insert({ + name = "mockbin", request_host = "mockbin.com", + upstream_url = "http://mockbin.com" + }, {ttl = 10}) + assert.falsy(err) + + os.execute("sleep 3") + + -- Retrieval + local row, err = factory.apis:find { + id = api.id + } + assert.falsy(err) + assert.truthy(row) + -- Updating the TTL to a lower value + local _, err = factory.apis:update({name = "mockbin2"}, {id = api.id}, {ttl = 3}) + assert.falsy(err) + + os.execute("sleep 4") + + row, err = factory.apis:find { + id = api.id + } assert.falsy(err) assert.falsy(row) end) diff --git a/spec/integration/05-proxy/resolver_spec.lua b/spec/integration/05-proxy/resolver_spec.lua index a9791fcbb3da..be281818f488 100644 --- a/spec/integration/05-proxy/resolver_spec.lua +++ b/spec/integration/05-proxy/resolver_spec.lua @@ -42,7 +42,9 @@ describe("Resolver", function() {name = "tests-trailing-slash-path", request_path = "/test-trailing-slash", strip_request_path = true, upstream_url = "http://www.mockbin.org/request"}, {name = "tests-trailing-slash-path2", request_path = "/test-trailing-slash2", strip_request_path = false, upstream_url = "http://www.mockbin.org/request"}, {name = "tests-trailing-slash-path3", request_path = "/test-trailing-slash3", strip_request_path = true, upstream_url = "http://www.mockbin.org"}, - {name = "tests-trailing-slash-path4", request_path = "/test-trailing-slash4", strip_request_path = true, upstream_url = "http://www.mockbin.org/"} + {name = "tests-trailing-slash-path4", request_path = "/test-trailing-slash4", strip_request_path = true, upstream_url = "http://www.mockbin.org/"}, + {name = "tests-deep-path", request_path = "/hello/world", strip_request_path = true, upstream_url = "http://mockbin.com"}, + {name = "tests-deep-path-two", request_path = "/hello/world/wot", strip_request_path = true, upstream_url = "http://httpbin.org"} }, plugin = { {name = "key-auth", config = {key_names = {"apikey"} }, __api = 2} @@ -169,6 +171,21 @@ describe("Resolver", function() assert.equal(200, status) assert.equal("http://www.mockbin.org/request/test-trailing-slash2?hello=world", cjson.decode(response).url) end) + it("should properly handle deep paths", function() + -- Should be httpbin + local response, status = http_client.get(spec_helper.PROXY_URL.."/hello/world/wot/get") + assert.equal(200, status) + local body = cjson.decode(response) + assert.truthy(body.args) + assert.truthy(body.headers) + + -- Should be Mockbin + local response, status = http_client.get(spec_helper.PROXY_URL.."/hello/world/request") + assert.equal(200, status) + local body = cjson.decode(response) + assert.truthy(body.postData) + assert.truthy(body.headers) + end) end) it("should return the correct Server and Via headers when the request was proxied", function() diff --git a/spec/plugins/acl/access_spec.lua b/spec/plugins/acl/access_spec.lua index 15bc65c7085d..d92b1763d4b4 100644 --- a/spec/plugins/acl/access_spec.lua +++ b/spec/plugins/acl/access_spec.lua @@ -79,8 +79,10 @@ describe("ACL Plugin", function() end) it("should work when in whitelist", function() - local _, status = http_client.get(STUB_GET_URL, {apikey = "apikey124"}, {host = "acl2.com"}) + local response, status = http_client.get(STUB_GET_URL, {apikey = "apikey124"}, {host = "acl2.com"}) assert.equal(200, status) + local body = cjson.decode(response) + assert.equal("admin", body.headers["x-consumer-groups"]) end) it("should work when not in blacklist", function() @@ -98,8 +100,10 @@ describe("ACL Plugin", function() describe("Multi lists", function() it("should work when in whitelist", function() - local _, status = http_client.get(STUB_GET_URL, {apikey = "apikey125"}, {host = "acl4.com"}) + local response, status = http_client.get(STUB_GET_URL, {apikey = "apikey125"}, {host = "acl4.com"}) assert.equal(200, status) + local body = cjson.decode(response) + assert.truthy(body.headers["x-consumer-groups"] == "pro, hello" or body.headers["x-consumer-groups"] == "hello, pro") end) it("should fail when not in whitelist", function() diff --git a/spec/plugins/ldap-auth/access_spec.lua b/spec/plugins/ldap-auth/access_spec.lua new file mode 100644 index 000000000000..c5bf60b209d4 --- /dev/null +++ b/spec/plugins/ldap-auth/access_spec.lua @@ -0,0 +1,127 @@ +local spec_helper = require "spec.spec_helpers" +local http_client = require "kong.tools.http_client" +local cjson = require "cjson" +local base64 = require "base64" +local cache = require "kong.tools.database_cache" + +local PROXY_URL = spec_helper.PROXY_URL +local API_URL = spec_helper.API_URL + +describe("LDAP-AUTH Plugin", function() + setup(function() + spec_helper.prepare_db() + spec_helper.insert_fixtures { + api = { + {name = "test-ldap", request_host = "ldap.com", upstream_url = "http://mockbin.com"}, + {name = "test-ldap2", request_host = "ldap2.com", upstream_url = "http://mockbin.com"} + }, + plugin = { + {name = "ldap-auth", config = {ldap_host = "ldap.forumsys.com", ldap_port = "389", start_tls = false, base_dn = "dc=example,dc=com", attribute = "uid"}, __api = 1}, + {name = "ldap-auth", config = {ldap_host = "ldap.forumsys.com", ldap_port = "389", start_tls = false, base_dn = "dc=example,dc=com", attribute = "uid", hide_credentials = true}, __api = 2}, + } + } + + spec_helper.start_kong() + end) + + teardown(function() + spec_helper.stop_kong() + end) + + describe("ldap-auth", function() + it("should return invalid credentials and www-authenticate header when the credential is missing", function() + local response, status, headers = http_client.get(PROXY_URL.."/get", {}, {host = "ldap.com"}) + assert.equal(401, status) + local body = cjson.decode(response) + assert.equal(headers["www-authenticate"], 'LDAP realm="kong"') + assert.equal("Unauthorized", body.message) + end) + + it("should return invalid credentials when credential value is in wrong format in authorization header", function() + local response, status = http_client.get(PROXY_URL.."/get", {}, {host = "ldap.com", authorization = "abcd"}) + local body = cjson.decode(response) + assert.equal(403, status) + assert.equal("Invalid authentication credentials", body.message) + end) + + it("should return invalid credentials when credential value is in wrong format in proxy-authorization header", function() + local response, status = http_client.get(PROXY_URL.."/get", {}, {host = "ldap.com", ["proxy-authorization"] = "abcd"}) + local body = cjson.decode(response) + assert.equal(403, status) + assert.equal("Invalid authentication credentials", body.message) + end) + + it("should return invalid credentials when credential value is missing in authorization header", function() + local _, status = http_client.get(PROXY_URL.."/get", {}, {host = "ldap.com", authorization = "ldap "}) + assert.equal(403, status) + end) + + it("should pass if credential is valid in post request", function() + local _, status = http_client.post(PROXY_URL.."/request", {}, {host = "ldap.com", authorization = "ldap "..base64.encode("einstein:password")}) + assert.equal(200, status) + end) + + it("should pass if credential is valid and starts with space in post request", function() + local _, status = http_client.post(PROXY_URL.."/request", {}, {host = "ldap.com", authorization = " ldap "..base64.encode("einstein:password")}) + assert.equal(200, status) + end) + + it("should pass if signature type indicator is in caps and credential is valid in post request", function() + local _, status = http_client.post(PROXY_URL.."/request", {}, {host = "ldap.com", authorization = "LDAP "..base64.encode("einstein:password")}) + assert.equal(200, status) + end) + + it("should pass if credential is valid in get request", function() + local response, status = http_client.get(PROXY_URL.."/request", {}, {host = "ldap.com", authorization = "ldap "..base64.encode("einstein:password")}) + assert.equal(200, status) + local parsed_response = cjson.decode(response) + assert.truthy(parsed_response.headers["x-credential-username"]) + assert.equal("einstein", parsed_response.headers["x-credential-username"]) + end) + + it("should not pass if credential does not has password encoded in get request", function() + local _, status = http_client.get(PROXY_URL.."/request", {}, {host = "ldap.com", authorization = "ldap "..base64.encode("einstein:")}) + assert.equal(403, status) + end) + + it("should not pass if credential has multiple encoded username or password separated by ':' in get request", function() + local _, status = http_client.get(PROXY_URL.."/request", {}, {host = "ldap.com", authorization = "ldap "..base64.encode("einstein:password:another_password")}) + assert.equal(403, status) + end) + + it("should not pass if credential is invalid in get request", function() + local _, status = http_client.get(PROXY_URL.."/request", {}, {host = "ldap.com", authorization = "ldap "..base64.encode("einstein:wrong_password")}) + assert.equal(403, status) + end) + + it("should not hide credential sent along with authorization header to upstream server", function() + local response, status = http_client.get(PROXY_URL.."/request", {}, {host = "ldap.com", authorization = "ldap "..base64.encode("einstein:password")}) + assert.equal(200, status) + local parsed_response = cjson.decode(response) + assert.equal("ldap "..base64.encode("einstein:password"), parsed_response.headers["authorization"]) + end) + + it("should hide credential sent along with authorization header to upstream server", function() + local response, status = http_client.get(PROXY_URL.."/request", {}, {host = "ldap2.com", authorization = "ldap "..base64.encode("einstein:password")}) + assert.equal(200, status) + local parsed_response = cjson.decode(response) + assert.falsy(parsed_response.headers["authorization"]) + end) + + it("should cache LDAP Auth Credential", function() + local _, status = http_client.get(PROXY_URL.."/request", {}, {host = "ldap.com", authorization = "ldap "..base64.encode("einstein:password")}) + assert.equals(200, status) + + -- Check that cache is populated + local cache_key = cache.ldap_credential_key("einstein") + local exists = true + while(exists) do + local _, status = http_client.get(API_URL.."/cache/"..cache_key) + if status ~= 200 then + exists = false + end + end + assert.equals(200, status) + end) + end) +end) diff --git a/spec/plugins/statsd/log_spec.lua b/spec/plugins/statsd/log_spec.lua new file mode 100644 index 000000000000..81a56cdee14f --- /dev/null +++ b/spec/plugins/statsd/log_spec.lua @@ -0,0 +1,122 @@ +local spec_helper = require "spec.spec_helpers" +local http_client = require "kong.tools.http_client" + +local STUB_GET_URL = spec_helper.STUB_GET_URL + +local UDP_PORT = spec_helper.find_port() + +describe("Statsd Plugin", function() + + setup(function() + spec_helper.prepare_db() + spec_helper.insert_fixtures { + api = { + {request_host = "logging1.com", upstream_url = "http://mockbin.com"}, + {request_host = "logging2.com", upstream_url = "http://mockbin.com"}, + {request_host = "logging3.com", upstream_url = "http://mockbin.com"}, + {request_host = "logging4.com", upstream_url = "http://mockbin.com"}, + {request_host = "logging5.com", upstream_url = "http://mockbin.com"}, + {request_host = "logging6.com", upstream_url = "http://mockbin.com"} + }, + plugin = { + {name = "statsd", config = {host = "127.0.0.1", port = UDP_PORT, metrics = {"request_count"}}, __api = 1}, + {name = "statsd", config = {host = "127.0.0.1", port = UDP_PORT, metrics = {"latency"}}, __api = 2}, + {name = "statsd", config = {host = "127.0.0.1", port = UDP_PORT, metrics = {"status_count"}}, __api = 3}, + {name = "statsd", config = {host = "127.0.0.1", port = UDP_PORT, metrics = {"request_size"}}, __api = 4}, + {name = "statsd", config = {host = "127.0.0.1", port = UDP_PORT}, __api = 5}, + {name = "statsd", config = {host = "127.0.0.1", port = UDP_PORT, metrics = {"response_size"}}, __api = 6} + } + } + spec_helper.start_kong() + end) + + teardown(function() + spec_helper.stop_kong() + end) + + it("should log to UDP when metrics is request_count", function() + local thread = spec_helper.start_udp_server(UDP_PORT) -- Starting the mock UDP server + + local _, status = http_client.get(STUB_GET_URL, nil, {host = "logging1.com"}) + assert.equal(200, status) + + local ok, res = thread:join() + assert.True(ok) + assert.truthy(res) + assert.equal("kong.logging1_com.request.count:1|c", res) + end) + + it("should log to UDP when metrics is status_count", function() + local thread = spec_helper.start_udp_server(UDP_PORT) -- Starting the mock UDP server + + local _, status = http_client.get(STUB_GET_URL, nil, {host = "logging3.com"}) + assert.equal(200, status) + + local ok, res = thread:join() + assert.True(ok) + assert.truthy(res) + assert.equal("kong.logging3_com.request.status.200:1|c", res) + end) + + it("should log to UDP when metrics is request_size", function() + local thread = spec_helper.start_udp_server(UDP_PORT) -- Starting the mock UDP server + + local _, status = http_client.get(STUB_GET_URL, nil, {host = "logging4.com"}) + assert.equal(200, status) + + local ok, res = thread:join() + assert.True(ok) + assert.truthy(res) + local message = {} + for w in string.gmatch(res,"kong.logging4_com.request.size:%d*|g") do + table.insert(message, w) + end + assert.equal(1, #message) + end) + + it("should log to UDP when metrics is latency", function() + local thread = spec_helper.start_udp_server(UDP_PORT) -- Starting the mock UDP server + + local _, status = http_client.get(STUB_GET_URL, nil, {host = "logging2.com"}) + assert.equal(200, status) + + local ok, res = thread:join() + assert.True(ok) + assert.truthy(res) + + local message = {} + for w in string.gmatch(res,"kong.logging2_com.latency:.*|g") do + table.insert(message, w) + end + + assert.equal(1, #message) + end) + + it("should log to UDP when metrics is request_count", function() + local thread = spec_helper.start_udp_server(UDP_PORT) -- Starting the mock UDP server + + local _, status = http_client.get(STUB_GET_URL, nil, {host = "logging5.com"}) + assert.equal(200, status) + + local ok, res = thread:join() + assert.True(ok) + assert.truthy(res) + assert.equal("kong.logging5_com.request.count:1|c", res) + end) + + it("should log to UDP when metrics is response_size", function() + local thread = spec_helper.start_udp_server(UDP_PORT) -- Starting the mock UDP server + + local _, status = http_client.get(STUB_GET_URL, nil, {host = "logging6.com"}) + assert.equal(200, status) + + local ok, res = thread:join() + assert.True(ok) + assert.truthy(res) + local message = {} + for w in string.gmatch(res,"kong.logging6_com.response.size:%d*|g") do + table.insert(message, w) + end + assert.equal(1, #message) + end) +end)