diff --git a/.travis.yml b/.travis.yml index 33efdde0f38..d0ed7be16e1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,10 @@ -dist: trusty +dist: xenial sudo: required -language: java +language: go -jdk: - - oraclejdk8 +go: + - "1.13.x" notifications: email: @@ -26,15 +26,15 @@ services: env: global: - - LUAROCKS=3.1.3 - - OPENSSL=1.1.1c + - LUAROCKS=3.3.1 + - OPENSSL=1.1.1d - CASSANDRA_BASE=2.2.12 - CASSANDRA_LATEST=3.9 - - OPENRESTY_BASE=1.15.8.1 - - OPENRESTY_LATEST=1.15.8.1 + - OPENRESTY_BASE=1.15.8.2 + - OPENRESTY_LATEST=1.15.8.2 - DOWNLOAD_CACHE=$HOME/download-cache - INSTALL_CACHE=$HOME/install-cache - - BUSTED_ARGS="-o gtest -v --exclude-tags=flaky,ipv6" + - BUSTED_ARGS="-o htest -v --exclude-tags=flaky,ipv6" - PLUGIN_NAME=session - KONG_TEST_PLUGINS=bundled,$PLUGIN_NAME - KONG_PLUGINS=bundled,$PLUGIN_NAME @@ -59,6 +59,8 @@ install: - cd ../ - KONG_DATABASE=postgres KONG_PG_DATABASE=kong_tests kong-ce/bin/kong migrations bootstrap - KONG_DATABASE=cassandra KONG_CASSANDRA_KEYSPACE=kong_tests kong-ce/bin/kong migrations bootstrap + - luarocks remove --force lua-resty-session + - luarocks install --force lua-resty-session 3.1 - cd kong-ce script: diff --git a/kong-plugin-session-2.2.0-1.rockspec b/kong-plugin-session-2.3.0-1.rockspec similarity index 87% rename from kong-plugin-session-2.2.0-1.rockspec rename to kong-plugin-session-2.3.0-1.rockspec index 2ed5d2696b1..b8111011ef2 100644 --- a/kong-plugin-session-2.2.0-1.rockspec +++ b/kong-plugin-session-2.3.0-1.rockspec @@ -1,12 +1,12 @@ package = "kong-plugin-session" -version = "2.2.0-1" +version = "2.3.0-1" supported_platforms = {"linux", "macosx"} source = { url = "git://github.com/Kong/kong-plugin-session", - tag = "2.2.0" + tag = "2.3.0" } description = { @@ -17,7 +17,7 @@ description = { dependencies = { "lua >= 5.1", - "lua-resty-session == 2.24", + "lua-resty-session == 3.1", --"kong >= 1.2.0", } @@ -27,6 +27,7 @@ build = { ["kong.plugins.session.handler"] = "kong/plugins/session/handler.lua", ["kong.plugins.session.schema"] = "kong/plugins/session/schema.lua", ["kong.plugins.session.access"] = "kong/plugins/session/access.lua", + ["kong.plugins.session.header_filter"] = "kong/plugins/session/header_filter.lua", ["kong.plugins.session.session"] = "kong/plugins/session/session.lua", ["kong.plugins.session.daos"] = "kong/plugins/session/daos.lua", ["kong.plugins.session.storage.kong"] = "kong/plugins/session/storage/kong.lua", diff --git a/kong/plugins/session/access.lua b/kong/plugins/session/access.lua index 9905b550989..d4bcb6b318f 100644 --- a/kong/plugins/session/access.lua +++ b/kong/plugins/session/access.lua @@ -1,24 +1,25 @@ local constants = require "kong.constants" local kong_session = require "kong.plugins.session.session" -local kong = kong -local _M = {} +local ngx = ngx +local kong = kong +local concat = table.concat -local function load_consumer(consumer_id) - local result, err = kong.db.consumers:select { id = consumer_id } - if not result then - return nil, err - end - return result -end + +local _M = {} local function authenticate(consumer, credential_id, groups) local set_header = kong.service.request.set_header local clear_header = kong.service.request.clear_header - set_header(constants.HEADERS.CONSUMER_ID, consumer.id) + if consumer.id then + set_header(constants.HEADERS.CONSUMER_ID, consumer.id) + else + clear_header(constants.HEADERS.CONSUMER_ID) + end + if consumer.custom_id then set_header(constants.HEADERS.CONSUMER_CUSTOM_ID, consumer.custom_id) else @@ -32,29 +33,46 @@ local function authenticate(consumer, credential_id, groups) end if groups then - set_header(constants.HEADERS.AUTHENTICATED_GROUPS, table.concat(groups, ", ")) + set_header(constants.HEADERS.AUTHENTICATED_GROUPS, concat(groups, ", ")) ngx.ctx.authenticated_groups = groups else clear_header(constants.HEADERS.AUTHENTICATED_GROUPS) end + local credential if credential_id then - local credential = {id = credential_id or consumer.id, consumer_id = consumer.id} + credential = { + id = credential_id, + consumer_id = consumer.id + } + + clear_header(constants.HEADERS.ANONYMOUS) + + if constants.HEADERS.CREDENTIAL_IDENTIFIER then + set_header(constants.HEADERS.CREDENTIAL_IDENTIFIER, credential.id) + end + + else set_header(constants.HEADERS.ANONYMOUS, true) - kong.client.authenticate(consumer, credential) - return + if constants.HEADERS.CREDENTIAL_IDENTIFIER then + clear_header(constants.HEADERS.CREDENTIAL_IDENTIFIER) + end end - kong.client.authenticate(consumer, nil) + kong.client.authenticate(consumer, credential) end function _M.execute(conf) - local s = kong_session.open_session(conf) + local s, present, reason = kong_session.open_session(conf) + if not present then + if reason then + kong.log.debug("session not present (", reason, ")") + else + kong.log.debug("session not present") + end - if not s.present then - kong.log.debug("session not present") return end @@ -70,7 +88,7 @@ function _M.execute(conf) local consumer_cache_key = kong.db.consumers:cache_key(cid) local consumer, err = kong.cache:get(consumer_cache_key, nil, - load_consumer, cid) + kong.client.load_consumer, cid) if err then kong.log.err("could not load consumer: ", err) diff --git a/kong/plugins/session/handler.lua b/kong/plugins/session/handler.lua index cfbf025d842..19276ca26c3 100644 --- a/kong/plugins/session/handler.lua +++ b/kong/plugins/session/handler.lua @@ -1,66 +1,19 @@ local access = require "kong.plugins.session.access" -local kong_session = require "kong.plugins.session.session" - - -local kong = kong +local header_filter = require "kong.plugins.session.header_filter" local KongSessionHandler = { PRIORITY = 1900, - VERSION = "2.2.0", + VERSION = "2.3.0", } -local function get_authenticated_groups() - local authenticated_groups = ngx.ctx.authenticated_groups - if authenticated_groups == nil then - return nil - end - - assert(type(authenticated_groups) == "table", - "invalid authenticated_groups, a table was expected") - - return authenticated_groups -end - - -function KongSessionHandler:header_filter(conf) - local credential = kong.client.get_credential() - local consumer = kong.client.get_consumer() - - if not credential then - -- don't open sessions for anonymous users - kong.log.debug("anonymous: no credential.") - return - end - - local credential_id = credential.id - local consumer_id = consumer and consumer.id - local s = kong.ctx.shared.authenticated_session - local groups = get_authenticated_groups() - - -- if session exists and the data in the session matches the ctx then - -- don't worry about saving the session data or sending cookie - if s and s.present then - local cid, cred_id = kong_session.retrieve_session_data(s) - if cred_id == credential_id and cid == consumer_id - then - return - end - end - - -- session is no longer valid - -- create new session and save the data / send the Set-Cookie header - if consumer_id then - s = s or kong_session.open_session(conf) - kong_session.store_session_data(s, consumer_id, credential_id or consumer_id, - groups) - s:save() - end +function KongSessionHandler.header_filter(_, conf) + header_filter.execute(conf) end -function KongSessionHandler:access(conf) +function KongSessionHandler.access(_, conf) access.execute(conf) end diff --git a/kong/plugins/session/header_filter.lua b/kong/plugins/session/header_filter.lua new file mode 100644 index 00000000000..1f9527443cf --- /dev/null +++ b/kong/plugins/session/header_filter.lua @@ -0,0 +1,64 @@ +local kong_session = require "kong.plugins.session.session" + + +local ngx = ngx +local kong = kong +local type = type +local assert = assert + + +local function get_authenticated_groups() + local authenticated_groups = ngx.ctx.authenticated_groups + if authenticated_groups == nil then + return nil + end + + assert(type(authenticated_groups) == "table", + "invalid authenticated_groups, a table was expected") + + return authenticated_groups +end + + +local _M = {} + + +function _M.execute(conf) + local credential = kong.client.get_credential() + local consumer = kong.client.get_consumer() + + if not credential then + -- don't open sessions for anonymous users + kong.log.debug("anonymous: no credential.") + return + end + + local credential_id = credential.id + local consumer_id = consumer and consumer.id + + -- if session exists and the data in the session matches the ctx then + -- don't worry about saving the session data or sending cookie + local s = kong.ctx.shared.authenticated_session + if s and s.present then + local cid, cred_id = kong_session.retrieve_session_data(s) + if cred_id == credential_id and cid == consumer_id + then + return + end + end + + -- session is no longer valid + -- create new session and save the data / send the Set-Cookie header + if consumer_id then + local groups = get_authenticated_groups() + s = s or kong_session.open_session(conf) + kong_session.store_session_data(s, + consumer_id, + credential_id or consumer_id, + groups) + s:save() + end +end + + +return _M diff --git a/kong/plugins/session/schema.lua b/kong/plugins/session/schema.lua index 83a5082f4af..671c7fc9834 100644 --- a/kong/plugins/session/schema.lua +++ b/kong/plugins/session/schema.lua @@ -1,17 +1,20 @@ local typedefs = require "kong.db.schema.typedefs" local Schema = require "kong.db.schema" +local utils = require "kong.tools.utils" + -local utils = require("kong.tools.utils") local char = string.char local rand = math.random local encode_base64 = ngx.encode_base64 + local samesite = Schema.define { type = "string", default = "Strict", one_of = { "Strict", "Lax", + "None", "off", } } @@ -43,6 +46,7 @@ return { }, { cookie_name = { type = "string", default = "session" } }, { cookie_lifetime = { type = "number", default = 3600 } }, + { cookie_idletime = { type = "number" } }, { cookie_renew = { type = "number", default = 600 } }, { cookie_path = { type = "string", default = "/" } }, { cookie_domain = { type = "string" } }, diff --git a/kong/plugins/session/session.lua b/kong/plugins/session/session.lua index a100c5a024e..e38595ca894 100644 --- a/kong/plugins/session/session.lua +++ b/kong/plugins/session/session.lua @@ -1,4 +1,4 @@ -local storage = require "kong.plugins.session.storage.kong" +local kong_storage = require "kong.plugins.session.storage.kong" local resty_session = require "resty.session" @@ -10,11 +10,18 @@ local _M = {} local function get_opts(conf) + local storage = conf.storage + if storage == "kong" then + storage = kong_storage + end + return { - name = conf.cookie_name, - secret = conf.secret, - cookie = { + name = conf.cookie_name, + secret = conf.secret, + storage = storage, + cookie = { lifetime = conf.cookie_lifetime, + idletime = conf.cookie_idletime, path = conf.cookie_path, domain = conf.cookie_domain, samesite = conf.cookie_samesite, @@ -30,25 +37,7 @@ end --- Open a session based on plugin config -- @returns resty.session session object function _M.open_session(conf) - local opts = get_opts(conf) - local s - - if conf.storage == 'kong' then - -- Required strategy for kong adapter which will allow for :regenerate - -- method to keep sessions around during renewal period to allow for - -- concurrent requests. When client exchanges cookie for new cookie, - -- old sessions will have their ttl updated, which will discard the item - -- after "cookie_discard" period. - opts.strategy = "regenerate" - s = resty_session.new(opts) - s.storage = storage.new(s) - s:open() - else - opts.storage = conf.storage - s = resty_session.open(opts) - end - - return s + return resty_session.open(get_opts(conf)) end @@ -56,10 +45,8 @@ end -- @param s - the session -- @returns consumer_id, credential_id, groups function _M.retrieve_session_data(s) - if not s then return nil, nil, nil end - - if s and not s.data then - return nil, nil, nil + if not s or not s.data then + return end return s.data[1], s.data[2], s.data[3] @@ -79,7 +66,6 @@ function _M.store_session_data(s, consumer_id, credential_id, groups) s.data[1] = consumer_id s.data[2] = credential_id s.data[3] = groups - end @@ -87,35 +73,37 @@ end -- @return boolean should logout of the session? function _M.logout(conf) local logout_methods = conf.logout_methods - if logout_methods then - local request_method = kong.request.get_method() - local logout - for _, logout_method in ipairs(logout_methods) do - if logout_method == request_method then - logout = true - break - end - end + if not logout_methods then + return false + end - if not logout then - return false + local request_method = kong.request.get_method() + local logout + for _, logout_method in ipairs(logout_methods) do + if logout_method == request_method then + logout = true + break end + end - local logout_query_arg = conf.logout_query_arg - if logout_query_arg then - if kong.request.get_query_arg(logout_query_arg) then - kong.log.debug("logout by query argument") - return true - end + if not logout then + return false + end + + local logout_query_arg = conf.logout_query_arg + if logout_query_arg then + if kong.request.get_query_arg(logout_query_arg) then + kong.log.debug("logout by query argument") + return true end + end - local logout_post_arg = conf.logout_post_arg - if logout_post_arg then - local post_args = kong.request.get_body() - if post_args and post_args[logout_post_arg] then - kong.log.debug("logout by post argument") - return true - end + local logout_post_arg = conf.logout_post_arg + if logout_post_arg then + local post_args = kong.request.get_body() + if post_args and post_args[logout_post_arg] then + kong.log.debug("logout by post argument") + return true end end diff --git a/kong/plugins/session/storage/kong.lua b/kong/plugins/session/storage/kong.lua index 462668a85ca..89044a7ba2c 100644 --- a/kong/plugins/session/storage/kong.lua +++ b/kong/plugins/session/storage/kong.lua @@ -1,161 +1,113 @@ -local concat = table.concat -local tonumber = tonumber local setmetatable = setmetatable -local floor = math.floor -local now = ngx.now +local get_phase = ngx.get_phase +local timer_at = ngx.timer.at local kong = kong -local kong_storage = {} -kong_storage.__index = kong_storage +local storage = {} -function kong_storage.new(config) - return setmetatable({ - db = kong.db, - encode = config.encoder.encode, - decode = config.encoder.decode, - delimiter = config.cookie.delimiter, - lifetime = config.cookie.lifetime, - }, kong_storage) -end +storage.__index = storage -local function load_session(sid) - local session, err = kong.db.sessions:select_by_session_id(sid) - if not session then - return nil, err - end - return session +function storage.new(session) + return setmetatable({ + session = session, + encode = session.encoder.encode, + decode = session.encoder.decode, + }, storage) end -function kong_storage:get(sid) - local cache_key = kong.db.sessions:cache_key(sid) - local s, err = kong.cache:get(cache_key, nil, load_session, sid) - - if err then - kong.log.err("could not find session:", err) - end - - return s, err +local function load_session(id) + return kong.db.sessions:select_by_session_id(id) end -function kong_storage:cookie(c) - local r, d = {}, self.delimiter - local i, p, s, e = 1, 1, c:find(d, 1, true) - while s do - if i > 2 then - return nil - end - r[i] = c:sub(p, e - 1) - i, p = i + 1, e + 1 - s, e = c:find(d, p, true) - end - if i ~= 3 then - return nil - end - r[3] = c:sub(p) - return r +function storage:get(id) + local cache_key = kong.db.sessions:cache_key(id) + return kong.cache:get(cache_key, nil, load_session, id) end -function kong_storage:open(cookie, lifetime) - local c = self:cookie(cookie) - - if c and c[1] and c[2] and c[3] then - local id, expires, hmac = self.decode(c[1]), tonumber(c[2]), self.decode(c[3]) - local data - - if ngx.get_phase() ~= 'header_filter' then - local db_s = self:get(c[1]) - if db_s then - data = self.decode(db_s.data) - expires = db_s.expires - end - end +function storage:open(id) + if get_phase() == "header_filter" then + return + end - return id, expires, data, hmac + local row, err = self:get(id) + if not row then + return nil, err end - return nil, "invalid" + return self.decode(row.data) end -function kong_storage:insert_session(sid, data, expires) - local _, err = self.db.sessions:insert({ - session_id = sid, - data = data, - expires = expires, - }, { ttl = self.lifetime }) - - if err then - kong.log.err("could not insert session: ", err) - end +function storage:insert_session(id, data, ttl) + return kong.db.sessions:insert({ + session_id = id, + data = data, + expires = self.session.now + ttl, + }, { ttl = ttl }) end -function kong_storage:update_session(id, params, ttl) - local _, err = self.db.sessions:update({ id = id }, params, { ttl = ttl }) - if err then - kong.log.err("could not update session: ", err) - end +function storage:update_session(id, params, ttl) + return kong.db.sessions:update({ id = id }, params, { ttl = ttl }) end -function kong_storage:save(id, expires, data, hmac) - local life, key = floor(expires - now()), self.encode(id) - local value = concat({key, expires, self.encode(hmac)}, self.delimiter) - - if life > 0 then - if ngx.get_phase() == 'header_filter' then - ngx.timer.at(0, function() - self:insert_session(key, self.encode(data), expires) - end) - else - self:insert_session(key, self.encode(data), expires) - end +function storage:save(id, ttl, data) + local data = self.encode(data) + if get_phase() == "header_filter" then + timer_at(0, function() + return self:insert_session(id, data, ttl) + end) - return value + return true end - return nil, "expired" + return self:insert_session(id, data, ttl) end -function kong_storage:destroy(id) - local db_s = self:get(self.encode(id)) - - if not db_s then - return +function storage:destroy(id) + local row, err = self:get(id) + if not row then + return nil, err end - local _, err = self.db.sessions:delete({ - id = db_s.id - }) - - if err then - kong.log.err("could not delete session: ", err) - end + return kong.db.sessions:delete({ id = row.id }) end -- used by regenerate strategy to expire old sessions during renewal -function kong_storage:ttl(id, ttl) - if ngx.get_phase() == 'header_filter' then - ngx.timer.at(0, function() - local s = self:get(self.encode(id)) - if s then - self:update_session(s.id, {session_id = s.session_id}, ttl) +function storage:ttl(id, ttl) + if get_phase() == "header_filter" then + timer_at(0, function() + local row, err = self:get(id) + if not row then + return nil, err end + + return self:update_session(row.id, { + session_id = row.session_id + }, ttl) end) - else - local s = self:get(self.encode(id)) - if s then - self:update_session(s.id, {session_id = s.session_id}, ttl) - end + + return true + end + + local row, err = self:get(id) + if not row then + return nil, err end + + return self:update_session(row.id, { + session_id = row.session_id + }, ttl) end -return kong_storage + +return storage diff --git a/spec/01-access_spec.lua b/spec/01-access_spec.lua index 57de66dea24..5852e15277b 100644 --- a/spec/01-access_spec.lua +++ b/spec/01-access_spec.lua @@ -7,7 +7,7 @@ local lower = string.lower for _, strategy in helpers.each_strategy() do describe("Plugin: Session (access) [#" .. strategy .. "]", function() - local client, consumer + local client, consumer, credential lazy_setup(function() local bp, db = helpers.get_db_utils(strategy, { @@ -84,7 +84,7 @@ for _, strategy in helpers.each_strategy() do consumer = db.consumers:insert({username = "coop"}) - bp.keyauth_credentials:insert { + credential = bp.keyauth_credentials:insert { key = "kong", consumer = { id = consumer.id, @@ -180,8 +180,8 @@ for _, strategy in helpers.each_strategy() do assert.equal("session", cookie_name) -- e.g. ["Set-Cookie"] = - -- "da_cookie=m1EL96jlDyQztslA4_6GI20eVuCmsfOtd6Y3lSo4BTY.|15434724 - -- 06|U5W4A6VXhvqvBSf4G_v0-Q..|DFJMMSR1HbleOSko25kctHZ44oo.; Path=/ + -- "da_cookie=m1EL96jlDyQztslA4_6GI20eVuCmsfOtd6Y3lSo4BTY|15434724 + -- 06|U5W4A6VXhvqvBSf4G_v0-Q|DFJMMSR1HbleOSko25kctHZ44oo; Path=/ -- ; SameSite=Lax; Secure; HttpOnly" local cookie_parts = utils.split(cookie, "; ") assert.equal("SameSite=Strict", cookie_parts[3]) @@ -247,6 +247,10 @@ for _, strategy in helpers.each_strategy() do assert.equal(consumer.id, json.headers[lower(constants.HEADERS.CONSUMER_ID)]) assert.equal(consumer.username, json.headers[lower(constants.HEADERS.CONSUMER_USERNAME)]) + if constants.HEADERS.CREDENTIAL_IDENTIFIER then + assert.equal(credential.id, json.headers[lower(constants.HEADERS.CREDENTIAL_IDENTIFIER)]) + end + assert.equal(nil, json.headers[lower(constants.HEADERS.ANONYMOUS)]) assert.equal(nil, json.headers[lower(constants.HEADERS.CONSUMER_CUSTOM_ID)]) assert.equal(nil, json.headers[lower(constants.HEADERS.AUTHENTICATED_GROUPS)]) end)