From b0b616567026b3f4d9c4b55d9907994497fa3eae Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Mon, 11 Jan 2021 16:42:12 +0100 Subject: [PATCH 01/94] Add our own mod of authz-keycloak plugin. --- apisix/plugins/authz-keycloak0.lua | 207 +++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 apisix/plugins/authz-keycloak0.lua diff --git a/apisix/plugins/authz-keycloak0.lua b/apisix/plugins/authz-keycloak0.lua new file mode 100644 index 000000000000..4e06d9190038 --- /dev/null +++ b/apisix/plugins/authz-keycloak0.lua @@ -0,0 +1,207 @@ +-- +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You under the Apache License, Version 2.0 +-- (the "License"); you may not use this file except in compliance with +-- the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +local core = require("apisix.core") +local http = require "resty.http" +local sub_str = string.sub +local url = require "net.url" +local tostring = tostring +local ngx = ngx +local plugin_name = "authz-keycloak" +local r_session = require("resty.session") +local openidc = require("resty.openidc") + + + +local schema = { + type = "object", + properties = { + token_endpoint = {type = "string", minLength = 1, maxLength = 4096}, + permissions = { + type = "array", + items = { + type = "string", + minLength = 1, maxLength = 100 + }, + uniqueItems = true + }, + grant_type = { + type = "string", + default="urn:ietf:params:oauth:grant-type:uma-ticket", + enum = {"urn:ietf:params:oauth:grant-type:uma-ticket"}, + minLength = 1, maxLength = 100 + }, + audience = {type = "string", minLength = 1, maxLength = 100}, + timeout = {type = "integer", minimum = 1000, default = 3000}, + policy_enforcement_mode = { + type = "string", + enum = {"ENFORCING", "PERMISSIVE"}, + default = "ENFORCING" + }, + keepalive = {type = "boolean", default = true}, + keepalive_timeout = {type = "integer", minimum = 1000, default = 60000}, + keepalive_pool = {type = "integer", minimum = 1, default = 5}, + ssl_verify = {type = "boolean", default = true}, + client_id = {type = "string", minLength = 1, maxLength = 100}, + client_secret = {type = "string", minLength = 1, maxLength = 100}, + }, + required = {"token_endpoint"} +} + + +local _M = { + version = 0.1, + priority = 2000, + name = plugin_name, + schema = schema, +} + +function _M.check_schema(conf) + return core.schema.check(schema, conf) +end + +local function is_path_protected(conf) + -- TODO if permissions are empty lazy load paths from Keycloak + if conf.permissions == nil then + return false + end + return true +end + + +local function evaluate_permissions(conf, token) + local url_decoded = url.parse(conf.token_endpoint) + local host = url_decoded.host + local port = url_decoded.port + + if not port then + if url_decoded.scheme == "https" then + port = 443 + else + port = 80 + end + end + + if not is_path_protected(conf) and conf.policy_enforcement_mode == "ENFORCING" then + return 403 + end + + core.log.error("Getting session for Protection API access.") + local session = r_session.start({id = "authz/" .. conf.token_endpoint, storage = "shm"}) + core.log.error("Got session for Protection API access.") + + if session.data.access_token == nil then + core.log.error("Session doesn't contain an access token yet.") + local httpc = http.new() + httpc:set_timeout(conf.timeout) + + local params = { + method = "POST", + body = ngx.encode_args({ + grant_type = "client_credentials", + client_id = conf.client_id, + client_secret = conf.client_secret, + }), + ssl_verify = conf.ssl_verify, + headers = { + ["Content-Type"] = "application/x-www-form-urlencoded" + } + } + + if conf.keepalive then + params.keepalive_timeout = conf.keepalive_timeout + params.keepalive_pool = conf.keepalive_pool + else + params.keepalive = conf.keepalive + end + + core.log.error("Sending request to token endpoint to obtain access token.") + local httpc_res, httpc_err = httpc:request_uri(conf.token_endpoint, params) + core.log.error("Response body: ", httpc_res.body) + end + + -- local token, err = openidc.access_token(opts, {storage = "shm"}) + + local httpc = http.new() + httpc:set_timeout(conf.timeout) + + local params = { + method = "POST", + body = ngx.encode_args({ + grant_type = conf.grant_type, + audience = conf.audience, + response_mode = "decision", + permission = conf.permissions + }), + ssl_verify = conf.ssl_verify, + headers = { + ["Content-Type"] = "application/x-www-form-urlencoded", + ["Authorization"] = token + } + } + + if conf.keepalive then + params.keepalive_timeout = conf.keepalive_timeout + params.keepalive_pool = conf.keepalive_pool + else + params.keepalive = conf.keepalive + end + + local httpc_res, httpc_err = httpc:request_uri(conf.token_endpoint, params) + + if not httpc_res then + core.log.error("error while sending authz request to [", host ,"] port[", + tostring(port), "] ", httpc_err) + return 500, httpc_err + end + + if httpc_res.status >= 400 then + core.log.error("status code: ", httpc_res.status, " msg: ", httpc_res.body) + return httpc_res.status, httpc_res.body + end +end + + +local function fetch_jwt_token(ctx) + local token = core.request.header(ctx, "Authorization") + if not token then + return nil, "authorization header not available" + end + + local prefix = sub_str(token, 1, 7) + if prefix ~= 'Bearer ' and prefix ~= 'bearer ' then + return "Bearer " .. token + end + return token +end + + +function _M.access(conf, ctx) + core.log.debug("hit keycloak-auth access") + local jwt_token, err = fetch_jwt_token(ctx) + if not jwt_token then + core.log.error("failed to fetch JWT token: ", err) + return 401, {message = "Missing JWT token in request"} + end + + local status, body = evaluate_permissions(conf, jwt_token) + if status then + return status, body + end +end + + +return _M From fd96cb67cfcb8a221d49e54e41720cd809d56aeb Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Mon, 11 Jan 2021 17:23:29 +0100 Subject: [PATCH 02/94] Fix session id parameter name. --- apisix/plugins/authz-keycloak0.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apisix/plugins/authz-keycloak0.lua b/apisix/plugins/authz-keycloak0.lua index 4e06d9190038..5931b4e3a0a6 100644 --- a/apisix/plugins/authz-keycloak0.lua +++ b/apisix/plugins/authz-keycloak0.lua @@ -100,7 +100,7 @@ local function evaluate_permissions(conf, token) end core.log.error("Getting session for Protection API access.") - local session = r_session.start({id = "authz/" .. conf.token_endpoint, storage = "shm"}) + local session = r_session.start({identifier = "authz/" .. conf.token_endpoint, storage = "shm"}) core.log.error("Got session for Protection API access.") if session.data.access_token == nil then From 8aedd8970f1713b5403fc73533cf4e607e7df5d6 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Mon, 11 Jan 2021 20:23:53 +0100 Subject: [PATCH 03/94] Adjust Nginx config template to allow setting trusted TLS certificates even when not using TLS for the server itself. --- apisix/cli/ngx_tpl.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apisix/cli/ngx_tpl.lua b/apisix/cli/ngx_tpl.lua index 9f496d22ca83..26e979b00be2 100644 --- a/apisix/cli/ngx_tpl.lua +++ b/apisix/cli/ngx_tpl.lua @@ -369,16 +369,16 @@ http { {% end %} {% end %} {% -- if enable_ipv6 %} + {% if ssl.ssl_trusted_certificate ~= nil then %} + lua_ssl_trusted_certificate {* ssl.ssl_trusted_certificate *}; + {% end %} + {% if ssl.enable then %} ssl_certificate {* ssl.ssl_cert *}; ssl_certificate_key {* ssl.ssl_cert_key *}; ssl_session_cache shared:SSL:20m; ssl_session_timeout 10m; - {% if ssl.ssl_trusted_certificate ~= nil then %} - lua_ssl_trusted_certificate {* ssl.ssl_trusted_certificate *}; - {% end %} - ssl_protocols {* ssl.ssl_protocols *}; ssl_ciphers {* ssl.ssl_ciphers *}; ssl_prefer_server_ciphers on; From 43364464abcb81d0ab0da08f009e0a652b70cf6c Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Mon, 11 Jan 2021 20:59:34 +0100 Subject: [PATCH 04/94] Fix plugin name. --- apisix/plugins/authz-keycloak0.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apisix/plugins/authz-keycloak0.lua b/apisix/plugins/authz-keycloak0.lua index 5931b4e3a0a6..47e5fd55f361 100644 --- a/apisix/plugins/authz-keycloak0.lua +++ b/apisix/plugins/authz-keycloak0.lua @@ -20,7 +20,7 @@ local sub_str = string.sub local url = require "net.url" local tostring = tostring local ngx = ngx -local plugin_name = "authz-keycloak" +local plugin_name = "authz-keycloak0" local r_session = require("resty.session") local openidc = require("resty.openidc") @@ -190,7 +190,7 @@ end function _M.access(conf, ctx) - core.log.debug("hit keycloak-auth access") + core.log.error("hit keycloak-auth access0") local jwt_token, err = fetch_jwt_token(ctx) if not jwt_token then core.log.error("failed to fetch JWT token: ", err) From 2d90fb4f9aa4e8de55f5564278d9a5f19b35ae9f Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Tue, 12 Jan 2021 10:03:14 +0100 Subject: [PATCH 05/94] Debugging. --- apisix/plugins/authz-keycloak0.lua | 61 ++++++++++++++---------------- 1 file changed, 29 insertions(+), 32 deletions(-) diff --git a/apisix/plugins/authz-keycloak0.lua b/apisix/plugins/authz-keycloak0.lua index 47e5fd55f361..5cfab07246a2 100644 --- a/apisix/plugins/authz-keycloak0.lua +++ b/apisix/plugins/authz-keycloak0.lua @@ -21,8 +21,9 @@ local url = require "net.url" local tostring = tostring local ngx = ngx local plugin_name = "authz-keycloak0" -local r_session = require("resty.session") local openidc = require("resty.openidc") +local cjson = require("cjson") +local cjson_s = require("cjson.safe") @@ -99,41 +100,37 @@ local function evaluate_permissions(conf, token) return 403 end - core.log.error("Getting session for Protection API access.") - local session = r_session.start({identifier = "authz/" .. conf.token_endpoint, storage = "shm"}) - core.log.error("Got session for Protection API access.") - - if session.data.access_token == nil then - core.log.error("Session doesn't contain an access token yet.") - local httpc = http.new() - httpc:set_timeout(conf.timeout) - - local params = { - method = "POST", - body = ngx.encode_args({ - grant_type = "client_credentials", - client_id = conf.client_id, - client_secret = conf.client_secret, - }), - ssl_verify = conf.ssl_verify, - headers = { - ["Content-Type"] = "application/x-www-form-urlencoded" - } - } + core.log.error("Getting access token for Protection API.") + local httpc = http.new() + httpc:set_timeout(conf.timeout) - if conf.keepalive then - params.keepalive_timeout = conf.keepalive_timeout - params.keepalive_pool = conf.keepalive_pool - else - params.keepalive = conf.keepalive - end + local params = { + method = "POST", + body = ngx.encode_args({ + grant_type = "client_credentials", + client_id = conf.client_id, + client_secret = conf.client_secret, + }), + ssl_verify = conf.ssl_verify, + headers = { + ["Content-Type"] = "application/x-www-form-urlencoded" + } + } - core.log.error("Sending request to token endpoint to obtain access token.") - local httpc_res, httpc_err = httpc:request_uri(conf.token_endpoint, params) - core.log.error("Response body: ", httpc_res.body) + if conf.keepalive then + params.keepalive_timeout = conf.keepalive_timeout + params.keepalive_pool = conf.keepalive_pool + else + params.keepalive = conf.keepalive end - -- local token, err = openidc.access_token(opts, {storage = "shm"}) + core.log.error("Sending request to token endpoint to obtain access token.") + local httpc_res, httpc_err = httpc:request_uri(conf.token_endpoint, params) + core.log.error("Response body: ", httpc_res.body) + local json = cjson_s.decode(httpc_res.body) + core.log.error("Access token: ", json.access_token) + core.log.error("Expires in: ", json.expires_in) + core.log.error("Refresh token: ", json.refresh_token) local httpc = http.new() httpc:set_timeout(conf.timeout) From 1c09d309df66f7c5c109a217efa4e1b05b282feb Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Tue, 12 Jan 2021 12:06:46 +0100 Subject: [PATCH 06/94] Query matching resources from server. --- apisix/plugins/authz-keycloak0.lua | 35 ++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/apisix/plugins/authz-keycloak0.lua b/apisix/plugins/authz-keycloak0.lua index 5cfab07246a2..3753756b8bbf 100644 --- a/apisix/plugins/authz-keycloak0.lua +++ b/apisix/plugins/authz-keycloak0.lua @@ -58,6 +58,7 @@ local schema = { ssl_verify = {type = "boolean", default = true}, client_id = {type = "string", minLength = 1, maxLength = 100}, client_secret = {type = "string", minLength = 1, maxLength = 100}, + resource_set_endpoint = {type = "string", minLength = 1, maxLength = 4096}, }, required = {"token_endpoint"} } @@ -83,7 +84,7 @@ local function is_path_protected(conf) end -local function evaluate_permissions(conf, token) +local function evaluate_permissions(conf, token, uri) local url_decoded = url.parse(conf.token_endpoint) local host = url_decoded.host local port = url_decoded.port @@ -100,6 +101,8 @@ local function evaluate_permissions(conf, token) return 403 end + -- Get access token for Protection API. + core.log.error("Getting access token for Protection API.") local httpc = http.new() httpc:set_timeout(conf.timeout) @@ -131,6 +134,34 @@ local function evaluate_permissions(conf, token) core.log.error("Access token: ", json.access_token) core.log.error("Expires in: ", json.expires_in) core.log.error("Refresh token: ", json.refresh_token) + core.log.error("Refresh expires in: ", json.refresh_expires_in) + + -- Get ID of resource trying to access. + core.log.error("Request URI: ", uri) + local httpc = http.new() + httpc:set_timeout(conf.timeout) + + local params = { + method = "GET", + query = {uri = uri, matchingUri = "true"}, + ssl_verify = conf.ssl_verify, + headers = { + ["Auhtorization"] = json.access_token + } + } + + if conf.keepalive then + params.keepalive_timeout = conf.keepalive_timeout + params.keepalive_pool = conf.keepalive_pool + else + params.keepalive = conf.keepalive + end + + core.log.error("Sending request to token endpoint to obtain access token.") + local httpc_res, httpc_err = httpc:request_uri(conf.resource_set_endpoint, params) + core.log.error("Response body: ", httpc_res.body) + + local httpc = http.new() httpc:set_timeout(conf.timeout) @@ -194,7 +225,7 @@ function _M.access(conf, ctx) return 401, {message = "Missing JWT token in request"} end - local status, body = evaluate_permissions(conf, jwt_token) + local status, body = evaluate_permissions(conf, jwt_token, ctx.var.request_uri) if status then return status, body end From 6e36faf7b65e919f38db8033524f389cf29c0cda Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Tue, 12 Jan 2021 12:23:29 +0100 Subject: [PATCH 07/94] Continue build out. --- apisix/plugins/authz-keycloak0.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apisix/plugins/authz-keycloak0.lua b/apisix/plugins/authz-keycloak0.lua index 3753756b8bbf..0df7e11c476d 100644 --- a/apisix/plugins/authz-keycloak0.lua +++ b/apisix/plugins/authz-keycloak0.lua @@ -146,7 +146,7 @@ local function evaluate_permissions(conf, token, uri) query = {uri = uri, matchingUri = "true"}, ssl_verify = conf.ssl_verify, headers = { - ["Auhtorization"] = json.access_token + ["Authorization"] = "Bearer " .. json.access_token } } @@ -160,6 +160,10 @@ local function evaluate_permissions(conf, token, uri) core.log.error("Sending request to token endpoint to obtain access token.") local httpc_res, httpc_err = httpc:request_uri(conf.resource_set_endpoint, params) core.log.error("Response body: ", httpc_res.body) + local json = = cjson_s.decode('{"ids": ' .. httpc_res.body .. '}') + for k, id in pairs(json.ids) do + core.log.error("Matched resource: ", id) + end From fc474926e2b0e64117cca463985f995b8d1a9d19 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Tue, 12 Jan 2021 12:34:19 +0100 Subject: [PATCH 08/94] More build out. --- apisix/plugins/authz-keycloak0.lua | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/apisix/plugins/authz-keycloak0.lua b/apisix/plugins/authz-keycloak0.lua index 0df7e11c476d..4baeca8a0f4f 100644 --- a/apisix/plugins/authz-keycloak0.lua +++ b/apisix/plugins/authz-keycloak0.lua @@ -84,7 +84,7 @@ local function is_path_protected(conf) end -local function evaluate_permissions(conf, token, uri) +local function evaluate_permissions(conf, token, uri, ctx) local url_decoded = url.parse(conf.token_endpoint) local host = url_decoded.host local port = url_decoded.port @@ -160,11 +160,19 @@ local function evaluate_permissions(conf, token, uri) core.log.error("Sending request to token endpoint to obtain access token.") local httpc_res, httpc_err = httpc:request_uri(conf.resource_set_endpoint, params) core.log.error("Response body: ", httpc_res.body) - local json = = cjson_s.decode('{"ids": ' .. httpc_res.body .. '}') + local json = cjson_s.decode('{"ids": ' .. httpc_res.body .. '}') for k, id in pairs(json.ids) do core.log.error("Matched resource: ", id) end + -- Determine scope. + local scope = ctx.var.request_method + + local permissions = {} + for k, id in pairs(json.ids) do + permissions[#permissions+1] = id .. "#" .. scope + core.log.error("Requested permission: ", permissions[#permissions]) + end local httpc = http.new() @@ -176,7 +184,7 @@ local function evaluate_permissions(conf, token, uri) grant_type = conf.grant_type, audience = conf.audience, response_mode = "decision", - permission = conf.permissions + permission = permissions }), ssl_verify = conf.ssl_verify, headers = { @@ -229,7 +237,7 @@ function _M.access(conf, ctx) return 401, {message = "Missing JWT token in request"} end - local status, body = evaluate_permissions(conf, jwt_token, ctx.var.request_uri) + local status, body = evaluate_permissions(conf, jwt_token, ctx.var.request_uri, ctx) if status then return status, body end From 51be98c09e4a2a576cff92fdea472e761a3639b7 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Tue, 12 Jan 2021 14:50:44 +0100 Subject: [PATCH 09/94] Add UMA discovery. --- apisix/plugins/authz-keycloak0.lua | 179 ++++++++++++++++++++++++++--- 1 file changed, 164 insertions(+), 15 deletions(-) diff --git a/apisix/plugins/authz-keycloak0.lua b/apisix/plugins/authz-keycloak0.lua index 4baeca8a0f4f..073d7251bc6c 100644 --- a/apisix/plugins/authz-keycloak0.lua +++ b/apisix/plugins/authz-keycloak0.lua @@ -20,17 +20,20 @@ local sub_str = string.sub local url = require "net.url" local tostring = tostring local ngx = ngx -local plugin_name = "authz-keycloak0" -local openidc = require("resty.openidc") local cjson = require("cjson") local cjson_s = require("cjson.safe") +local plugin_name = "authz-keycloak0" +local log = core.log + local schema = { type = "object", properties = { + discovery = {type = "string", minLength = 1, maxLength = 4096}, token_endpoint = {type = "string", minLength = 1, maxLength = 4096}, + resource_registration_endpoint = {type = "string", minLength = 1, maxLength = 4096}, permissions = { type = "array", items = { @@ -58,7 +61,6 @@ local schema = { ssl_verify = {type = "boolean", default = true}, client_id = {type = "string", minLength = 1, maxLength = 100}, client_secret = {type = "string", minLength = 1, maxLength = 100}, - resource_set_endpoint = {type = "string", minLength = 1, maxLength = 4096}, }, required = {"token_endpoint"} } @@ -83,22 +85,158 @@ local function is_path_protected(conf) return true end +-- Retrieve value from server-wide cache, if available. +local function authz_keycloak_cache_get(type, key) + local dict = ngx.shared[type] + local value + if dict then + value = dict:get(key) + if value then log(DEBUG, "cache hit: type=", type, " key=", key) end + end + return value +end + +-- Set value in server-wide cache, if available. +local function authz_keycloak_cache_set(type, key, value, exp) + local dict = ngx.shared[type] + if dict and (exp > 0) then + local success, err, forcible = dict:set(key, value, exp) + log.debug("cache set: success=", success, " err=", err, " forcible=", forcible) + end +end + +local function authz_keycloak_configure_timeouts(httpc, timeout) + if timeout then + if type(timeout) == "table" then + local r, e = httpc:set_timeouts(timeout.connect or 0, timeout.send or 0, timeout.read or 0) + else + local r, e = httpc:set_timeout(timeout) + end + end +end + +-- Set outgoing proxy options. +local function authz_keycloak_configure_proxy(httpc, proxy_opts) + if httpc and proxy_opts and type(proxy_opts) == "table" then + log(DEBUG, "authz_keycloak_configure_proxy : use http proxy") + httpc:set_proxy_options(proxy_opts) + else + log(DEBUG, "authz_keycloak_configure_proxy : don't use http proxy") + end +end + +-- Parse the JSON result from a call to the OP. +local function authz_keycloak_parse_json_response(response, ignore_body_on_success) + local ignore_body_on_success = ignore_body_on_success or false + + local err + local res + + -- Check the response from the OP. + if response.status ~= 200 then + err = "response indicates failure, status=" .. response.status .. ", body=" .. response.body + else + if ignore_body_on_success then + return nil, nil + end + + -- Decode the response and extract the JSON object. + res = cjson_s.decode(response.body) + + if not res then + err = "JSON decoding failed" + end + end + + return res, err +end + +-- get the Discovery metadata from the specified URL. +local function authz_keycloak_discover(url, ssl_verify, keepalive, timeout, exptime, proxy_opts, http_request_decorator) + log.debug("authz_keycloak_discover: URL is: " .. url) + + local json, err + local v = authz_keycloak_cache_get("discovery", url) + if not v then + + log.debug("Discovery data not in cache, making call to discovery endpoint.") + -- Make the call to the discovery endpoint. + local httpc = http.new() + authz_keycloak_configure_timeouts(httpc, timeout) + authz_keycloak_configure_proxy(httpc, proxy_opts) + local res, error = httpc:request_uri(url, decorate_request(http_request_decorator, { + ssl_verify = (ssl_verify ~= "no"), + keepalive = (keepalive ~= "no") + })) + if not res then + err = "accessing discovery url (" .. url .. ") failed: " .. error + log.error(err) + else + log.debug("response data: " .. res.body) + json, err = authz_keycloak_parse_json_response(res) + if json then + authz_keycloak_cache_set("discovery", url, cjson.encode(json), exptime or 24 * 60 * 60) + else + err = "could not decode JSON from Discovery data" .. (err and (": " .. err) or '') + log.error(err) + end + end + + else + json = cjson.decode(v) + end + + return json, err +end + +-- Turn a discovery url set in the opts dictionary into the discovered information. +local function authz_keycloak_ensure_discovered_data(opts) + local err + if type(opts.discovery) == "string" then + local discovery + discovery, err = authz_keycloak_discover(opts.discovery, opts.ssl_verify, opts.keepalive, opts.timeout, opts.jwk_expires_in, opts.proxy_opts, opts.http_request_decorator) + if not err then + opts.discovery = discovery + end + end + return err +end local function evaluate_permissions(conf, token, uri, ctx) - local url_decoded = url.parse(conf.token_endpoint) - local host = url_decoded.host - local port = url_decoded.port + if not is_path_protected(conf) and conf.policy_enforcement_mode == "ENFORCING" then + return 403 + end + + -- Ensure discovered data. + local err = authz_keycloak_ensure_discovered_data(conf) + if err then + return nil, err + end - if not port then - if url_decoded.scheme == "https" then - port = 443 + -- Get token endpoint URL. + local token_endpoint + if not (conf and (conf.token_endpoint or (conf.discovery and conf.discovery.token_endpoint))) then + log.error("No token endpoint supplied.") + return 500, "No token endpoint supplied." + else + if conf.token_endpoint then + token_endpoint = conf.token_endpoint else - port = 80 + token_endpoint = conf.discovery.token_endpoint end end - if not is_path_protected(conf) and conf.policy_enforcement_mode == "ENFORCING" then - return 403 + -- Get resource registration endpoint URL. + local resource_registration_endpoint + if not (conf and (conf.resource_registration_endpoint or (conf.discovery and conf.discovery.resource_registration_endpoint))) then + log.error("No resource registration endpoint supplied.") + return 500, "No resource registration endpoint supplied." + else + if conf.token_endpoint then + resource_registration_endpoint = conf.resource_registration_endpoint + else + resource_registration_endpoint = conf.discovery.resource_registration_endpoint + end end -- Get access token for Protection API. @@ -128,7 +266,7 @@ local function evaluate_permissions(conf, token, uri, ctx) end core.log.error("Sending request to token endpoint to obtain access token.") - local httpc_res, httpc_err = httpc:request_uri(conf.token_endpoint, params) + local httpc_res, httpc_err = httpc:request_uri(token_endpoint, params) core.log.error("Response body: ", httpc_res.body) local json = cjson_s.decode(httpc_res.body) core.log.error("Access token: ", json.access_token) @@ -158,7 +296,7 @@ local function evaluate_permissions(conf, token, uri, ctx) end core.log.error("Sending request to token endpoint to obtain access token.") - local httpc_res, httpc_err = httpc:request_uri(conf.resource_set_endpoint, params) + local httpc_res, httpc_err = httpc:request_uri(resource_registration_endpoint, params) core.log.error("Response body: ", httpc_res.body) local json = cjson_s.decode('{"ids": ' .. httpc_res.body .. '}') for k, id in pairs(json.ids) do @@ -200,9 +338,20 @@ local function evaluate_permissions(conf, token, uri, ctx) params.keepalive = conf.keepalive end - local httpc_res, httpc_err = httpc:request_uri(conf.token_endpoint, params) + local httpc_res, httpc_err = httpc:request_uri(token_endpoint, params) if not httpc_res then + local url_decoded = url.parse(conf.token_endpoint) + local host = url_decoded.host + local port = url_decoded.port + + if not port then + if url_decoded.scheme == "https" then + port = 443 + else + port = 80 + end + end core.log.error("error while sending authz request to [", host ,"] port[", tostring(port), "] ", httpc_err) return 500, httpc_err From d16dd16e833919b657171b20d62aea6e9389d74e Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Tue, 12 Jan 2021 14:52:58 +0100 Subject: [PATCH 10/94] Remove audience parameter in favour of client_id. --- apisix/plugins/authz-keycloak0.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apisix/plugins/authz-keycloak0.lua b/apisix/plugins/authz-keycloak0.lua index 073d7251bc6c..7483fbd96de5 100644 --- a/apisix/plugins/authz-keycloak0.lua +++ b/apisix/plugins/authz-keycloak0.lua @@ -48,7 +48,6 @@ local schema = { enum = {"urn:ietf:params:oauth:grant-type:uma-ticket"}, minLength = 1, maxLength = 100 }, - audience = {type = "string", minLength = 1, maxLength = 100}, timeout = {type = "integer", minimum = 1000, default = 3000}, policy_enforcement_mode = { type = "string", @@ -320,7 +319,7 @@ local function evaluate_permissions(conf, token, uri, ctx) method = "POST", body = ngx.encode_args({ grant_type = conf.grant_type, - audience = conf.audience, + audience = conf.client_id, response_mode = "decision", permission = permissions }), From 0deede0e54a3c1580cbfe01077008a0668f4d9db Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Tue, 12 Jan 2021 14:55:15 +0100 Subject: [PATCH 11/94] Add request decorator. --- apisix/plugins/authz-keycloak0.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apisix/plugins/authz-keycloak0.lua b/apisix/plugins/authz-keycloak0.lua index 7483fbd96de5..9a372f2040e4 100644 --- a/apisix/plugins/authz-keycloak0.lua +++ b/apisix/plugins/authz-keycloak0.lua @@ -150,6 +150,10 @@ local function authz_keycloak_parse_json_response(response, ignore_body_on_succe return res, err end +local function decorate_request(http_request_decorator, req) + return http_request_decorator and http_request_decorator(req) or req +end + -- get the Discovery metadata from the specified URL. local function authz_keycloak_discover(url, ssl_verify, keepalive, timeout, exptime, proxy_opts, http_request_decorator) log.debug("authz_keycloak_discover: URL is: " .. url) From 67d4823b7c7a087de1039dfdcc35880a7f04baf3 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Tue, 12 Jan 2021 15:43:50 +0100 Subject: [PATCH 12/94] Make token endpoint optional. --- apisix/plugins/authz-keycloak0.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apisix/plugins/authz-keycloak0.lua b/apisix/plugins/authz-keycloak0.lua index 9a372f2040e4..e6919d78ea1a 100644 --- a/apisix/plugins/authz-keycloak0.lua +++ b/apisix/plugins/authz-keycloak0.lua @@ -61,7 +61,7 @@ local schema = { client_id = {type = "string", minLength = 1, maxLength = 100}, client_secret = {type = "string", minLength = 1, maxLength = 100}, }, - required = {"token_endpoint"} + required = {} } From 5ddfe3d4402013636d773df167eecc52bb8e5eb7 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Tue, 12 Jan 2021 15:51:32 +0100 Subject: [PATCH 13/94] Small fixes. --- apisix/plugins/authz-keycloak0.lua | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apisix/plugins/authz-keycloak0.lua b/apisix/plugins/authz-keycloak0.lua index e6919d78ea1a..70b4f0136676 100644 --- a/apisix/plugins/authz-keycloak0.lua +++ b/apisix/plugins/authz-keycloak0.lua @@ -20,8 +20,8 @@ local sub_str = string.sub local url = require "net.url" local tostring = tostring local ngx = ngx -local cjson = require("cjson") -local cjson_s = require("cjson.safe") +local cjson = require("cjson") +local cjson_s = require("cjson.safe") local plugin_name = "authz-keycloak0" local log = core.log @@ -60,8 +60,7 @@ local schema = { ssl_verify = {type = "boolean", default = true}, client_id = {type = "string", minLength = 1, maxLength = 100}, client_secret = {type = "string", minLength = 1, maxLength = 100}, - }, - required = {} + } } From c032bfb0ac119dc3f08b2252f419a7a3bc40341f Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Tue, 12 Jan 2021 16:11:06 +0100 Subject: [PATCH 14/94] Add debug output. --- apisix/plugins/authz-keycloak0.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apisix/plugins/authz-keycloak0.lua b/apisix/plugins/authz-keycloak0.lua index 70b4f0136676..d82f82aceb29 100644 --- a/apisix/plugins/authz-keycloak0.lua +++ b/apisix/plugins/authz-keycloak0.lua @@ -227,6 +227,7 @@ local function evaluate_permissions(conf, token, uri, ctx) token_endpoint = conf.discovery.token_endpoint end end + log.error("Token endpoint: ", token_endpoint) -- Get resource registration endpoint URL. local resource_registration_endpoint @@ -240,6 +241,7 @@ local function evaluate_permissions(conf, token, uri, ctx) resource_registration_endpoint = conf.discovery.resource_registration_endpoint end end + log.error("Resource registration endpoint: ", resource_registration_endpoint) -- Get access token for Protection API. From 1ca808dedb848802f4b901534049d5a7017a7897 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Tue, 12 Jan 2021 16:42:04 +0100 Subject: [PATCH 15/94] Polishing. --- apisix/plugins/authz-keycloak0.lua | 61 ++++++++++++++++-------------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/apisix/plugins/authz-keycloak0.lua b/apisix/plugins/authz-keycloak0.lua index d82f82aceb29..06ee5fe07bcc 100644 --- a/apisix/plugins/authz-keycloak0.lua +++ b/apisix/plugins/authz-keycloak0.lua @@ -89,7 +89,7 @@ local function authz_keycloak_cache_get(type, key) local value if dict then value = dict:get(key) - if value then log(DEBUG, "cache hit: type=", type, " key=", key) end + if value then log.debug("cache hit: type=", type, " key=", key) end end return value end @@ -116,10 +116,10 @@ end -- Set outgoing proxy options. local function authz_keycloak_configure_proxy(httpc, proxy_opts) if httpc and proxy_opts and type(proxy_opts) == "table" then - log(DEBUG, "authz_keycloak_configure_proxy : use http proxy") + log.debug("authz_keycloak_configure_proxy : use http proxy") httpc:set_proxy_options(proxy_opts) else - log(DEBUG, "authz_keycloak_configure_proxy : don't use http proxy") + log.debug("authz_keycloak_configure_proxy : don't use http proxy") end end @@ -204,6 +204,24 @@ local function authz_keycloak_ensure_discovered_data(opts) return err end +local function authz_keycloak_get_endpoint(conf, endpoint) + if conf and conf[endpoint] then + return conf[endpoint] + elseif conf and conf.discovery and type(conf.discovery) == "table" + return = conf.discovery[endpoint] + end + + return nil +end + +local function authz_keycloak_get_token_endpoint(conf) + return authz_keycloak_get_endpoint(conf, "token_endpoint") +end + +local function authz_keycloak_get_resource_registration_endpoint(conf) + return authz_keycloak_get_endpoint(conf, "resource_registration_endpoint") +end + local function evaluate_permissions(conf, token, uri, ctx) if not is_path_protected(conf) and conf.policy_enforcement_mode == "ENFORCING" then return 403 @@ -216,36 +234,15 @@ local function evaluate_permissions(conf, token, uri, ctx) end -- Get token endpoint URL. - local token_endpoint - if not (conf and (conf.token_endpoint or (conf.discovery and conf.discovery.token_endpoint))) then + local token_endpoint = authz_keycloak_get_token_endpoint(conf) + if not token_endpoint then log.error("No token endpoint supplied.") return 500, "No token endpoint supplied." - else - if conf.token_endpoint then - token_endpoint = conf.token_endpoint - else - token_endpoint = conf.discovery.token_endpoint - end end - log.error("Token endpoint: ", token_endpoint) - - -- Get resource registration endpoint URL. - local resource_registration_endpoint - if not (conf and (conf.resource_registration_endpoint or (conf.discovery and conf.discovery.resource_registration_endpoint))) then - log.error("No resource registration endpoint supplied.") - return 500, "No resource registration endpoint supplied." - else - if conf.token_endpoint then - resource_registration_endpoint = conf.resource_registration_endpoint - else - resource_registration_endpoint = conf.discovery.resource_registration_endpoint - end - end - log.error("Resource registration endpoint: ", resource_registration_endpoint) + log.debug("Token endpoint: ", token_endpoint) -- Get access token for Protection API. - - core.log.error("Getting access token for Protection API.") + core.log.error("Getting access token for Protection API from token endpoint.") local httpc = http.new() httpc:set_timeout(conf.timeout) @@ -278,6 +275,14 @@ local function evaluate_permissions(conf, token, uri, ctx) core.log.error("Refresh token: ", json.refresh_token) core.log.error("Refresh expires in: ", json.refresh_expires_in) + -- Get resource registration endpoint URL. + local resource_registration_endpoint = authz_keycloak_get_resource_registration_endpoint(conf) + if not resource_registration_endpoint then + log.error("No resource registration endpoint supplied.") + return 500, "No resource registration endpoint supplied." + end + log.error("Resource registration endpoint: ", resource_registration_endpoint) + -- Get ID of resource trying to access. core.log.error("Request URI: ", uri) local httpc = http.new() From 1d58f48a133dcbcdf4f41412b940d2e98b600190 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Thu, 14 Jan 2021 13:57:12 +0100 Subject: [PATCH 16/94] Add service account access token retrieval. --- apisix/plugins/authz-keycloak.lua | 99 ++++++++++++++++++++++++++++++- 1 file changed, 96 insertions(+), 3 deletions(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index 8c2bee353f1a..8fb4ae573cfc 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -42,7 +42,6 @@ local schema = { enum = {"urn:ietf:params:oauth:grant-type:uma-ticket"}, minLength = 1, maxLength = 100 }, - audience = {type = "string", minLength = 1, maxLength = 100}, timeout = {type = "integer", minimum = 1000, default = 3000}, policy_enforcement_mode = { type = "string", @@ -53,6 +52,8 @@ local schema = { keepalive_timeout = {type = "integer", minimum = 1000, default = 60000}, keepalive_pool = {type = "integer", minimum = 1, default = 5}, ssl_verify = {type = "boolean", default = true}, + client_id = {type = "string", minLength = 1, maxLength = 100}, + client_secret = {type = "string", minLength = 1, maxLength = 100}, }, anyOf = { {required = {"discovery"}}, @@ -171,7 +172,7 @@ local function authz_keycloak_discover(url, ssl_verify, keepalive, timeout, keepalive = (keepalive ~= "no") })) if not res then - err = "accessing discovery url (" .. url .. ") failed: " .. error + err = "Accessing discovery url (" .. url .. ") failed: " .. error log.error(err) else log.debug("response data: " .. res.body) @@ -224,6 +225,92 @@ local function authz_keycloak_get_token_endpoint(conf) end +-- computes access_token expires_in value (in seconds) +local function authz_keycloak_access_token_expires_in(opts, expires_in) + return (expires_in or opts.access_token_expires_in or 300) + - 1 - (opts.access_token_expires_leeway or 0) +end + + +-- computes refresh_token expires_in value (in seconds) +local function authz_keycloak_refresh_token_expires_in(opts, expires_in) + return (expires_in or opts.refresh_token_expires_in or 3600) + - 1 - (opts.refresh_token_expires_leeway or 0) +end + + +local authz_keycloak_ensure_sa_access_token(conf) + local token_endpoint = authz_keycloak_get_token_endpoint(conf) + + if not token_endpoint then + log.error("Unable to determine token endpoint.") + return 500, "Unable to determine token endpoint." + end + + core.log.debug("Getting access token for Protection API from token endpoint.") + local httpc = http.new() + httpc:set_timeout(conf.timeout) + + local params = { + method = "POST", + body = ngx.encode_args({ + grant_type = "client_credentials", + client_id = conf.client_id, + client_secret = conf.client_secret, + }), + ssl_verify = conf.ssl_verify, + headers = { + ["Content-Type"] = "application/x-www-form-urlencoded" + } + } + + local current_time = ngx.time() + + local res, err = httpc:request_uri(token_endpoint, params) + + if not res then + err = "Accessing token endpoint url (" .. url .. ") failed: " .. error + log.error(err) + return nil, err + end + + log.debug("Response data: " .. res.body) + json, err = authz_keycloak_parse_json_response(res) + + if not json + err = "Could not decode JSON from token endpoint" .. (err and (": " .. err) or '.') + log.error(err) + return nil, err + end + + if not json.access_token then + err = "Response does not contain access_token field." + log.error(err) + return nil, err + end + + local session = {} + + -- Save access token. + session.access_token = json.access_token + + -- Calculate and save access token expiry time. + session.access_token_expiration = current_time + + authz_keycloak_access_token_expires_in(conf, json.expires_in) + + -- Save refresh token, maybe. + if json.refresh_token ~= nil then + session.refresh_token = json.refresh_token + + -- Calculate and save refresh token expiry time. + session.refresh_token_expiration = current_time + + authz_keycloak_refresh_token_expires_in(conf, json.refresh_expires_in) + end + + return session.access_token +end + + local function is_path_protected(conf) -- TODO if permissions are empty lazy load paths from Keycloak if conf.permissions == nil then @@ -244,6 +331,12 @@ local function evaluate_permissions(conf, token) return 500, err end + -- Ensure service account access token. + local sa_access_token, err = authz_keycloak_ensure_sa_access_token(conf) + if err then + return 500, err + end + -- Get token endpoint URL. local token_endpoint = authz_keycloak_get_token_endpoint(conf) if not token_endpoint then @@ -259,7 +352,7 @@ local function evaluate_permissions(conf, token) method = "POST", body = ngx.encode_args({ grant_type = conf.grant_type, - audience = conf.audience, + audience = conf.client_id, response_mode = "decision", permission = conf.permissions }), From b5c66268430b3661282ffd7916fdbfe044804e8e Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Thu, 14 Jan 2021 15:06:29 +0100 Subject: [PATCH 17/94] Smaller fixes. --- apisix/plugins/authz-keycloak.lua | 2 +- apisix/plugins/authz-keycloak0.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index 8fb4ae573cfc..e799dacf2821 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -275,7 +275,7 @@ local authz_keycloak_ensure_sa_access_token(conf) end log.debug("Response data: " .. res.body) - json, err = authz_keycloak_parse_json_response(res) + local json, err = authz_keycloak_parse_json_response(res) if not json err = "Could not decode JSON from token endpoint" .. (err and (": " .. err) or '.') diff --git a/apisix/plugins/authz-keycloak0.lua b/apisix/plugins/authz-keycloak0.lua index 06ee5fe07bcc..b2090a5df953 100644 --- a/apisix/plugins/authz-keycloak0.lua +++ b/apisix/plugins/authz-keycloak0.lua @@ -207,7 +207,7 @@ end local function authz_keycloak_get_endpoint(conf, endpoint) if conf and conf[endpoint] then return conf[endpoint] - elseif conf and conf.discovery and type(conf.discovery) == "table" + elseif conf and conf.discovery and type(conf.discovery) == "table" then return = conf.discovery[endpoint] end From 6d7749fa02197592d341ffd5183c95e479271abe Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Thu, 14 Jan 2021 16:16:06 +0100 Subject: [PATCH 18/94] Add complete session management, including use of refresh tokens to re-new access token. --- apisix/cli/ngx_tpl.lua | 3 + apisix/plugins/authz-keycloak.lua | 185 ++++++++++++++++++++++------- apisix/plugins/authz-keycloak0.lua | 2 +- 3 files changed, 143 insertions(+), 47 deletions(-) diff --git a/apisix/cli/ngx_tpl.lua b/apisix/cli/ngx_tpl.lua index 6dda02c7790a..9af2515cd914 100644 --- a/apisix/cli/ngx_tpl.lua +++ b/apisix/cli/ngx_tpl.lua @@ -146,6 +146,9 @@ http { lua_shared_dict jwks 1m; # cache for JWKs lua_shared_dict introspection 10m; # cache for JWT verification results + # for authz-keycloak + lua_shared_dict access_tokens 1m; # cache for service account access tokens + # for custom shared dict {% if http.lua_shared_dicts then %} {% for cache_key, cache_size in pairs(http.lua_shared_dicts) do %} diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index e799dacf2821..24b3f81a788d 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -239,7 +239,7 @@ local function authz_keycloak_refresh_token_expires_in(opts, expires_in) end -local authz_keycloak_ensure_sa_access_token(conf) +local function authz_keycloak_ensure_sa_access_token(conf) local token_endpoint = authz_keycloak_get_token_endpoint(conf) if not token_endpoint then @@ -247,64 +247,157 @@ local authz_keycloak_ensure_sa_access_token(conf) return 500, "Unable to determine token endpoint." end - core.log.debug("Getting access token for Protection API from token endpoint.") - local httpc = http.new() - httpc:set_timeout(conf.timeout) + local session = authz_keycloak_cache_get("access_tokens", token_endpoint .. ":" .. conf.client_id) + + if session then + local current_time = ngx.time() + + if current_time < session.access_token_expiration then + -- Access token is still valid. + log.debug("Access token is still valid.") + return session.access_token + else + -- Access token has expired. + log.debug("Access token has expired.") + if session.refresh_token and (not session.refresh_token_expiration or current_time < refresh_token_expiration) then + -- Try to get a new access token, using the refresh token. + log.debug("Trying to get new access token using refresh token.") + + local httpc = http.new() + httpc:set_timeout(conf.timeout) + + local params = { + method = "POST", + body = ngx.encode_args({ + grant_type = "refresh_token", + client_id = conf.client_id, + refresh_token = session.refresh_token, + }), + ssl_verify = conf.ssl_verify, + headers = { + ["Content-Type"] = "application/x-www-form-urlencoded" + } + } + + local current_time = ngx.time() + + local res, err = httpc:request_uri(token_endpoint, params) + + if not res then + err = "Accessing token endpoint url (" .. url .. ") failed: " .. error + log.error(err) + return nil, err + end + + log.debug("Response data: " .. res.body) + + local json, err = authz_keycloak_parse_json_response(res) + + if not json then + err = "Could not decode JSON from token endpoint" .. (err and (": " .. err) or '.') + log.error(err) + return nil, err + end + + if not json.access_token then + -- Clear session. + log.debug("Answer didn't contain a new access token. Clearing session.") + session = nil + else + log.debug("Got new access token.") + -- Save access token. + session.access_token = json.access_token + + -- Calculate and save access token expiry time. + session.access_token_expiration = current_time + + authz_keycloak_access_token_expires_in(conf, json.expires_in) + + -- Save refresh token, maybe. + if json.refresh_token ~= nil then + log.debug("Got new refresh token.") + session.refresh_token = json.refresh_token + + -- Calculate and save refresh token expiry time. + session.refresh_token_expiration = current_time + + authz_keycloak_refresh_token_expires_in(conf, json.refresh_expires_in) + end + + authz_keycloak_cache_set("access_tokens", token_endpoint .. ":" .. conf.client_id, + core.json.encode(session), 24 * 60 * 60) + end + else + -- No refresh token available, or it has expired. Clear session. + log.debug("No or expired refresh token. Clearing session.") + session = nil + end + end + end - local params = { - method = "POST", - body = ngx.encode_args({ - grant_type = "client_credentials", - client_id = conf.client_id, - client_secret = conf.client_secret, - }), - ssl_verify = conf.ssl_verify, - headers = { - ["Content-Type"] = "application/x-www-form-urlencoded" + if not session then + -- No session available. Create a new one. + + core.log.debug("Getting access token for Protection API from token endpoint.") + local httpc = http.new() + httpc:set_timeout(conf.timeout) + + local params = { + method = "POST", + body = ngx.encode_args({ + grant_type = "client_credentials", + client_id = conf.client_id, + client_secret = conf.client_secret, + }), + ssl_verify = conf.ssl_verify, + headers = { + ["Content-Type"] = "application/x-www-form-urlencoded" + } } - } - local current_time = ngx.time() + local current_time = ngx.time() - local res, err = httpc:request_uri(token_endpoint, params) + local res, err = httpc:request_uri(token_endpoint, params) - if not res then - err = "Accessing token endpoint url (" .. url .. ") failed: " .. error - log.error(err) - return nil, err - end + if not res then + err = "Accessing token endpoint url (" .. url .. ") failed: " .. error + log.error(err) + return nil, err + end - log.debug("Response data: " .. res.body) - local json, err = authz_keycloak_parse_json_response(res) + log.debug("Response data: " .. res.body) + local json, err = authz_keycloak_parse_json_response(res) - if not json - err = "Could not decode JSON from token endpoint" .. (err and (": " .. err) or '.') - log.error(err) - return nil, err - end + if not json then + err = "Could not decode JSON from token endpoint" .. (err and (": " .. err) or '.') + log.error(err) + return nil, err + end - if not json.access_token then - err = "Response does not contain access_token field." - log.error(err) - return nil, err - end + if not json.access_token then + err = "Response does not contain access_token field." + log.error(err) + return nil, err + end + + session = {} - local session = {} + -- Save access token. + session.access_token = json.access_token - -- Save access token. - session.access_token = json.access_token + -- Calculate and save access token expiry time. + session.access_token_expiration = current_time + + authz_keycloak_access_token_expires_in(conf, json.expires_in) - -- Calculate and save access token expiry time. - session.access_token_expiration = current_time - + authz_keycloak_access_token_expires_in(conf, json.expires_in) + -- Save refresh token, maybe. + if json.refresh_token ~= nil then + session.refresh_token = json.refresh_token - -- Save refresh token, maybe. - if json.refresh_token ~= nil then - session.refresh_token = json.refresh_token + -- Calculate and save refresh token expiry time. + session.refresh_token_expiration = current_time + + authz_keycloak_refresh_token_expires_in(conf, json.refresh_expires_in) + end - -- Calculate and save refresh token expiry time. - session.refresh_token_expiration = current_time - + authz_keycloak_refresh_token_expires_in(conf, json.refresh_expires_in) + authz_keycloak_cache_set("access_tokens", token_endpoint .. ":" .. conf.client_id, + core.json.encode(session), 24 * 60 * 60) end return session.access_token diff --git a/apisix/plugins/authz-keycloak0.lua b/apisix/plugins/authz-keycloak0.lua index b2090a5df953..25345e4e11d8 100644 --- a/apisix/plugins/authz-keycloak0.lua +++ b/apisix/plugins/authz-keycloak0.lua @@ -208,7 +208,7 @@ local function authz_keycloak_get_endpoint(conf, endpoint) if conf and conf[endpoint] then return conf[endpoint] elseif conf and conf.discovery and type(conf.discovery) == "table" then - return = conf.discovery[endpoint] + return conf.discovery[endpoint] end return nil From a73a702bcafd0a80a45857a59124c00706476b3a Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Thu, 14 Jan 2021 22:00:36 +0100 Subject: [PATCH 19/94] Add lazy_load_paths and http_method_as_scope parameters and implement them. --- apisix/plugins/authz-keycloak.lua | 138 ++++++++++++++++++++++++++---- 1 file changed, 121 insertions(+), 17 deletions(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index 24b3f81a788d..6155fd3232ac 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -28,6 +28,7 @@ local schema = { properties = { discovery = {type = "string", minLength = 1, maxLength = 4096}, token_endpoint = {type = "string", minLength = 1, maxLength = 4096}, + resource_registration_endpoint = {type = "string", minLength = 1, maxLength = 4096}, permissions = { type = "array", items = { @@ -54,10 +55,21 @@ local schema = { ssl_verify = {type = "boolean", default = true}, client_id = {type = "string", minLength = 1, maxLength = 100}, client_secret = {type = "string", minLength = 1, maxLength = 100}, + lazy_load_paths = {type = "boolean", default = false}, + http_method_as_scope = {type = "boolean", default = false}, }, anyOf = { {required = {"discovery"}}, - {required = {"token_endpoint"}}} + {required = {"token_endpoint"}} + }, + dependencies = { + lazy_load_paths = { + anyOf = { + {required = {"discovery"}}, + {required = {"resource_registration_endpoint"}} + } + } + } } @@ -175,7 +187,7 @@ local function authz_keycloak_discover(url, ssl_verify, keepalive, timeout, err = "Accessing discovery url (" .. url .. ") failed: " .. error log.error(err) else - log.debug("response data: " .. res.body) + log.debug("Response data: " .. res.body) json, err = authz_keycloak_parse_json_response(res) if json then authz_keycloak_cache_set("discovery", url, core.json.encode(json), exptime or 24 * 60 * 60) @@ -225,6 +237,11 @@ local function authz_keycloak_get_token_endpoint(conf) end +local function authz_keycloak_get_resource_registration_endpoint(conf) + return authz_keycloak_get_endpoint(conf, "resource_registration_endpoint") +end + + -- computes access_token expires_in value (in seconds) local function authz_keycloak_access_token_expires_in(opts, expires_in) return (expires_in or opts.access_token_expires_in or 300) @@ -250,6 +267,14 @@ local function authz_keycloak_ensure_sa_access_token(conf) local session = authz_keycloak_cache_get("access_tokens", token_endpoint .. ":" .. conf.client_id) if session then + -- Decode session string. + session, err = core.json.decode(session) + + if not session then + -- Should never happen. + return 500, err + end + local current_time = ngx.time() if current_time < session.access_token_expiration then @@ -259,7 +284,7 @@ local function authz_keycloak_ensure_sa_access_token(conf) else -- Access token has expired. log.debug("Access token has expired.") - if session.refresh_token and (not session.refresh_token_expiration or current_time < refresh_token_expiration) then + if session.refresh_token and (not session.refresh_token_expiration or current_time < session.refresh_token_expiration) then -- Try to get a new access token, using the refresh token. log.debug("Trying to get new access token using refresh token.") @@ -271,6 +296,7 @@ local function authz_keycloak_ensure_sa_access_token(conf) body = ngx.encode_args({ grant_type = "refresh_token", client_id = conf.client_id, + client_secret = conf.client_secret, refresh_token = session.refresh_token, }), ssl_verify = conf.ssl_verify, @@ -290,7 +316,6 @@ local function authz_keycloak_ensure_sa_access_token(conf) end log.debug("Response data: " .. res.body) - local json, err = authz_keycloak_parse_json_response(res) if not json then @@ -404,6 +429,54 @@ local function authz_keycloak_ensure_sa_access_token(conf) end +local function authz_keycloak_resolve_permission(conf, uri, scope, sa_access_token) + local httpc = http.new() + httpc:set_timeout(conf.timeout) + + local params = { + method = "GET", + query = {uri = uri, matchingUri = "true"}, + ssl_verify = conf.ssl_verify, + headers = { + ["Authorization"] = "Bearer " .. sa_access_token + } + } + + if conf.keepalive then + params.keepalive_timeout = conf.keepalive_timeout + params.keepalive_pool = conf.keepalive_pool + else + params.keepalive = conf.keepalive + end + + local res, err = httpc:request_uri(resource_registration_endpoint, params) + + if not res then + err = "Accessing resource registration endpoint url (" .. url .. ") failed: " .. error + log.error(err) + return nil, err + end + + log.debug("Response data: " .. res.body) + res.body = '{"resources": ' .. res.body .. '}' + local json, err = authz_keycloak_parse_json_response(res) + + if not json then + err = "Could not decode JSON from resource registration endpoint" .. (err and (": " .. err) or '.') + log.error(err) + return nil, err + end + + local permission = {} + for k, id in pairs(json.resources) do + permission[#permission+1] = id .. (scope and ("#" .. scope) or '') + core.log.error("Adding permission ", permissions[#permission]) + end + + return permission +end + + local function is_path_protected(conf) -- TODO if permissions are empty lazy load paths from Keycloak if conf.permissions == nil then @@ -413,28 +486,59 @@ local function is_path_protected(conf) end -local function evaluate_permissions(conf, token) - if not is_path_protected(conf) and conf.policy_enforcement_mode == "ENFORCING" then - return 403 - end - +local function evaluate_permissions(conf, ctx, token) -- Ensure discovered data. local err = authz_keycloak_ensure_discovered_data(conf) if err then return 500, err end - -- Ensure service account access token. - local sa_access_token, err = authz_keycloak_ensure_sa_access_token(conf) - if err then - return 500, err + local permission + + if lazy_load_paths then + -- Ensure service account access token. + local sa_access_token, err = authz_keycloak_ensure_sa_access_token(conf) + if err then + return 500, err + end + + -- Get resource registration endpoint URL. + local resource_registration_endpoint = authz_keycloak_get_resource_registration_endpoint(conf) + if not resource_registration_endpoint then + err = "Unable to determine registration endpoint." + log.error(err) + return 500, err + end + log.error("Resource registration endpoint: ", resource_registration_endpoint) + + -- Determine scope from HTTP method, maybe. + local scope + if conf.http_method_as_scope then + scope = ctx.var.request_method + end + + permission, err = authz_keycloak_resolve_permission(conf, ctx.var.request_uri, scope, sa_access_token) + + if permission == nil then + return 500, err + end + else + -- Use statically configured permissions. + if not is_path_protected(conf) and conf.policy_enforcement_mode == "ENFORCING" then + return 403 + end + end + + if permission == nil then + return 500, "Unable to determine permission to check." end -- Get token endpoint URL. local token_endpoint = authz_keycloak_get_token_endpoint(conf) if not token_endpoint then - log.error("Unable to determine token endpoint.") - return 500, "Unable to determine token endpoint." + err = "Unable to determine token endpoint." + log.error(err) + return 500, err end log.debug("Token endpoint: ", token_endpoint) @@ -447,7 +551,7 @@ local function evaluate_permissions(conf, token) grant_type = conf.grant_type, audience = conf.client_id, response_mode = "decision", - permission = conf.permissions + permission = permission }), ssl_verify = conf.ssl_verify, headers = { @@ -499,7 +603,7 @@ function _M.access(conf, ctx) return 401, {message = "Missing JWT token in request"} end - local status, body = evaluate_permissions(conf, jwt_token) + local status, body = evaluate_permissions(conf, ctx, jwt_token) if status then return status, body end From b9e9c8006e298f5f7465bac6cddc0db05fae4fe3 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Fri, 15 Jan 2021 09:51:30 +0100 Subject: [PATCH 20/94] Several fixes. --- apisix/plugins/authz-keycloak.lua | 58 ++++++++++++++++++++----------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index 6155fd3232ac..dbf12da0074b 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -429,7 +429,7 @@ local function authz_keycloak_ensure_sa_access_token(conf) end -local function authz_keycloak_resolve_permission(conf, uri, scope, sa_access_token) +local function authz_keycloak_resolve_permission(conf, uri, sa_access_token) local httpc = http.new() httpc:set_timeout(conf.timeout) @@ -467,13 +467,7 @@ local function authz_keycloak_resolve_permission(conf, uri, scope, sa_access_tok return nil, err end - local permission = {} - for k, id in pairs(json.resources) do - permission[#permission+1] = id .. (scope and ("#" .. scope) or '') - core.log.error("Adding permission ", permissions[#permission]) - end - - return permission + return json.resources end @@ -495,7 +489,7 @@ local function evaluate_permissions(conf, ctx, token) local permission - if lazy_load_paths then + if conf.lazy_load_paths then -- Ensure service account access token. local sa_access_token, err = authz_keycloak_ensure_sa_access_token(conf) if err then @@ -511,26 +505,50 @@ local function evaluate_permissions(conf, ctx, token) end log.error("Resource registration endpoint: ", resource_registration_endpoint) - -- Determine scope from HTTP method, maybe. - local scope - if conf.http_method_as_scope then - scope = ctx.var.request_method - end - - permission, err = authz_keycloak_resolve_permission(conf, ctx.var.request_uri, scope, sa_access_token) + -- Resolve URI to resource(s). + permission, err = authz_keycloak_resolve_permission(conf, ctx.var.request_uri, sa_access_token) + -- Check result. if permission == nil then + -- No result back from resource registration endpoint. return 500, err end else -- Use statically configured permissions. - if not is_path_protected(conf) and conf.policy_enforcement_mode == "ENFORCING" then - return 403 + + if conf.permission == nil then + -- No static permission configured. + return 500, "No static permission configured." + end + + permission = conf.permission + end + + -- Return 403 if permission is empty and enforcement mode is "ENFORCING". + if #permission == 0 and conf.policy_enforcement_mode == "ENFORCING" then + return 403 + end + + -- Determine scope from HTTP method, maybe. + local scope + if conf.http_method_as_scope then + scope = ctx.var.request_method + end + + if scope then + -- Loop over permissions and add scope. + for k, v in pairs(permission) do + if v:find("#", 1, true) then + -- Already contains scope. + permission[k] = v .. ", " .. scope + else + -- Doesn't contain scope yet. + permission[k] = v .. "#" .. scope end end - if permission == nil then - return 500, "Unable to determine permission to check." + for k, v in pairs(permission) do + log.debug("Requesting permission ", v, ".") end -- Get token endpoint URL. From 65d491fdd5cb64ce28ee1c3bfa73469b94c987a7 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Fri, 15 Jan 2021 10:19:14 +0100 Subject: [PATCH 21/94] Several fixes. --- apisix/plugins/authz-keycloak.lua | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index dbf12da0074b..76243b6fb766 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -430,6 +430,15 @@ end local function authz_keycloak_resolve_permission(conf, uri, sa_access_token) + -- Get resource registration endpoint URL. + local resource_registration_endpoint = authz_keycloak_get_resource_registration_endpoint(conf) + if not resource_registration_endpoint then + err = "Unable to determine registration endpoint." + log.error(err) + return 500, err + end + log.error("Resource registration endpoint: ", resource_registration_endpoint) + local httpc = http.new() httpc:set_timeout(conf.timeout) @@ -496,15 +505,6 @@ local function evaluate_permissions(conf, ctx, token) return 500, err end - -- Get resource registration endpoint URL. - local resource_registration_endpoint = authz_keycloak_get_resource_registration_endpoint(conf) - if not resource_registration_endpoint then - err = "Unable to determine registration endpoint." - log.error(err) - return 500, err - end - log.error("Resource registration endpoint: ", resource_registration_endpoint) - -- Resolve URI to resource(s). permission, err = authz_keycloak_resolve_permission(conf, ctx.var.request_uri, sa_access_token) @@ -544,6 +544,7 @@ local function evaluate_permissions(conf, ctx, token) else -- Doesn't contain scope yet. permission[k] = v .. "#" .. scope + end end end From be6cde97f43fea0f8db61567dcdde93f128b1ea3 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Fri, 15 Jan 2021 10:51:10 +0100 Subject: [PATCH 22/94] Polishing. --- apisix/plugins/authz-keycloak.lua | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index 76243b6fb766..43228a72c32e 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -586,17 +586,27 @@ local function evaluate_permissions(conf, ctx, token) params.keepalive = conf.keepalive end - local httpc_res, httpc_err = httpc:request_uri(token_endpoint, params) + local res, err = httpc:request_uri(token_endpoint, params) - if not httpc_res then - log.error("error while sending authz request to ", token_endpoint, ": ", httpc_err) - return 500, httpc_err + if not res then + err = "Error while sending authz request to " .. token_endpoint .. ": " .. err + log.error(err) + return 500, err end - if httpc_res.status >= 400 then - log.error("status code: ", httpc_res.status, " msg: ", httpc_res.body) - return httpc_res.status, httpc_res.body + log.debug("Response status: ", res.status, ", data: ", res.body) + + if res.status == 403 then + -- Request denied. + log.debug("Request denied.") + return res.status, res.body + elseif res.status >= 400 then + -- Some other error. Log full response. + log.error("Token endpoint returned an error: status: ", res.status, ", body: ", res.body) + return res.status, res.body end + + -- Request accepted. end From fb4e0ade3a2ed24dafc1cc8940a71987eecd44e5 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Fri, 15 Jan 2021 10:55:30 +0100 Subject: [PATCH 23/94] Return Keycloak-style message when unable to resolve permission. --- apisix/plugins/authz-keycloak.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index 43228a72c32e..175c0d5c3cdc 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -526,7 +526,8 @@ local function evaluate_permissions(conf, ctx, token) -- Return 403 if permission is empty and enforcement mode is "ENFORCING". if #permission == 0 and conf.policy_enforcement_mode == "ENFORCING" then - return 403 + -- Return Keycloak-style message for consistency. + return 403, '{"error":"access_denied","error_description":"not_authorized"}' end -- Determine scope from HTTP method, maybe. From 6a9f12c9fc34c9a078ada1c25f627bec2f0b902d Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Fri, 15 Jan 2021 12:11:28 +0100 Subject: [PATCH 24/94] Update documentation. --- apisix/plugins/authz-keycloak.lua | 3 +- doc/plugins/authz-keycloak.md | 77 +++++++++++++++++++++++-------- 2 files changed, 61 insertions(+), 19 deletions(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index 175c0d5c3cdc..fea8a086e3a2 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -58,6 +58,7 @@ local schema = { lazy_load_paths = {type = "boolean", default = false}, http_method_as_scope = {type = "boolean", default = false}, }, + required = {"client_id"}, anyOf = { {required = {"discovery"}}, {required = {"token_endpoint"}} @@ -67,7 +68,7 @@ local schema = { anyOf = { {required = {"discovery"}}, {required = {"resource_registration_endpoint"}} - } + }, } } } diff --git a/doc/plugins/authz-keycloak.md b/doc/plugins/authz-keycloak.md index 4b6973b72d34..3d511880bf08 100644 --- a/doc/plugins/authz-keycloak.md +++ b/doc/plugins/authz-keycloak.md @@ -38,24 +38,36 @@ For more information on Keycloak, refer to [Keycloak Authorization Docs](https:/ ## Attributes -| Name | Type | Requirement | Default | Valid | Description | -| ----------------------- | ------------- | ----------- | --------------------------------------------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| discovery | string | optional | | https://host.domain/auth/realms/foo/.well-known/uma2-configuration | URL to discovery document for Keycloak Authorization Services. | -| token_endpoint | string | optional | | https://host.domain/auth/realms/foo/protocol/openid-connect/token | A OAuth2-compliant Token Endpoint that supports the `urn:ietf:params:oauth:grant-type:uma-ticket` grant type. Overrides value from discovery, if given. | -| grant_type | string | optional | "urn:ietf:params:oauth:grant-type:uma-ticket" | ["urn:ietf:params:oauth:grant-type:uma-ticket"] | | -| audience | string | optional | | | The client identifier of the resource server to which the client is seeking access.
This parameter is mandatory when parameter permission is defined. | -| permissions | array[string] | optional | | | A string representing a set of one or more resources and scopes the client is seeking access. The format of the string must be: `RESOURCE_ID#SCOPE_ID`. | -| timeout | integer | optional | 3000 | [1000, ...] | Timeout(ms) for the http connection with the Identity Server. | -| ssl_verify | boolean | optional | true | | Verify if SSL cert matches hostname. | -| policy_enforcement_mode | string | optional | "ENFORCING" | ["ENFORCING", "PERMISSIVE"] | | - -### Endpoints - -Endpoints can optionally be discovered by providing a URL pointing to Keycloak's discovery document for Authorization Services for the realm -in the `discovery` attribute. The token endpoint URL will then be determined from that document. Alternatively, the token endpoint can be -specified explicitly via the `token_endpoint` attribute. - -One of `discovery` and `token_endpoint` has to be set. If both are given, the value from `token_endpoint` is used. +| Name | Type | Requirement | Default | Valid | Description | +| ------------------------------ | ------------- | ----------- | --------------------------------------------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | +| discovery | string | optional | | https://host.domain/auth/realms/foo/.well-known/uma2-configuration | URL to discovery document for Keycloak Authorization Services. | +| token_endpoint | string | optional | | https://host.domain/auth/realms/foo/protocol/openid-connect/token | A OAuth2-compliant Token Endpoint that supports the `urn:ietf:params:oauth:grant-type:uma-ticket` grant type. Overrides value from discovery, if given. | +| resource_registration_endpoint | string | optional | | https://host.domain/auth/realms/foo/authz/protection/resource_set | A Keycloak Protection API-compliant resource registration endpoint. Overrides value from discovery, if given. | +| grant_type | string | optional | "urn:ietf:params:oauth:grant-type:uma-ticket" | ["urn:ietf:params:oauth:grant-type:uma-ticket"] | | +| client_id | string | required | | | The client identifier of the resource server to which the client is seeking access.
This parameter is mandatory when parameter permission is defined. | +| client_secret | string | optional | | | The client secret, if required. | +| policy_enforcement_mode | string | optional | "ENFORCING" | ["ENFORCING", "PERMISSIVE"] | | +| permissions | array[string] | optional | | | Static permission to request, an array of strings each representing a resources and optionally one or more scopes the client is seeking access. | +| lazy_load_paths | boolean | optional | false | | Dynamically resolve the request URI to resource(s) using the resource registration endpoint instead of using the static permission. | +| http_method_as_scope | boolean | optional | false | | Map HTTP request type to scope of same name and add to all permissions requested. | +| timeout | integer | optional | 3000 | [1000, ...] | Timeout(ms) for the http connection with the Identity Server. | +| ssl_verify | boolean | optional | true | | Verify if SSL cert matches hostname. | + +### Discovery and Endpoints + +The plugin can discover Keycloak API endpoints from a URL in the `discovery` attribute that points to +Keycloak's discovery document for Authorization Services for the respective realm. This is the recommended +option and typically most convenient. + +If the discovery document is available, the plugin determines the token endpoint URL from it. If present, the +`token_endpoint` attribute overrides the URL. + +Analogously, the plugin determines the registration endpoint from the discovery document. The +`resource_registration_endpoint` overrides, if present. + +### Client ID and Secret +The `client_id` attribute is needed to identify the plugin when interacting with Keycloak. +If the access is confidential, the `client_secret` attribute needs to contain the client secret. ### Policy Enforcement Mode @@ -69,6 +81,35 @@ Specifies how policies are enforced when processing authorization requests sent - Requests are allowed even when there is no policy associated with a given resource. +### Permissions + +When handling an incoming request, the plugin can determine the permissions to check with Keycloak either +statically, or dynamically from properties of the request. + +If `lazy_load_paths` is `false`, the plugin takes the permissions from the `permissions` attribute. Each entry +needs to be formatted as expected by the token endpoint's `permission` parameter; +see https://www.keycloak.org/docs/latest/authorization_services/index.html#_service_obtaining_permissions. +Note that a valid permission can be a single resource, or a resource paired with one or more scopes. + +if `lazy_load_paths` is `true`, the plugin resolves the request URI to one or more resources, as configured +in Keycloak. It uses the resource registration endpoint to do so. The plugin uses the resolved resources +as the permissions to check. + +Note that this requires that the plugin can obtain a separate access token for itself from the token endpoint. +Therefore, in the respective client settings in Keycloak, make sure to set the `Service Accounts Enabled` +option. Also make sure that the issued access token contains the `resource_access` claim with the +`uma_protection` role. Otherwise, plugin may be unable to query resources through the Protection API. + +### Automatic Mapping of HTTP Method to Scope + +This option is often used together with `lazy_load_paths`, but can also be used with a static permission list. + +If the `http_method_as_scope` attribute is set to `true`, the plugin maps the request's HTTP method to a scope +of the same name. The scope is then added to every permission to check. + +If `lazy_load_paths` is `false`, the plugin adds the mapped scope to any of the static permissions configured +in the `permissions` attribute, even if they contain one or more scopes alreay. + ## How To Enable Create a `route` and enable the `authz-keycloak` plugin on the route: From 1ec673ca675f9ec984472b66fdb597dc4cad319c Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Fri, 15 Jan 2021 12:36:10 +0100 Subject: [PATCH 25/94] Remove temporary plugin version. --- apisix/plugins/authz-keycloak0.lua | 405 ----------------------------- 1 file changed, 405 deletions(-) delete mode 100644 apisix/plugins/authz-keycloak0.lua diff --git a/apisix/plugins/authz-keycloak0.lua b/apisix/plugins/authz-keycloak0.lua deleted file mode 100644 index 25345e4e11d8..000000000000 --- a/apisix/plugins/authz-keycloak0.lua +++ /dev/null @@ -1,405 +0,0 @@ --- --- Licensed to the Apache Software Foundation (ASF) under one or more --- contributor license agreements. See the NOTICE file distributed with --- this work for additional information regarding copyright ownership. --- The ASF licenses this file to You under the Apache License, Version 2.0 --- (the "License"); you may not use this file except in compliance with --- the License. You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. --- -local core = require("apisix.core") -local http = require "resty.http" -local sub_str = string.sub -local url = require "net.url" -local tostring = tostring -local ngx = ngx -local cjson = require("cjson") -local cjson_s = require("cjson.safe") - -local plugin_name = "authz-keycloak0" -local log = core.log - - - -local schema = { - type = "object", - properties = { - discovery = {type = "string", minLength = 1, maxLength = 4096}, - token_endpoint = {type = "string", minLength = 1, maxLength = 4096}, - resource_registration_endpoint = {type = "string", minLength = 1, maxLength = 4096}, - permissions = { - type = "array", - items = { - type = "string", - minLength = 1, maxLength = 100 - }, - uniqueItems = true - }, - grant_type = { - type = "string", - default="urn:ietf:params:oauth:grant-type:uma-ticket", - enum = {"urn:ietf:params:oauth:grant-type:uma-ticket"}, - minLength = 1, maxLength = 100 - }, - timeout = {type = "integer", minimum = 1000, default = 3000}, - policy_enforcement_mode = { - type = "string", - enum = {"ENFORCING", "PERMISSIVE"}, - default = "ENFORCING" - }, - keepalive = {type = "boolean", default = true}, - keepalive_timeout = {type = "integer", minimum = 1000, default = 60000}, - keepalive_pool = {type = "integer", minimum = 1, default = 5}, - ssl_verify = {type = "boolean", default = true}, - client_id = {type = "string", minLength = 1, maxLength = 100}, - client_secret = {type = "string", minLength = 1, maxLength = 100}, - } -} - - -local _M = { - version = 0.1, - priority = 2000, - name = plugin_name, - schema = schema, -} - -function _M.check_schema(conf) - return core.schema.check(schema, conf) -end - -local function is_path_protected(conf) - -- TODO if permissions are empty lazy load paths from Keycloak - if conf.permissions == nil then - return false - end - return true -end - --- Retrieve value from server-wide cache, if available. -local function authz_keycloak_cache_get(type, key) - local dict = ngx.shared[type] - local value - if dict then - value = dict:get(key) - if value then log.debug("cache hit: type=", type, " key=", key) end - end - return value -end - --- Set value in server-wide cache, if available. -local function authz_keycloak_cache_set(type, key, value, exp) - local dict = ngx.shared[type] - if dict and (exp > 0) then - local success, err, forcible = dict:set(key, value, exp) - log.debug("cache set: success=", success, " err=", err, " forcible=", forcible) - end -end - -local function authz_keycloak_configure_timeouts(httpc, timeout) - if timeout then - if type(timeout) == "table" then - local r, e = httpc:set_timeouts(timeout.connect or 0, timeout.send or 0, timeout.read or 0) - else - local r, e = httpc:set_timeout(timeout) - end - end -end - --- Set outgoing proxy options. -local function authz_keycloak_configure_proxy(httpc, proxy_opts) - if httpc and proxy_opts and type(proxy_opts) == "table" then - log.debug("authz_keycloak_configure_proxy : use http proxy") - httpc:set_proxy_options(proxy_opts) - else - log.debug("authz_keycloak_configure_proxy : don't use http proxy") - end -end - --- Parse the JSON result from a call to the OP. -local function authz_keycloak_parse_json_response(response, ignore_body_on_success) - local ignore_body_on_success = ignore_body_on_success or false - - local err - local res - - -- Check the response from the OP. - if response.status ~= 200 then - err = "response indicates failure, status=" .. response.status .. ", body=" .. response.body - else - if ignore_body_on_success then - return nil, nil - end - - -- Decode the response and extract the JSON object. - res = cjson_s.decode(response.body) - - if not res then - err = "JSON decoding failed" - end - end - - return res, err -end - -local function decorate_request(http_request_decorator, req) - return http_request_decorator and http_request_decorator(req) or req -end - --- get the Discovery metadata from the specified URL. -local function authz_keycloak_discover(url, ssl_verify, keepalive, timeout, exptime, proxy_opts, http_request_decorator) - log.debug("authz_keycloak_discover: URL is: " .. url) - - local json, err - local v = authz_keycloak_cache_get("discovery", url) - if not v then - - log.debug("Discovery data not in cache, making call to discovery endpoint.") - -- Make the call to the discovery endpoint. - local httpc = http.new() - authz_keycloak_configure_timeouts(httpc, timeout) - authz_keycloak_configure_proxy(httpc, proxy_opts) - local res, error = httpc:request_uri(url, decorate_request(http_request_decorator, { - ssl_verify = (ssl_verify ~= "no"), - keepalive = (keepalive ~= "no") - })) - if not res then - err = "accessing discovery url (" .. url .. ") failed: " .. error - log.error(err) - else - log.debug("response data: " .. res.body) - json, err = authz_keycloak_parse_json_response(res) - if json then - authz_keycloak_cache_set("discovery", url, cjson.encode(json), exptime or 24 * 60 * 60) - else - err = "could not decode JSON from Discovery data" .. (err and (": " .. err) or '') - log.error(err) - end - end - - else - json = cjson.decode(v) - end - - return json, err -end - --- Turn a discovery url set in the opts dictionary into the discovered information. -local function authz_keycloak_ensure_discovered_data(opts) - local err - if type(opts.discovery) == "string" then - local discovery - discovery, err = authz_keycloak_discover(opts.discovery, opts.ssl_verify, opts.keepalive, opts.timeout, opts.jwk_expires_in, opts.proxy_opts, opts.http_request_decorator) - if not err then - opts.discovery = discovery - end - end - return err -end - -local function authz_keycloak_get_endpoint(conf, endpoint) - if conf and conf[endpoint] then - return conf[endpoint] - elseif conf and conf.discovery and type(conf.discovery) == "table" then - return conf.discovery[endpoint] - end - - return nil -end - -local function authz_keycloak_get_token_endpoint(conf) - return authz_keycloak_get_endpoint(conf, "token_endpoint") -end - -local function authz_keycloak_get_resource_registration_endpoint(conf) - return authz_keycloak_get_endpoint(conf, "resource_registration_endpoint") -end - -local function evaluate_permissions(conf, token, uri, ctx) - if not is_path_protected(conf) and conf.policy_enforcement_mode == "ENFORCING" then - return 403 - end - - -- Ensure discovered data. - local err = authz_keycloak_ensure_discovered_data(conf) - if err then - return nil, err - end - - -- Get token endpoint URL. - local token_endpoint = authz_keycloak_get_token_endpoint(conf) - if not token_endpoint then - log.error("No token endpoint supplied.") - return 500, "No token endpoint supplied." - end - log.debug("Token endpoint: ", token_endpoint) - - -- Get access token for Protection API. - core.log.error("Getting access token for Protection API from token endpoint.") - local httpc = http.new() - httpc:set_timeout(conf.timeout) - - local params = { - method = "POST", - body = ngx.encode_args({ - grant_type = "client_credentials", - client_id = conf.client_id, - client_secret = conf.client_secret, - }), - ssl_verify = conf.ssl_verify, - headers = { - ["Content-Type"] = "application/x-www-form-urlencoded" - } - } - - if conf.keepalive then - params.keepalive_timeout = conf.keepalive_timeout - params.keepalive_pool = conf.keepalive_pool - else - params.keepalive = conf.keepalive - end - - core.log.error("Sending request to token endpoint to obtain access token.") - local httpc_res, httpc_err = httpc:request_uri(token_endpoint, params) - core.log.error("Response body: ", httpc_res.body) - local json = cjson_s.decode(httpc_res.body) - core.log.error("Access token: ", json.access_token) - core.log.error("Expires in: ", json.expires_in) - core.log.error("Refresh token: ", json.refresh_token) - core.log.error("Refresh expires in: ", json.refresh_expires_in) - - -- Get resource registration endpoint URL. - local resource_registration_endpoint = authz_keycloak_get_resource_registration_endpoint(conf) - if not resource_registration_endpoint then - log.error("No resource registration endpoint supplied.") - return 500, "No resource registration endpoint supplied." - end - log.error("Resource registration endpoint: ", resource_registration_endpoint) - - -- Get ID of resource trying to access. - core.log.error("Request URI: ", uri) - local httpc = http.new() - httpc:set_timeout(conf.timeout) - - local params = { - method = "GET", - query = {uri = uri, matchingUri = "true"}, - ssl_verify = conf.ssl_verify, - headers = { - ["Authorization"] = "Bearer " .. json.access_token - } - } - - if conf.keepalive then - params.keepalive_timeout = conf.keepalive_timeout - params.keepalive_pool = conf.keepalive_pool - else - params.keepalive = conf.keepalive - end - - core.log.error("Sending request to token endpoint to obtain access token.") - local httpc_res, httpc_err = httpc:request_uri(resource_registration_endpoint, params) - core.log.error("Response body: ", httpc_res.body) - local json = cjson_s.decode('{"ids": ' .. httpc_res.body .. '}') - for k, id in pairs(json.ids) do - core.log.error("Matched resource: ", id) - end - - -- Determine scope. - local scope = ctx.var.request_method - - local permissions = {} - for k, id in pairs(json.ids) do - permissions[#permissions+1] = id .. "#" .. scope - core.log.error("Requested permission: ", permissions[#permissions]) - end - - - local httpc = http.new() - httpc:set_timeout(conf.timeout) - - local params = { - method = "POST", - body = ngx.encode_args({ - grant_type = conf.grant_type, - audience = conf.client_id, - response_mode = "decision", - permission = permissions - }), - ssl_verify = conf.ssl_verify, - headers = { - ["Content-Type"] = "application/x-www-form-urlencoded", - ["Authorization"] = token - } - } - - if conf.keepalive then - params.keepalive_timeout = conf.keepalive_timeout - params.keepalive_pool = conf.keepalive_pool - else - params.keepalive = conf.keepalive - end - - local httpc_res, httpc_err = httpc:request_uri(token_endpoint, params) - - if not httpc_res then - local url_decoded = url.parse(conf.token_endpoint) - local host = url_decoded.host - local port = url_decoded.port - - if not port then - if url_decoded.scheme == "https" then - port = 443 - else - port = 80 - end - end - core.log.error("error while sending authz request to [", host ,"] port[", - tostring(port), "] ", httpc_err) - return 500, httpc_err - end - - if httpc_res.status >= 400 then - core.log.error("status code: ", httpc_res.status, " msg: ", httpc_res.body) - return httpc_res.status, httpc_res.body - end -end - - -local function fetch_jwt_token(ctx) - local token = core.request.header(ctx, "Authorization") - if not token then - return nil, "authorization header not available" - end - - local prefix = sub_str(token, 1, 7) - if prefix ~= 'Bearer ' and prefix ~= 'bearer ' then - return "Bearer " .. token - end - return token -end - - -function _M.access(conf, ctx) - core.log.error("hit keycloak-auth access0") - local jwt_token, err = fetch_jwt_token(ctx) - if not jwt_token then - core.log.error("failed to fetch JWT token: ", err) - return 401, {message = "Missing JWT token in request"} - end - - local status, body = evaluate_permissions(conf, jwt_token, ctx.var.request_uri, ctx) - if status then - return status, body - end -end - - -return _M From 6bcf69ecf3a8c2faa2fed356a143a16900080f6d Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Fri, 15 Jan 2021 12:51:13 +0100 Subject: [PATCH 26/94] Breake some long lines. --- apisix/plugins/authz-keycloak.lua | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index fea8a086e3a2..7b21944f36fd 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -265,7 +265,8 @@ local function authz_keycloak_ensure_sa_access_token(conf) return 500, "Unable to determine token endpoint." end - local session = authz_keycloak_cache_get("access_tokens", token_endpoint .. ":" .. conf.client_id) + local session = authz_keycloak_cache_get("access_tokens", token_endpoint .. ":" + .. conf.client_id) if session then -- Decode session string. @@ -285,7 +286,9 @@ local function authz_keycloak_ensure_sa_access_token(conf) else -- Access token has expired. log.debug("Access token has expired.") - if session.refresh_token and (not session.refresh_token_expiration or current_time < session.refresh_token_expiration) then + if session.refresh_token + and (not session.refresh_token_expiration + or current_time < session.refresh_token_expiration) then -- Try to get a new access token, using the refresh token. log.debug("Trying to get new access token using refresh token.") @@ -320,7 +323,8 @@ local function authz_keycloak_ensure_sa_access_token(conf) local json, err = authz_keycloak_parse_json_response(res) if not json then - err = "Could not decode JSON from token endpoint" .. (err and (": " .. err) or '.') + err = "Could not decode JSON from token endpoint" + .. (err and (": " .. err) or '.') log.error(err) return nil, err end @@ -345,11 +349,13 @@ local function authz_keycloak_ensure_sa_access_token(conf) -- Calculate and save refresh token expiry time. session.refresh_token_expiration = current_time - + authz_keycloak_refresh_token_expires_in(conf, json.refresh_expires_in) + + authz_keycloak_refresh_token_expires_in(conf, + json.refresh_expires_in) end - authz_keycloak_cache_set("access_tokens", token_endpoint .. ":" .. conf.client_id, - core.json.encode(session), 24 * 60 * 60) + authz_keycloak_cache_set("access_tokens", + token_endpoint .. ":" .. conf.client_id, + core.json.encode(session), 24 * 60 * 60) end else -- No refresh token available, or it has expired. Clear session. @@ -472,7 +478,8 @@ local function authz_keycloak_resolve_permission(conf, uri, sa_access_token) local json, err = authz_keycloak_parse_json_response(res) if not json then - err = "Could not decode JSON from resource registration endpoint" .. (err and (": " .. err) or '.') + err = "Could not decode JSON from resource registration endpoint" + .. (err and (": " .. err) or '.') log.error(err) return nil, err end @@ -507,7 +514,8 @@ local function evaluate_permissions(conf, ctx, token) end -- Resolve URI to resource(s). - permission, err = authz_keycloak_resolve_permission(conf, ctx.var.request_uri, sa_access_token) + permission, err = authz_keycloak_resolve_permission(conf, ctx.var.request_uri, + sa_access_token) -- Check result. if permission == nil then From be03eebcbfe3f5c824ed8f68d3ca44884a748dc4 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Fri, 15 Jan 2021 12:52:56 +0100 Subject: [PATCH 27/94] Break some long lines and general polishing. --- apisix/plugins/authz-keycloak.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index 7b21944f36fd..076536f18c0b 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -439,11 +439,13 @@ end local function authz_keycloak_resolve_permission(conf, uri, sa_access_token) -- Get resource registration endpoint URL. local resource_registration_endpoint = authz_keycloak_get_resource_registration_endpoint(conf) + if not resource_registration_endpoint then err = "Unable to determine registration endpoint." log.error(err) return 500, err end + log.error("Resource registration endpoint: ", resource_registration_endpoint) local httpc = http.new() From 38617bbeee9119a636e3d10551c69d4ab6095aba Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Fri, 15 Jan 2021 13:37:51 +0100 Subject: [PATCH 28/94] Fix linting error. --- doc/plugins/authz-keycloak.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/plugins/authz-keycloak.md b/doc/plugins/authz-keycloak.md index 3d511880bf08..9cf149700e40 100644 --- a/doc/plugins/authz-keycloak.md +++ b/doc/plugins/authz-keycloak.md @@ -66,6 +66,7 @@ Analogously, the plugin determines the registration endpoint from the discovery `resource_registration_endpoint` overrides, if present. ### Client ID and Secret + The `client_id` attribute is needed to identify the plugin when interacting with Keycloak. If the access is confidential, the `client_secret` attribute needs to contain the client secret. From a92df4b7c4cf5c081212a27290e9a074cfca6df8 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Fri, 15 Jan 2021 13:43:12 +0100 Subject: [PATCH 29/94] Fix linting errors. --- apisix/plugins/authz-keycloak.lua | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index 076536f18c0b..efb8d376f12c 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -270,7 +270,7 @@ local function authz_keycloak_ensure_sa_access_token(conf) if session then -- Decode session string. - session, err = core.json.decode(session) + local session, err = core.json.decode(session) if not session then -- Should never happen. @@ -314,7 +314,8 @@ local function authz_keycloak_ensure_sa_access_token(conf) local res, err = httpc:request_uri(token_endpoint, params) if not res then - err = "Accessing token endpoint url (" .. url .. ") failed: " .. error + err = "Accessing token endpoint URL (" .. token_endpoint + .. ") failed: " .. err log.error(err) return nil, err end @@ -390,7 +391,7 @@ local function authz_keycloak_ensure_sa_access_token(conf) local res, err = httpc:request_uri(token_endpoint, params) if not res then - err = "Accessing token endpoint url (" .. url .. ") failed: " .. error + err = "Accessing token endpoint URL (" .. token_endpoint .. ") failed: " .. err log.error(err) return nil, err end @@ -441,11 +442,11 @@ local function authz_keycloak_resolve_permission(conf, uri, sa_access_token) local resource_registration_endpoint = authz_keycloak_get_resource_registration_endpoint(conf) if not resource_registration_endpoint then - err = "Unable to determine registration endpoint." + local err = "Unable to determine registration endpoint." log.error(err) return 500, err end - + log.error("Resource registration endpoint: ", resource_registration_endpoint) local httpc = http.new() @@ -470,7 +471,8 @@ local function authz_keycloak_resolve_permission(conf, uri, sa_access_token) local res, err = httpc:request_uri(resource_registration_endpoint, params) if not res then - err = "Accessing resource registration endpoint url (" .. url .. ") failed: " .. error + err = "Accessing resource registration endpoint URL (" .. resource_registration_endpoint + .. ") failed: " .. error log.error(err) return nil, err end @@ -490,15 +492,6 @@ local function authz_keycloak_resolve_permission(conf, uri, sa_access_token) end -local function is_path_protected(conf) - -- TODO if permissions are empty lazy load paths from Keycloak - if conf.permissions == nil then - return false - end - return true -end - - local function evaluate_permissions(conf, ctx, token) -- Ensure discovered data. local err = authz_keycloak_ensure_discovered_data(conf) From 2d1ef83997fdb3c29d7938cf5421270bea298f36 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Fri, 15 Jan 2021 13:59:23 +0100 Subject: [PATCH 30/94] Fix inting errors. --- apisix/plugins/authz-keycloak.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index efb8d376f12c..4505511be338 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -270,7 +270,8 @@ local function authz_keycloak_ensure_sa_access_token(conf) if session then -- Decode session string. - local session, err = core.json.decode(session) + local err + session, err = core.json.decode(session) if not session then -- Should never happen. @@ -472,7 +473,7 @@ local function authz_keycloak_resolve_permission(conf, uri, sa_access_token) if not res then err = "Accessing resource registration endpoint URL (" .. resource_registration_endpoint - .. ") failed: " .. error + .. ") failed: " .. err log.error(err) return nil, err end From 63cde92559b9b15ba7f67a6fb2ddbc036a22b2ca Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Fri, 15 Jan 2021 14:31:42 +0100 Subject: [PATCH 31/94] Fix linting error. --- apisix/plugins/authz-keycloak.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index 4505511be338..939cd62a232a 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -22,6 +22,7 @@ local ngx = ngx local plugin_name = "authz-keycloak" local log = core.log +local pairs = pairs local schema = { type = "object", From 6698f99a9a7c4a4ad96e7d3ff48b06a80728470b Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Mon, 18 Jan 2021 09:36:19 +0100 Subject: [PATCH 32/94] Add back deprecated audience attribute. --- t/plugin/authz-keycloak.t | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/t/plugin/authz-keycloak.t b/t/plugin/authz-keycloak.t index 06b613cf5f71..a95517737746 100644 --- a/t/plugin/authz-keycloak.t +++ b/t/plugin/authz-keycloak.t @@ -30,6 +30,7 @@ __DATA__ content_by_lua_block { local plugin = require("apisix.plugins.authz-keycloak") local ok, err = plugin.check_schema({ + client_id = "foo", token_endpoint = "https://host.domain/auth/realms/foo/protocol/openid-connect/token", grant_type = "urn:ietf:params:oauth:grant-type:uma-ticket" }) @@ -55,6 +56,7 @@ done content_by_lua_block { local plugin = require("apisix.plugins.authz-keycloak") local ok, err = plugin.check_schema({ + client_id = "foo", discovery = "https://host.domain/auth/realms/foo/.well-known/uma2-configuration", grant_type = "urn:ietf:params:oauth:grant-type:uma-ticket" }) @@ -83,7 +85,7 @@ done token_endpoint = "https://host.domain/auth/realms/foo/protocol/openid-connect/token", permissions = {"res:customer#scopes:view"}, timeout = 1000, - audience = "University", + client_id = "University", grant_type = "urn:ietf:params:oauth:grant-type:uma-ticket" }) if not ok then @@ -107,7 +109,7 @@ done location /t { content_by_lua_block { local plugin = require("apisix.plugins.authz-keycloak") - local ok, err = plugin.check_schema({permissions = {"res:customer#scopes:view"}}) + local ok, err = plugin.check_schema({client_id = "University", permissions = {"res:customer#scopes:view"}}) if not ok then ngx.say(err) end @@ -137,7 +139,7 @@ done "authz-keycloak": { "token_endpoint": "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token", "permissions": ["course_resource#view"], - "audience": "course_management", + "client_id": "course_management", "grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket", "timeout": 3000 } @@ -157,7 +159,7 @@ done "authz-keycloak": { "token_endpoint": "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token", "permissions": ["course_resource#view"], - "audience": "course_management", + "client_id": "course_management", "grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket", "timeout": 3000 } @@ -278,7 +280,7 @@ Invalid bearer token "authz-keycloak": { "discovery": "http://127.0.0.1:8090/auth/realms/University/.well-known/uma2-configuration", "permissions": ["course_resource#view"], - "audience": "course_management", + "client_id": "course_management", "grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket", "timeout": 3000 } @@ -298,7 +300,7 @@ Invalid bearer token "authz-keycloak": { "discovery": "http://127.0.0.1:8090/auth/realms/University/.well-known/uma2-configuration", "permissions": ["course_resource#view"], - "audience": "course_management", + "client_id": "course_management", "grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket", "timeout": 3000 } @@ -419,7 +421,7 @@ Invalid bearer token "authz-keycloak": { "token_endpoint": "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token", "permissions": ["course_resource#delete"], - "audience": "course_management", + "client_id": "course_management", "grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket", "timeout": 3000 } @@ -439,7 +441,7 @@ Invalid bearer token "authz-keycloak": { "token_endpoint": "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token", "permissions": ["course_resource#delete"], - "audience": "course_management", + "client_id": "course_management", "grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket", "timeout": 3000 } @@ -533,7 +535,7 @@ true "authz-keycloak": { "token_endpoint": "https://127.0.0.1:8443/auth/realms/University/protocol/openid-connect/token", "permissions": ["course_resource#delete"], - "audience": "course_management", + "client_id": "course_management", "grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket", "timeout": 3000 } @@ -553,7 +555,7 @@ true "authz-keycloak": { "token_endpoint": "https://127.0.0.1:8443/auth/realms/University/protocol/openid-connect/token", "permissions": ["course_resource#delete"], - "audience": "course_management", + "client_id": "course_management", "grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket", "timeout": 3000 } @@ -629,7 +631,7 @@ error while sending authz request to https://127.0.0.1:8443/auth/realms/Universi "authz-keycloak": { "token_endpoint": "https://127.0.0.1:8443/auth/realms/University/protocol/openid-connect/token", "permissions": ["course_resource#delete"], - "audience": "course_management", + "client_id": "course_management", "grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket", "timeout": 3000, "ssl_verify": false @@ -650,7 +652,7 @@ error while sending authz request to https://127.0.0.1:8443/auth/realms/Universi "authz-keycloak": { "token_endpoint": "https://127.0.0.1:8443/auth/realms/University/protocol/openid-connect/token", "permissions": ["course_resource#delete"], - "audience": "course_management", + "client_id": "course_management", "grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket", "timeout": 3000, "ssl_verify": false From 7b7c5a9a7314fac14a46b520c2a2e507ccf9bd95 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Mon, 18 Jan 2021 09:37:02 +0100 Subject: [PATCH 33/94] Replace audience with client_id and add where necessary. --- apisix/plugins/authz-keycloak.lua | 38 +++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index 939cd62a232a..c96d9166955e 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -55,11 +55,16 @@ local schema = { keepalive_pool = {type = "integer", minimum = 1, default = 5}, ssl_verify = {type = "boolean", default = true}, client_id = {type = "string", minLength = 1, maxLength = 100}, + audience = {type = "string", minLength = 1, maxLength = 100, + description = "Deprecated, use `client_id` instead."}, client_secret = {type = "string", minLength = 1, maxLength = 100}, lazy_load_paths = {type = "boolean", default = false}, http_method_as_scope = {type = "boolean", default = false}, }, - required = {"client_id"}, + anyOf { + {required = {"client_id"}}, + {required = {"audience"}} + }, anyOf = { {required = {"discovery"}}, {required = {"token_endpoint"}} @@ -84,10 +89,28 @@ local _M = { function _M.check_schema(conf) + -- Check for deprecated audience attribute and emit warnings if used. + if conf.audience then + log.warn("Plugin attribute `audience` is deprecated, use `client_id` instead.") + if conf.client_id then + log.warn("Ignoring `audience` attribute in favor of `client_id`.") + end + end return core.schema.check(schema, conf) end +-- Return the configured client ID parameter. +local function authz_keycloak_get_client_id(conf) + if conf.client_id then + -- Prefer client_id, if given. + return client_id + end + + return conf.audience +end + + -- Some auxiliary functions below heavily inspired by the excellent -- lua-resty-openidc module; see https://github.com/zmartzone/lua-resty-openidc @@ -259,6 +282,7 @@ end local function authz_keycloak_ensure_sa_access_token(conf) + local client_id = authz_keycloak_get_client_id(conf) local token_endpoint = authz_keycloak_get_token_endpoint(conf) if not token_endpoint then @@ -267,7 +291,7 @@ local function authz_keycloak_ensure_sa_access_token(conf) end local session = authz_keycloak_cache_get("access_tokens", token_endpoint .. ":" - .. conf.client_id) + .. client_id) if session then -- Decode session string. @@ -301,7 +325,7 @@ local function authz_keycloak_ensure_sa_access_token(conf) method = "POST", body = ngx.encode_args({ grant_type = "refresh_token", - client_id = conf.client_id, + client_id = client_id, client_secret = conf.client_secret, refresh_token = session.refresh_token, }), @@ -357,7 +381,7 @@ local function authz_keycloak_ensure_sa_access_token(conf) end authz_keycloak_cache_set("access_tokens", - token_endpoint .. ":" .. conf.client_id, + token_endpoint .. ":" .. client_id, core.json.encode(session), 24 * 60 * 60) end else @@ -379,7 +403,7 @@ local function authz_keycloak_ensure_sa_access_token(conf) method = "POST", body = ngx.encode_args({ grant_type = "client_credentials", - client_id = conf.client_id, + client_id = client_id, client_secret = conf.client_secret, }), ssl_verify = conf.ssl_verify, @@ -431,7 +455,7 @@ local function authz_keycloak_ensure_sa_access_token(conf) + authz_keycloak_refresh_token_expires_in(conf, json.refresh_expires_in) end - authz_keycloak_cache_set("access_tokens", token_endpoint .. ":" .. conf.client_id, + authz_keycloak_cache_set("access_tokens", token_endpoint .. ":" .. client_id, core.json.encode(session), 24 * 60 * 60) end @@ -575,7 +599,7 @@ local function evaluate_permissions(conf, ctx, token) method = "POST", body = ngx.encode_args({ grant_type = conf.grant_type, - audience = conf.client_id, + audience = authz_keycloak_get_client_id(conf), response_mode = "decision", permission = permission }), From 5645de21a8614e6979a81307c28a8bc12e520de5 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Mon, 18 Jan 2021 09:45:50 +0100 Subject: [PATCH 34/94] Fix syntax error. --- apisix/plugins/authz-keycloak.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index c96d9166955e..9dbbb037e77e 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -61,7 +61,7 @@ local schema = { lazy_load_paths = {type = "boolean", default = false}, http_method_as_scope = {type = "boolean", default = false}, }, - anyOf { + anyOf = { {required = {"client_id"}}, {required = {"audience"}} }, From 194425b201e95faf918ced2a3b23eaf2cf9f0b1d Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Mon, 18 Jan 2021 09:51:52 +0100 Subject: [PATCH 35/94] Make cache ttl configurable. --- apisix/plugins/authz-keycloak.lua | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index 9dbbb037e77e..7ea4583801a0 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -60,6 +60,7 @@ local schema = { client_secret = {type = "string", minLength = 1, maxLength = 100}, lazy_load_paths = {type = "boolean", default = false}, http_method_as_scope = {type = "boolean", default = false}, + cache_ttl_seconds = {type = "integer", minimum = 1, default = 24 * 60 * 60}, }, anyOf = { {required = {"client_id"}}, @@ -215,7 +216,7 @@ local function authz_keycloak_discover(url, ssl_verify, keepalive, timeout, log.debug("Response data: " .. res.body) json, err = authz_keycloak_parse_json_response(res) if json then - authz_keycloak_cache_set("discovery", url, core.json.encode(json), exptime or 24 * 60 * 60) + authz_keycloak_cache_set("discovery", url, core.json.encode(json), exptime) else err = "could not decode JSON from Discovery data" .. (err and (": " .. err) or '') log.error(err) @@ -236,8 +237,8 @@ local function authz_keycloak_ensure_discovered_data(opts) if type(opts.discovery) == "string" then local discovery discovery, err = authz_keycloak_discover(opts.discovery, opts.ssl_verify, opts.keepalive, - opts.timeout, opts.jwk_expires_in, opts.proxy_opts, - opts.http_request_decorator) + opts.timeout, opts.cache_ttl_seconds, + opts.proxy_opts, opts.http_request_decorator) if not err then opts.discovery = discovery end @@ -283,6 +284,7 @@ end local function authz_keycloak_ensure_sa_access_token(conf) local client_id = authz_keycloak_get_client_id(conf) + local ttl = conf.cache_ttl_seconds local token_endpoint = authz_keycloak_get_token_endpoint(conf) if not token_endpoint then @@ -382,7 +384,7 @@ local function authz_keycloak_ensure_sa_access_token(conf) authz_keycloak_cache_set("access_tokens", token_endpoint .. ":" .. client_id, - core.json.encode(session), 24 * 60 * 60) + core.json.encode(session), ttl) end else -- No refresh token available, or it has expired. Clear session. @@ -456,7 +458,7 @@ local function authz_keycloak_ensure_sa_access_token(conf) end authz_keycloak_cache_set("access_tokens", token_endpoint .. ":" .. client_id, - core.json.encode(session), 24 * 60 * 60) + core.json.encode(session), ttl) end return session.access_token From 536267fd631cc64766a70f839f461815b2df324c Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Mon, 18 Jan 2021 10:24:42 +0100 Subject: [PATCH 36/94] Remove duplicate call to ngx.time(). --- apisix/plugins/authz-keycloak.lua | 2 -- 1 file changed, 2 deletions(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index 7ea4583801a0..426174c23939 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -337,8 +337,6 @@ local function authz_keycloak_ensure_sa_access_token(conf) } } - local current_time = ngx.time() - local res, err = httpc:request_uri(token_endpoint, params) if not res then From ef7bc823954ac4696b37aaf262244001ea8540e7 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Mon, 18 Jan 2021 10:25:14 +0100 Subject: [PATCH 37/94] Move shared cache definition. --- apisix/cli/ngx_tpl.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apisix/cli/ngx_tpl.lua b/apisix/cli/ngx_tpl.lua index 9af2515cd914..d512211c9199 100644 --- a/apisix/cli/ngx_tpl.lua +++ b/apisix/cli/ngx_tpl.lua @@ -132,6 +132,7 @@ http { lua_shared_dict worker-events 10m; lua_shared_dict lrucache-lock 10m; lua_shared_dict skywalking-tracing-buffer 100m; + lua_shared_dict access_tokens 1m; # for authz-keycloak: cache for service account access tokens lua_shared_dict balancer_ewma 10m; lua_shared_dict balancer_ewma_locks 10m; lua_shared_dict balancer_ewma_last_touched_at 10m; @@ -146,8 +147,6 @@ http { lua_shared_dict jwks 1m; # cache for JWKs lua_shared_dict introspection 10m; # cache for JWT verification results - # for authz-keycloak - lua_shared_dict access_tokens 1m; # cache for service account access tokens # for custom shared dict {% if http.lua_shared_dicts then %} From 3503df746a1ff2a9fb08e14b46d5e44b13345644 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Mon, 18 Jan 2021 10:25:58 +0100 Subject: [PATCH 38/94] Don't require client_id or audience. --- apisix/plugins/authz-keycloak.lua | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index 426174c23939..f81b7d66a128 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -62,10 +62,6 @@ local schema = { http_method_as_scope = {type = "boolean", default = false}, cache_ttl_seconds = {type = "integer", minimum = 1, default = 24 * 60 * 60}, }, - anyOf = { - {required = {"client_id"}}, - {required = {"audience"}} - }, anyOf = { {required = {"discovery"}}, {required = {"token_endpoint"}} From 71bb50a47986950fbddc9e7c5b1e81e12aae5487 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Mon, 18 Jan 2021 10:26:53 +0100 Subject: [PATCH 39/94] Fix undefined variable reference. --- apisix/plugins/authz-keycloak.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index f81b7d66a128..1db8b27ce44f 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -101,7 +101,7 @@ end local function authz_keycloak_get_client_id(conf) if conf.client_id then -- Prefer client_id, if given. - return client_id + return conf.client_id end return conf.audience From 84e7a7f96f898d53b9ea4202ff5dffe0848e8231 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Mon, 18 Jan 2021 11:17:25 +0100 Subject: [PATCH 40/94] Fix test for 401 Unauthorized case. --- apisix/plugins/authz-keycloak.lua | 10 +++++++--- t/plugin/authz-keycloak.t | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index 1db8b27ce44f..ff235b43b786 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -624,12 +624,16 @@ local function evaluate_permissions(conf, ctx, token) log.debug("Response status: ", res.status, ", data: ", res.body) if res.status == 403 then - -- Request denied. - log.debug("Request denied.") + -- Request permanently denied, e.g. due to lacking permissions. + log.debug('Request denied: HTTP 403 Forbidden. Body: ', res.body) + return res.status, res.body + elseif res.status == 401 then + -- Request temporarily denied, e.g access token not valid. + log.debug('Request denied: HTTP 401 Unauthorized. Body: ', res.body) return res.status, res.body elseif res.status >= 400 then -- Some other error. Log full response. - log.error("Token endpoint returned an error: status: ", res.status, ", body: ", res.body) + log.error('Request denied: Token endpoint returned an error (status: ', res.status, ', body: ', res.body, ').') return res.status, res.body end diff --git a/t/plugin/authz-keycloak.t b/t/plugin/authz-keycloak.t index a95517737746..67413d1a699f 100644 --- a/t/plugin/authz-keycloak.t +++ b/t/plugin/authz-keycloak.t @@ -713,4 +713,4 @@ GET /t --- response_body false --- error_log -status code: 401 msg: {"error":"HTTP 401 Unauthorized"} +Request denied: HTTP 401 Unauthorized. Body: {"error":"HTTP 401 Unauthorized"} From f423e40353e5560b12d3eed8caff49859e83df35 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Mon, 18 Jan 2021 11:38:01 +0100 Subject: [PATCH 41/94] Fix too long line. --- apisix/plugins/authz-keycloak.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index ff235b43b786..bd85863f58f7 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -633,7 +633,8 @@ local function evaluate_permissions(conf, ctx, token) return res.status, res.body elseif res.status >= 400 then -- Some other error. Log full response. - log.error('Request denied: Token endpoint returned an error (status: ', res.status, ', body: ', res.body, ').') + log.error('Request denied: Token endpoint returned an error (status: ', + res.status, ', body: ', res.body, ').') return res.status, res.body end From 80fd8215ab084da00c9b5a42aab561ac44c81ea7 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Mon, 18 Jan 2021 12:30:43 +0100 Subject: [PATCH 42/94] Fix JSON schema. --- apisix/plugins/authz-keycloak.lua | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index bd85863f58f7..8a946812caa0 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -62,16 +62,30 @@ local schema = { http_method_as_scope = {type = "boolean", default = false}, cache_ttl_seconds = {type = "integer", minimum = 1, default = 24 * 60 * 60}, }, - anyOf = { - {required = {"discovery"}}, - {required = {"token_endpoint"}} - }, - dependencies = { - lazy_load_paths = { + allOf = { + { anyOf = { {required = {"discovery"}}, - {required = {"resource_registration_endpoint"}} - }, + {required = {"token_endpoint"}} + } + }, + { + anyOf = { + { + not = { + properties: { + lazy_load_paths: {const: true}, + required: {"lazy_load_paths"} + } + }, + }, + { + anyOf = { + {required = {"discovery"}}, + {required = {"resource_registration_endpoint"}} + } + } + } } } } From cbae8b898f01bdd7b9620a04c99564982d1ea6d8 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Mon, 18 Jan 2021 12:35:21 +0100 Subject: [PATCH 43/94] Revert previous change. --- apisix/cli/ngx_tpl.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apisix/cli/ngx_tpl.lua b/apisix/cli/ngx_tpl.lua index d512211c9199..9af2515cd914 100644 --- a/apisix/cli/ngx_tpl.lua +++ b/apisix/cli/ngx_tpl.lua @@ -132,7 +132,6 @@ http { lua_shared_dict worker-events 10m; lua_shared_dict lrucache-lock 10m; lua_shared_dict skywalking-tracing-buffer 100m; - lua_shared_dict access_tokens 1m; # for authz-keycloak: cache for service account access tokens lua_shared_dict balancer_ewma 10m; lua_shared_dict balancer_ewma_locks 10m; lua_shared_dict balancer_ewma_last_touched_at 10m; @@ -147,6 +146,8 @@ http { lua_shared_dict jwks 1m; # cache for JWKs lua_shared_dict introspection 10m; # cache for JWT verification results + # for authz-keycloak + lua_shared_dict access_tokens 1m; # cache for service account access tokens # for custom shared dict {% if http.lua_shared_dicts then %} From bf6d5f5e9279d8c2ed9d08d7ab20ab8f85f4e629 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Mon, 18 Jan 2021 12:35:41 +0100 Subject: [PATCH 44/94] Add shared dictionary for authz-keycloak plugin. --- t/APISIX.pm | 1 + 1 file changed, 1 insertion(+) diff --git a/t/APISIX.pm b/t/APISIX.pm index 2634eea45b2a..95d0119eabc4 100644 --- a/t/APISIX.pm +++ b/t/APISIX.pm @@ -251,6 +251,7 @@ _EOC_ lua_shared_dict balancer_ewma_last_touched_at 1m; lua_shared_dict plugin-limit-count-redis-cluster-slot-lock 1m; lua_shared_dict tracing_buffer 10m; # plugin skywalking + lua_shared_dict access_tokens 1m; # plugin authz-keycloak lua_shared_dict plugin-api-breaker 10m; lua_capture_error_log 1m; # plugin error-log-logger From f3773f6f4b3caa3f4cf2725de3450024b5bec7fb Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Mon, 18 Jan 2021 12:43:20 +0100 Subject: [PATCH 45/94] Fix JSON schema syntax error. --- apisix/plugins/authz-keycloak.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index 8a946812caa0..7579f9b580cf 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -74,7 +74,7 @@ local schema = { { not = { properties: { - lazy_load_paths: {const: true}, + lazy_load_paths: {const = true}, required: {"lazy_load_paths"} } }, From 0ae4713e12bcd237e2fb96da65d2eb59a08e9cad Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Mon, 18 Jan 2021 12:46:17 +0100 Subject: [PATCH 46/94] Fix and simplify JSON schema. --- apisix/plugins/authz-keycloak.lua | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index 7579f9b580cf..b3659abde53a 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -72,14 +72,16 @@ local schema = { { anyOf = { { - not = { - properties: { - lazy_load_paths: {const = true}, - required: {"lazy_load_paths"} - } - }, + properties = { + lazy_load_paths = {const = false}, + } + required = {"lazy_load_paths"} }, { + properties = { + lazy_load_paths = {const = true}, + }, + required = {"lazy_load_paths"}, anyOf = { {required = {"discovery"}}, {required = {"resource_registration_endpoint"}} From 2862111fc4ee94652f2bd2d6d0b5cad3cdee5042 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Mon, 18 Jan 2021 13:14:37 +0100 Subject: [PATCH 47/94] Fix syntax error. --- apisix/plugins/authz-keycloak.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index b3659abde53a..cc22860d51b9 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -63,18 +63,20 @@ local schema = { cache_ttl_seconds = {type = "integer", minimum = 1, default = 24 * 60 * 60}, }, allOf = { + -- Require discovery or token endpoint. { anyOf = { {required = {"discovery"}}, {required = {"token_endpoint"}} } }, + -- If lazy_load_paths is true, require discovery or resource registration endpoint. { anyOf = { { properties = { lazy_load_paths = {const = false}, - } + }, required = {"lazy_load_paths"} }, { From dd1240b8ed9e6d85239a55bb7cb63520b59f6436 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Mon, 18 Jan 2021 13:51:19 +0100 Subject: [PATCH 48/94] Add and fix tests. --- t/plugin/authz-keycloak.t | 123 ++++++++++++++++++++++++++++---------- 1 file changed, 91 insertions(+), 32 deletions(-) diff --git a/t/plugin/authz-keycloak.t b/t/plugin/authz-keycloak.t index 67413d1a699f..7c2d89bc8e9b 100644 --- a/t/plugin/authz-keycloak.t +++ b/t/plugin/authz-keycloak.t @@ -24,16 +24,14 @@ run_tests; __DATA__ -=== TEST 1: sanity (using token endpoint) +=== TEST 1: minimal valid configuration w/o discovery --- config location /t { content_by_lua_block { local plugin = require("apisix.plugins.authz-keycloak") local ok, err = plugin.check_schema({ - client_id = "foo", - token_endpoint = "https://host.domain/auth/realms/foo/protocol/openid-connect/token", - grant_type = "urn:ietf:params:oauth:grant-type:uma-ticket" - }) + token_endpoint = "https://host.domain/auth/realms/foo/protocol/openid-connect/token" + }) if not ok then ngx.say(err) end @@ -50,16 +48,14 @@ done -=== TEST 2: sanity (using discovery endpoint) +=== TEST 2: minimal valid configuration with discovery --- config location /t { content_by_lua_block { local plugin = require("apisix.plugins.authz-keycloak") local ok, err = plugin.check_schema({ - client_id = "foo", - discovery = "https://host.domain/auth/realms/foo/.well-known/uma2-configuration", - grant_type = "urn:ietf:params:oauth:grant-type:uma-ticket" - }) + discovery = "https://host.domain/auth/realms/foo/.well-known/uma2-configuration" + }) if not ok then ngx.say(err) end @@ -76,18 +72,81 @@ done -=== TEST 3: full schema check +=== TEST 3: minimal valid configuration w/o discovery when lazy_load_paths=true --- config location /t { content_by_lua_block { local plugin = require("apisix.plugins.authz-keycloak") - local ok, err = plugin.check_schema({discovery = "https://host.domain/auth/realms/foo/.well-known/uma2-configuration", - token_endpoint = "https://host.domain/auth/realms/foo/protocol/openid-connect/token", - permissions = {"res:customer#scopes:view"}, - timeout = 1000, - client_id = "University", - grant_type = "urn:ietf:params:oauth:grant-type:uma-ticket" - }) + local ok, err = plugin.check_schema({ + lazy_load_paths = true, + token_endpoint = "https://host.domain/auth/realms/foo/protocol/openid-connect/token", + resource_registrytion_endpoint = "https://host.domain/auth/realms/foo/authz/protection/resource_set" + }) + if not ok then + ngx.say(err) + end + + ngx.say("done") + } + } +--- request +GET /t +--- response_body +done +--- no_error_log +[error] + + + +=== TEST 4: minimal valid configuration with discovery when lazy_load_paths=true +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.authz-keycloak") + local ok, err = plugin.check_schema({ + lazy_load_paths = true, + discovery = "https://host.domain/auth/realms/foo/.well-known/uma2-configuration" + }) + if not ok then + ngx.say(err) + end + + ngx.say("done") + } + } +--- request +GET /t +--- response_body +done +--- no_error_log +[error] + + + +=== TEST 5: full schema check +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.authz-keycloak") + local ok, err = plugin.check_schema({ + discovery = "https://host.domain/auth/realms/foo/.well-known/uma2-configuration", + token_endpoint = "https://host.domain/auth/realms/foo/protocol/openid-connect/token", + resource_registration_endpoint = "https://host.domain/auth/realms/foo/authz/protection/resource_set", + permissions = {"res:customer#scopes:view"}, + grant_type = "urn:ietf:params:oauth:grant-type:uma-ticket", + timeout = 1000, + policy_enforcement_mode = "ENFORCING", + keepalive = true, + keepalive_timeout = 10000, + keepalive_pool = 5, + ssl_verify = false, + client_id = "University", + audience = "University", + client_secret = "secret", + lazy_load_paths = false, + http_method_as_scope = false, + cache_ttl_seconds = 1000 + }) if not ok then ngx.say(err) end @@ -104,7 +163,7 @@ done -=== TEST 4: token_endpoint and discovery both missing +=== TEST 6: token_endpoint and discovery both missing --- config location /t { content_by_lua_block { @@ -120,14 +179,14 @@ done --- request GET /t --- response_body -object matches none of the requireds: ["discovery"] or ["token_endpoint"] +allOf 1 failed: object matches none of the requireds: ["discovery"] or ["token_endpoint"] done --- no_error_log [error] -=== TEST 5: add plugin with view course permissions (using token endpoint) +=== TEST 7: add plugin with view course permissions (using token endpoint) --- config location /t { content_by_lua_block { @@ -193,7 +252,7 @@ passed -=== TEST 6: Get access token for teacher and access view course route +=== TEST 8: Get access token for teacher and access view course route --- config location /t { content_by_lua_block { @@ -241,7 +300,7 @@ true -=== TEST 7: invalid access token +=== TEST 9: invalid access token --- config location /t { content_by_lua_block { @@ -268,7 +327,7 @@ Invalid bearer token -=== TEST 8: add plugin with view course permissions (using discovery) +=== TEST 10: add plugin with view course permissions (using discovery) --- config location /t { content_by_lua_block { @@ -334,7 +393,7 @@ passed -=== TEST 9: Get access token for teacher and access view course route +=== TEST 11: Get access token for teacher and access view course route --- config location /t { content_by_lua_block { @@ -382,7 +441,7 @@ true -=== TEST 10: invalid access token +=== TEST 12: invalid access token --- config location /t { content_by_lua_block { @@ -409,7 +468,7 @@ Invalid bearer token -=== TEST 11: add plugin for delete course route +=== TEST 13: add plugin for delete course route --- config location /t { content_by_lua_block { @@ -475,7 +534,7 @@ passed -=== TEST 12: Get access token for student and delete course +=== TEST 14: Get access token for student and delete course --- config location /t { content_by_lua_block { @@ -523,7 +582,7 @@ true -=== TEST 13: Add https endpoint with ssl_verify true (default) +=== TEST 15: Add https endpoint with ssl_verify true (default) --- config location /t { content_by_lua_block { @@ -589,7 +648,7 @@ passed -=== TEST 14: TEST with fake token and https endpoint +=== TEST 16: TEST with fake token and https endpoint --- config location /t { content_by_lua_block { @@ -619,7 +678,7 @@ error while sending authz request to https://127.0.0.1:8443/auth/realms/Universi -=== TEST 15: Add htttps endpoint with ssl_verify false +=== TEST 17: Add htttps endpoint with ssl_verify false --- config location /t { content_by_lua_block { @@ -687,7 +746,7 @@ passed -=== TEST 16: TEST for https based token verification with ssl_verify false +=== TEST 18: TEST for https based token verification with ssl_verify false --- config location /t { content_by_lua_block { From bd4a929b13ee546dbfdd91eb60640d2a8167ffbd Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Mon, 18 Jan 2021 14:30:09 +0100 Subject: [PATCH 49/94] Fix test case. --- t/plugin/authz-keycloak.t | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/t/plugin/authz-keycloak.t b/t/plugin/authz-keycloak.t index 7c2d89bc8e9b..e414b76c0f2d 100644 --- a/t/plugin/authz-keycloak.t +++ b/t/plugin/authz-keycloak.t @@ -80,7 +80,7 @@ done local ok, err = plugin.check_schema({ lazy_load_paths = true, token_endpoint = "https://host.domain/auth/realms/foo/protocol/openid-connect/token", - resource_registrytion_endpoint = "https://host.domain/auth/realms/foo/authz/protection/resource_set" + resource_registration_endpoint = "https://host.domain/auth/realms/foo/authz/protection/resource_set" }) if not ok then ngx.say(err) From 42f4b16eec4c4c30772217710686121a431e2a58 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Mon, 18 Jan 2021 15:54:38 +0100 Subject: [PATCH 50/94] Debugging. --- apisix/plugins/authz-keycloak.lua | 23 ++++++++--------------- t/plugin/authz-keycloak.t | 4 ++++ 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index cc22860d51b9..9d0bafa0f720 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -72,22 +72,15 @@ local schema = { }, -- If lazy_load_paths is true, require discovery or resource registration endpoint. { - anyOf = { - { - properties = { - lazy_load_paths = {const = false}, - }, - required = {"lazy_load_paths"} + if = { + properties = { + lazy_load_paths = {const = true}, }, - { - properties = { - lazy_load_paths = {const = true}, - }, - required = {"lazy_load_paths"}, - anyOf = { - {required = {"discovery"}}, - {required = {"resource_registration_endpoint"}} - } + }, + then = { + anyOf = { + {required = {"discovery"}}, + {required = {"resource_registration_endpoint"}} } } } diff --git a/t/plugin/authz-keycloak.t b/t/plugin/authz-keycloak.t index e414b76c0f2d..0ba04394fd87 100644 --- a/t/plugin/authz-keycloak.t +++ b/t/plugin/authz-keycloak.t @@ -269,6 +269,7 @@ passed }) if res.status == 200 then + ngx.say("Got token.") local body = json_decode(res.body) local accessToken = body["access_token"] @@ -289,6 +290,8 @@ passed else ngx.say(false) end + ngx.say(res.status) + ngx.say(res.body) } } --- request @@ -297,6 +300,7 @@ GET /t true --- no_error_log [error] +[debug] From ae02b894d22c9a82a267aa3dbff221f425f474f5 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Mon, 18 Jan 2021 15:58:22 +0100 Subject: [PATCH 51/94] Temporarily only run tests for authz-keycloak plugin. --- .travis/linux_openresty_runner.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis/linux_openresty_runner.sh b/.travis/linux_openresty_runner.sh index f451bbe053f0..08ef25b129a6 100755 --- a/.travis/linux_openresty_runner.sh +++ b/.travis/linux_openresty_runner.sh @@ -138,7 +138,7 @@ script() { make lint && make license-check || exit 1 # APISIX_ENABLE_LUACOV=1 PERL5LIB=.:$PERL5LIB prove -Itest-nginx/lib -r t - PERL5LIB=.:$PERL5LIB prove -Itest-nginx/lib -r t + PERL5LIB=.:$PERL5LIB prove -Itest-nginx/lib -r t/plugin/authz-keycloak.t } after_success() { From d97f283b295c7413f7416a9390c60e6d64c72d80 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Mon, 18 Jan 2021 16:00:34 +0100 Subject: [PATCH 52/94] Add shared dictionary for discovery documents. --- t/APISIX.pm | 1 + 1 file changed, 1 insertion(+) diff --git a/t/APISIX.pm b/t/APISIX.pm index 95d0119eabc4..94126e5896f3 100644 --- a/t/APISIX.pm +++ b/t/APISIX.pm @@ -252,6 +252,7 @@ _EOC_ lua_shared_dict plugin-limit-count-redis-cluster-slot-lock 1m; lua_shared_dict tracing_buffer 10m; # plugin skywalking lua_shared_dict access_tokens 1m; # plugin authz-keycloak + lua_shared_dict discovery 1m; # plugin authz-keycloak lua_shared_dict plugin-api-breaker 10m; lua_capture_error_log 1m; # plugin error-log-logger From 1aea5d4e78e4778b345fb247571478fdb2ffd4af Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Mon, 18 Jan 2021 16:51:14 +0100 Subject: [PATCH 53/94] Fix syntax error. --- apisix/plugins/authz-keycloak.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index 9d0bafa0f720..cab3ab82a55a 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -72,12 +72,12 @@ local schema = { }, -- If lazy_load_paths is true, require discovery or resource registration endpoint. { - if = { + ["if"] = { properties = { lazy_load_paths = {const = true}, }, }, - then = { + ["then"] = { anyOf = { {required = {"discovery"}}, {required = {"resource_registration_endpoint"}} From 12039329ff4397fba39c8aeb5708e2a727ecaa37 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Mon, 18 Jan 2021 17:19:50 +0100 Subject: [PATCH 54/94] Debugging. --- t/plugin/authz-keycloak.t | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/t/plugin/authz-keycloak.t b/t/plugin/authz-keycloak.t index 0ba04394fd87..075a1d53622a 100644 --- a/t/plugin/authz-keycloak.t +++ b/t/plugin/authz-keycloak.t @@ -287,11 +287,15 @@ passed else ngx.say(false) end + + ngx.say(res.status) + ngx.say(res.body) + ngx.say(err) else ngx.say(false) + ngx.say(res.status) + ngx.say(res.body) end - ngx.say(res.status) - ngx.say(res.body) } } --- request From bd57d6229f638fbf46bd2b46064673975dcc70db Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Mon, 18 Jan 2021 17:35:30 +0100 Subject: [PATCH 55/94] Fix incorrect reference to configuration entry. --- apisix/plugins/authz-keycloak.lua | 6 +++--- t/plugin/authz-keycloak.t | 8 -------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index cab3ab82a55a..8b555c031f2f 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -60,7 +60,7 @@ local schema = { client_secret = {type = "string", minLength = 1, maxLength = 100}, lazy_load_paths = {type = "boolean", default = false}, http_method_as_scope = {type = "boolean", default = false}, - cache_ttl_seconds = {type = "integer", minimum = 1, default = 24 * 60 * 60}, + cache_ttl_seconds = {type = "integer", minimum = 1, default = 24 * 60 * 60} }, allOf = { -- Require discovery or token endpoint. @@ -553,12 +553,12 @@ local function evaluate_permissions(conf, ctx, token) else -- Use statically configured permissions. - if conf.permission == nil then + if conf.permissions == nil then -- No static permission configured. return 500, "No static permission configured." end - permission = conf.permission + permission = conf.permissions end -- Return 403 if permission is empty and enforcement mode is "ENFORCING". diff --git a/t/plugin/authz-keycloak.t b/t/plugin/authz-keycloak.t index 075a1d53622a..e414b76c0f2d 100644 --- a/t/plugin/authz-keycloak.t +++ b/t/plugin/authz-keycloak.t @@ -269,7 +269,6 @@ passed }) if res.status == 200 then - ngx.say("Got token.") local body = json_decode(res.body) local accessToken = body["access_token"] @@ -287,14 +286,8 @@ passed else ngx.say(false) end - - ngx.say(res.status) - ngx.say(res.body) - ngx.say(err) else ngx.say(false) - ngx.say(res.status) - ngx.say(res.body) end } } @@ -304,7 +297,6 @@ GET /t true --- no_error_log [error] -[debug] From 22793b52d17ee6af882a56c5947f9497d652c9ea Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Mon, 18 Jan 2021 17:43:06 +0100 Subject: [PATCH 56/94] Fix test case. --- apisix/plugins/authz-keycloak.lua | 2 +- t/plugin/authz-keycloak.t | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index 8b555c031f2f..e21ddfa87580 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -555,7 +555,7 @@ local function evaluate_permissions(conf, ctx, token) if conf.permissions == nil then -- No static permission configured. - return 500, "No static permission configured." + return 500, "No static permissions configured." end permission = conf.permissions diff --git a/t/plugin/authz-keycloak.t b/t/plugin/authz-keycloak.t index e414b76c0f2d..7fab6a830ba1 100644 --- a/t/plugin/authz-keycloak.t +++ b/t/plugin/authz-keycloak.t @@ -674,7 +674,7 @@ GET /t --- response_body false --- error_log -error while sending authz request to https://127.0.0.1:8443/auth/realms/University/protocol/openid-connect/token: 18: self signed certificate +Error while sending authz request to https://127.0.0.1:8443/auth/realms/University/protocol/openid-connect/token: 18: self signed certificate From df9e0facf2b4f61b8778020152023476575690cd Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Mon, 18 Jan 2021 17:52:59 +0100 Subject: [PATCH 57/94] Some minor adjustments. --- apisix/plugins/authz-keycloak.lua | 6 ---- t/plugin/authz-keycloak.t | 52 +++++++++++++++++++++++-------- 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index e21ddfa87580..e092a07b11ae 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -552,12 +552,6 @@ local function evaluate_permissions(conf, ctx, token) end else -- Use statically configured permissions. - - if conf.permissions == nil then - -- No static permission configured. - return 500, "No static permissions configured." - end - permission = conf.permissions end diff --git a/t/plugin/authz-keycloak.t b/t/plugin/authz-keycloak.t index 7fab6a830ba1..74d96713c440 100644 --- a/t/plugin/authz-keycloak.t +++ b/t/plugin/authz-keycloak.t @@ -168,7 +168,7 @@ done location /t { content_by_lua_block { local plugin = require("apisix.plugins.authz-keycloak") - local ok, err = plugin.check_schema({client_id = "University", permissions = {"res:customer#scopes:view"}}) + local ok, err = plugin.check_schema({}) if not ok then ngx.say(err) end @@ -186,7 +186,33 @@ done -=== TEST 7: add plugin with view course permissions (using token endpoint) +=== TEST 7: resource_registration_endpoint and discovery both missing and lazy_load_paths is true +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.authz-keycloak") + local ok, err = plugin.check_schema({ + token_endpoint = "https://host.domain/auth/realms/foo/protocol/openid-connect/token", + lazy_load_paths = true + }) + if not ok then + ngx.say(err) + end + + ngx.say("done") + } + } +--- request +GET /t +--- response_body +allOf 2 failed: object matches none of the requireds: ["discovery"] or ["resource_registration_endpoint"] +done +--- no_error_log +[error] + + + +=== TEST 8: add plugin with view course permissions (using token endpoint) --- config location /t { content_by_lua_block { @@ -252,7 +278,7 @@ passed -=== TEST 8: Get access token for teacher and access view course route +=== TEST 9: Get access token for teacher and access view course route --- config location /t { content_by_lua_block { @@ -300,7 +326,7 @@ true -=== TEST 9: invalid access token +=== TEST 10: invalid access token --- config location /t { content_by_lua_block { @@ -327,7 +353,7 @@ Invalid bearer token -=== TEST 10: add plugin with view course permissions (using discovery) +=== TEST 11: add plugin with view course permissions (using discovery) --- config location /t { content_by_lua_block { @@ -393,7 +419,7 @@ passed -=== TEST 11: Get access token for teacher and access view course route +=== TEST 12: Get access token for teacher and access view course route --- config location /t { content_by_lua_block { @@ -441,7 +467,7 @@ true -=== TEST 12: invalid access token +=== TEST 13: invalid access token --- config location /t { content_by_lua_block { @@ -468,7 +494,7 @@ Invalid bearer token -=== TEST 13: add plugin for delete course route +=== TEST 14: add plugin for delete course route --- config location /t { content_by_lua_block { @@ -534,7 +560,7 @@ passed -=== TEST 14: Get access token for student and delete course +=== TEST 15: Get access token for student and delete course --- config location /t { content_by_lua_block { @@ -582,7 +608,7 @@ true -=== TEST 15: Add https endpoint with ssl_verify true (default) +=== TEST 16: Add https endpoint with ssl_verify true (default) --- config location /t { content_by_lua_block { @@ -648,7 +674,7 @@ passed -=== TEST 16: TEST with fake token and https endpoint +=== TEST 17: TEST with fake token and https endpoint --- config location /t { content_by_lua_block { @@ -678,7 +704,7 @@ Error while sending authz request to https://127.0.0.1:8443/auth/realms/Universi -=== TEST 17: Add htttps endpoint with ssl_verify false +=== TEST 18: Add htttps endpoint with ssl_verify false --- config location /t { content_by_lua_block { @@ -746,7 +772,7 @@ passed -=== TEST 18: TEST for https based token verification with ssl_verify false +=== TEST 19: TEST for https based token verification with ssl_verify false --- config location /t { content_by_lua_block { From a48ed600f0c50ba37ee8faa9bcb16b4397f516c2 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Mon, 18 Jan 2021 17:56:09 +0100 Subject: [PATCH 58/94] Re-enable all test cases. --- .travis/linux_openresty_runner.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis/linux_openresty_runner.sh b/.travis/linux_openresty_runner.sh index 08ef25b129a6..f451bbe053f0 100755 --- a/.travis/linux_openresty_runner.sh +++ b/.travis/linux_openresty_runner.sh @@ -138,7 +138,7 @@ script() { make lint && make license-check || exit 1 # APISIX_ENABLE_LUACOV=1 PERL5LIB=.:$PERL5LIB prove -Itest-nginx/lib -r t - PERL5LIB=.:$PERL5LIB prove -Itest-nginx/lib -r t/plugin/authz-keycloak.t + PERL5LIB=.:$PERL5LIB prove -Itest-nginx/lib -r t } after_success() { From b7432a3ffe904bc7501bd0d208b0f86bba754dc5 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Mon, 18 Jan 2021 21:07:07 +0100 Subject: [PATCH 59/94] Attempt at fixing schema. --- apisix/plugins/authz-keycloak.lua | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index e092a07b11ae..c22467b013fa 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -72,15 +72,20 @@ local schema = { }, -- If lazy_load_paths is true, require discovery or resource registration endpoint. { - ["if"] = { - properties = { - lazy_load_paths = {const = true}, + anyOf = { + { + properties = { + lazy_load_paths = {const = false}, + } }, - }, - ["then"] = { - anyOf = { - {required = {"discovery"}}, - {required = {"resource_registration_endpoint"}} + { + properties = { + lazy_load_paths = {const = true}, + }, + anyOf = { + {required = {"discovery"}}, + {required = {"resource_registration_endpoint"}} + } } } } From 83374372169b0337f6d06fb5a325c20df1f6b53d Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Mon, 18 Jan 2021 22:28:50 +0100 Subject: [PATCH 60/94] Another attempt at fixing schema. --- .travis/linux_openresty_runner.sh | 2 +- apisix/plugins/authz-keycloak.lua | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis/linux_openresty_runner.sh b/.travis/linux_openresty_runner.sh index f451bbe053f0..08ef25b129a6 100755 --- a/.travis/linux_openresty_runner.sh +++ b/.travis/linux_openresty_runner.sh @@ -138,7 +138,7 @@ script() { make lint && make license-check || exit 1 # APISIX_ENABLE_LUACOV=1 PERL5LIB=.:$PERL5LIB prove -Itest-nginx/lib -r t - PERL5LIB=.:$PERL5LIB prove -Itest-nginx/lib -r t + PERL5LIB=.:$PERL5LIB prove -Itest-nginx/lib -r t/plugin/authz-keycloak.t } after_success() { diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index c22467b013fa..f41128dd9e21 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -75,12 +75,12 @@ local schema = { anyOf = { { properties = { - lazy_load_paths = {const = false}, + lazy_load_paths = {enum = {false}}, } }, { properties = { - lazy_load_paths = {const = true}, + lazy_load_paths = {enum = {true}}, }, anyOf = { {required = {"discovery"}}, From f7e06c630f2bde95d144201a636b0f33628b9556 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Mon, 18 Jan 2021 22:34:04 +0100 Subject: [PATCH 61/94] Fix test case. --- .travis/linux_openresty_runner.sh | 2 +- t/plugin/authz-keycloak.t | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis/linux_openresty_runner.sh b/.travis/linux_openresty_runner.sh index 08ef25b129a6..f451bbe053f0 100755 --- a/.travis/linux_openresty_runner.sh +++ b/.travis/linux_openresty_runner.sh @@ -138,7 +138,7 @@ script() { make lint && make license-check || exit 1 # APISIX_ENABLE_LUACOV=1 PERL5LIB=.:$PERL5LIB prove -Itest-nginx/lib -r t - PERL5LIB=.:$PERL5LIB prove -Itest-nginx/lib -r t/plugin/authz-keycloak.t + PERL5LIB=.:$PERL5LIB prove -Itest-nginx/lib -r t } after_success() { diff --git a/t/plugin/authz-keycloak.t b/t/plugin/authz-keycloak.t index 74d96713c440..1ec3c7cfe43f 100644 --- a/t/plugin/authz-keycloak.t +++ b/t/plugin/authz-keycloak.t @@ -205,7 +205,7 @@ done --- request GET /t --- response_body -allOf 2 failed: object matches none of the requireds: ["discovery"] or ["resource_registration_endpoint"] +allOf 2 failed: object matches none of the requireds done --- no_error_log [error] From f9b002a02ecd54558ef7de4f4313d3dcb4a3085d Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Tue, 19 Jan 2021 09:43:02 +0100 Subject: [PATCH 62/94] Switch to updated Keycloak Docker image to enable testing of URI-to-resource and HTTP-method-to-scope mappings. --- .travis/linux_openresty_runner.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis/linux_openresty_runner.sh b/.travis/linux_openresty_runner.sh index f451bbe053f0..f52f8d28814d 100755 --- a/.travis/linux_openresty_runner.sh +++ b/.travis/linux_openresty_runner.sh @@ -24,7 +24,7 @@ before_install() { docker run --rm -itd -p 6379:6379 --name apisix_redis redis:3.0-alpine docker run --rm -itd -e HTTP_PORT=8888 -e HTTPS_PORT=9999 -p 8888:8888 -p 9999:9999 mendhak/http-https-echo # Runs Keycloak version 10.0.2 with inbuilt policies for unit tests - docker run --rm -itd -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=123456 -p 8090:8080 -p 8443:8443 sshniro/keycloak-apisix + docker run --rm -itd -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=123456 -p 8090:8080 -p 8443:8443 jenskeiner/keycloak-apisix # spin up kafka cluster for tests (1 zookeper and 1 kafka instance) docker pull bitnami/zookeeper:3.6.0 docker pull bitnami/kafka:latest From d7e98e6915775e1e4c332a342014788690bd238c Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Tue, 19 Jan 2021 09:44:33 +0100 Subject: [PATCH 63/94] Temporarily only test authz-keycloak plugin to spped up checks. --- .travis/linux_openresty_runner.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis/linux_openresty_runner.sh b/.travis/linux_openresty_runner.sh index f52f8d28814d..e7f33be91751 100755 --- a/.travis/linux_openresty_runner.sh +++ b/.travis/linux_openresty_runner.sh @@ -138,7 +138,7 @@ script() { make lint && make license-check || exit 1 # APISIX_ENABLE_LUACOV=1 PERL5LIB=.:$PERL5LIB prove -Itest-nginx/lib -r t - PERL5LIB=.:$PERL5LIB prove -Itest-nginx/lib -r t + PERL5LIB=.:$PERL5LIB prove -Itest-nginx/lib -r t/plugin/authz-keycloak.t } after_success() { From 9541bc2adb869dc8e81404935b8f76b5c3ac7ada Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Tue, 19 Jan 2021 10:49:55 +0100 Subject: [PATCH 64/94] Add test case to set up lazy_load_paths and http_method_as_scope. --- t/plugin/authz-keycloak.t | 66 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/t/plugin/authz-keycloak.t b/t/plugin/authz-keycloak.t index 1ec3c7cfe43f..cf9e39c676b5 100644 --- a/t/plugin/authz-keycloak.t +++ b/t/plugin/authz-keycloak.t @@ -799,3 +799,69 @@ GET /t false --- error_log Request denied: HTTP 401 Unauthorized. Body: {"error":"HTTP 401 Unauthorized"} + + + +=== TEST 20: add plugin with lazy_load_paths and http_method_as_scope +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "authz-keycloak": { + "discovery": "http://127.0.0.1:8090/auth/realms/University/.well-known/uma2-configuration", + "client_id": "course_management", + "client_secret": "d1ec69e9-55d2-4109-a3ea-befa071579d5", + "lazy_load_paths": true, + "http_method_as_scope": true + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "uri": "/course/*" + }]], + [[{ + "node": { + "value": { + "plugins": { + "authz-keycloak": { + "discovery": "http://127.0.0.1:8090/auth/realms/University/.well-known/uma2-configuration", + "client_id": "course_management", + "client_secret": "d1ec69e9-55d2-4109-a3ea-befa071579d5", + "lazy_load_paths": true, + "http_method_as_scope": true + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "uri": "/course/*" + }, + "key": "/apisix/routes/1" + }, + "action": "set" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] From 6bfd47ff5305ece7b870531668aaa7ff67359416 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Tue, 19 Jan 2021 11:28:23 +0100 Subject: [PATCH 65/94] Add tests to check Keycloak permissions mapped from URI and HTTP method. --- t/plugin/authz-keycloak.t | 192 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) diff --git a/t/plugin/authz-keycloak.t b/t/plugin/authz-keycloak.t index cf9e39c676b5..3c808f07f805 100644 --- a/t/plugin/authz-keycloak.t +++ b/t/plugin/authz-keycloak.t @@ -865,3 +865,195 @@ GET /t passed --- no_error_log [error] + + + +=== TEST 21: Get access token for teacher and access view course route +--- config + location /t { + content_by_lua_block { + local json_decode = require("toolkit.json").decode + local http = require "resty.http" + local httpc = http.new() + local uri = "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token" + local res, err = httpc:request_uri(uri, { + method = "POST", + body = "grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=teacher@gmail.com&password=123456", + headers = { + ["Content-Type"] = "application/x-www-form-urlencoded" + } + }) + + if res.status == 200 then + local body = json_decode(res.body) + local accessToken = body["access_token"] + + + uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/course/foo" + local res, err = httpc:request_uri(uri, { + method = "GET", + headers = { + ["Authorization"] = "Bearer " .. accessToken, + } + }) + + if res.status == 200 then + ngx.say(true) + else + ngx.say(false) + end + else + ngx.say(false) + end + } + } +--- request +GET /t +--- response_body +true +--- no_error_log +[error] + + + +=== TEST 22: Get access token for student and access view course route +--- config + location /t { + content_by_lua_block { + local json_decode = require("toolkit.json").decode + local http = require "resty.http" + local httpc = http.new() + local uri = "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token" + local res, err = httpc:request_uri(uri, { + method = "POST", + body = "grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=student@gmail.com&password=123456", + headers = { + ["Content-Type"] = "application/x-www-form-urlencoded" + } + }) + + if res.status == 200 then + local body = json_decode(res.body) + local accessToken = body["access_token"] + + + uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/course/foo" + local res, err = httpc:request_uri(uri, { + method = "GET", + headers = { + ["Authorization"] = "Bearer " .. accessToken, + } + }) + + if res.status == 200 then + ngx.say(true) + else + ngx.say(false) + end + else + ngx.say(false) + end + } + } +--- request +GET /t +--- response_body +true +--- no_error_log +[error] + + + +=== TEST 23: Get access token for teacher and delete course +--- config + location /t { + content_by_lua_block { + local json_decode = require("toolkit.json").decode + local http = require "resty.http" + local httpc = http.new() + local uri = "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token" + local res, err = httpc:request_uri(uri, { + method = "POST", + body = "grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=teacher@gmail.com&password=123456", + headers = { + ["Content-Type"] = "application/x-www-form-urlencoded" + } + }) + + if res.status == 200 then + local body = json_decode(res.body) + local accessToken = body["access_token"] + + + uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/course/foo" + local res, err = httpc:request_uri(uri, { + method = "DELETE", + headers = { + ["Authorization"] = "Bearer " .. accessToken, + } + }) + + if res.status == 200 then + ngx.say(true) + else + ngx.say(false) + end + else + ngx.say(false) + end + } + } +--- request +GET /t +--- response_body +true +--- no_error_log +[error] + + + +=== TEST 24: Get access token for student and delete course +--- config + location /t { + content_by_lua_block { + local json_decode = require("toolkit.json").decode + local http = require "resty.http" + local httpc = http.new() + local uri = "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token" + local res, err = httpc:request_uri(uri, { + method = "POST", + body = "grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=student@gmail.com&password=123456", + headers = { + ["Content-Type"] = "application/x-www-form-urlencoded" + } + }) + + if res.status == 200 then + local body = json_decode(res.body) + local accessToken = body["access_token"] + + + uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/course/foo" + local res, err = httpc:request_uri(uri, { + method = "DELETE", + headers = { + ["Authorization"] = "Bearer " .. accessToken, + } + }) + + if res.status == 403 then + ngx.say(true) + else + ngx.say(false) + end + else + ngx.say(false) + end + } + } +--- request +GET /t +--- response_body +true +--- error_log +{"error":"access_denied","error_description":"not_authorized"} From af205a1888ec447a460c5c966bb1c6998a9b0dda Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Tue, 19 Jan 2021 11:57:18 +0100 Subject: [PATCH 66/94] Debugging. --- apisix/plugins/authz-keycloak.lua | 2 +- t/plugin/authz-keycloak.t | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index f41128dd9e21..047af2c91e28 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -485,7 +485,7 @@ local function authz_keycloak_resolve_permission(conf, uri, sa_access_token) return 500, err end - log.error("Resource registration endpoint: ", resource_registration_endpoint) + log.debug("Resource registration endpoint: ", resource_registration_endpoint) local httpc = http.new() httpc:set_timeout(conf.timeout) diff --git a/t/plugin/authz-keycloak.t b/t/plugin/authz-keycloak.t index 3c808f07f805..2b2377357289 100644 --- a/t/plugin/authz-keycloak.t +++ b/t/plugin/authz-keycloak.t @@ -902,6 +902,8 @@ passed else ngx.say(false) end + ngx.say(res.status) + ngx.say(res.body) else ngx.say(false) end From 0ad8601daf8c37a32349f3080b61cbf196821e8e Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Tue, 19 Jan 2021 12:21:31 +0100 Subject: [PATCH 67/94] Fix test cases. --- t/plugin/authz-keycloak.t | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/t/plugin/authz-keycloak.t b/t/plugin/authz-keycloak.t index 2b2377357289..8acc80a56fcb 100644 --- a/t/plugin/authz-keycloak.t +++ b/t/plugin/authz-keycloak.t @@ -825,7 +825,7 @@ Request denied: HTTP 401 Unauthorized. Body: {"error":"HTTP 401 Unauthorized"} }, "type": "roundrobin" }, - "uri": "/course/*" + "uri": "/course/foo" }]], [[{ "node": { @@ -845,7 +845,7 @@ Request denied: HTTP 401 Unauthorized. Body: {"error":"HTTP 401 Unauthorized"} }, "type": "roundrobin" }, - "uri": "/course/*" + "uri": "/course/foo" }, "key": "/apisix/routes/1" }, From c583cb5e989d60ae3b89dda67b02d790da2ccd79 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Tue, 19 Jan 2021 12:53:53 +0100 Subject: [PATCH 68/94] Add fake endpoint for authz-keycloak plugin testing. --- t/lib/server.lua | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/t/lib/server.lua b/t/lib/server.lua index e5edde9e9484..588a5698d388 100644 --- a/t/lib/server.lua +++ b/t/lib/server.lua @@ -56,6 +56,12 @@ function _M.hello_() end +-- Fake endpoint, needed for testing authz-keycloak plugin. +function _M.course_foo() + ngx.say("course foo") +end + + function _M.server_port() ngx.print(ngx.var.server_port) end From 46b9c1f777642adc3142a4dab8b6815b234c7425 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Tue, 19 Jan 2021 13:20:31 +0100 Subject: [PATCH 69/94] Remove debug code. --- t/plugin/authz-keycloak.t | 2 -- 1 file changed, 2 deletions(-) diff --git a/t/plugin/authz-keycloak.t b/t/plugin/authz-keycloak.t index 8acc80a56fcb..034bfe7ea0bb 100644 --- a/t/plugin/authz-keycloak.t +++ b/t/plugin/authz-keycloak.t @@ -902,8 +902,6 @@ passed else ngx.say(false) end - ngx.say(res.status) - ngx.say(res.body) else ngx.say(false) end From 697cdbe939813b7fc66893764ecac9121250506b Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Tue, 19 Jan 2021 13:39:44 +0100 Subject: [PATCH 70/94] Debugging. --- t/plugin/authz-keycloak.t | 2 ++ 1 file changed, 2 insertions(+) diff --git a/t/plugin/authz-keycloak.t b/t/plugin/authz-keycloak.t index 034bfe7ea0bb..b09e4281a643 100644 --- a/t/plugin/authz-keycloak.t +++ b/t/plugin/authz-keycloak.t @@ -950,6 +950,8 @@ true else ngx.say(false) end + ngx.say(res.status) + ngx.say(res.body) else ngx.say(false) end From 68ca6cc11a3508bb9591a3b4902b5fa13955fe77 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Tue, 19 Jan 2021 14:23:41 +0100 Subject: [PATCH 71/94] Remove debug code after fixing Docker image. --- t/plugin/authz-keycloak.t | 2 -- 1 file changed, 2 deletions(-) diff --git a/t/plugin/authz-keycloak.t b/t/plugin/authz-keycloak.t index b09e4281a643..034bfe7ea0bb 100644 --- a/t/plugin/authz-keycloak.t +++ b/t/plugin/authz-keycloak.t @@ -950,8 +950,6 @@ true else ngx.say(false) end - ngx.say(res.status) - ngx.say(res.body) else ngx.say(false) end From 8b982aace5d6db9d7cdd883dfd9723085610b78b Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Tue, 19 Jan 2021 14:35:50 +0100 Subject: [PATCH 72/94] Cleanup. --- .travis/linux_openresty_runner.sh | 2 +- t/plugin/authz-keycloak.t | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.travis/linux_openresty_runner.sh b/.travis/linux_openresty_runner.sh index e7f33be91751..f52f8d28814d 100755 --- a/.travis/linux_openresty_runner.sh +++ b/.travis/linux_openresty_runner.sh @@ -138,7 +138,7 @@ script() { make lint && make license-check || exit 1 # APISIX_ENABLE_LUACOV=1 PERL5LIB=.:$PERL5LIB prove -Itest-nginx/lib -r t - PERL5LIB=.:$PERL5LIB prove -Itest-nginx/lib -r t/plugin/authz-keycloak.t + PERL5LIB=.:$PERL5LIB prove -Itest-nginx/lib -r t } after_success() { diff --git a/t/plugin/authz-keycloak.t b/t/plugin/authz-keycloak.t index 034bfe7ea0bb..607758f3623b 100644 --- a/t/plugin/authz-keycloak.t +++ b/t/plugin/authz-keycloak.t @@ -868,7 +868,7 @@ passed -=== TEST 21: Get access token for teacher and access view course route +=== TEST 21: Get access token for teacher and access view course route. --- config location /t { content_by_lua_block { @@ -916,7 +916,7 @@ true -=== TEST 22: Get access token for student and access view course route +=== TEST 22: Get access token for student and access view course route. --- config location /t { content_by_lua_block { @@ -964,7 +964,7 @@ true -=== TEST 23: Get access token for teacher and delete course +=== TEST 23: Get access token for teacher and delete course. --- config location /t { content_by_lua_block { @@ -1012,7 +1012,7 @@ true -=== TEST 24: Get access token for student and delete course +=== TEST 24: Get access token for student and try to delete course. Should fail. --- config location /t { content_by_lua_block { From a6ae71a5d9f80b9af3766b57e03eace72fe6f3ed Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Tue, 19 Jan 2021 15:02:53 +0100 Subject: [PATCH 73/94] Fix CI build on Cent OS that's using an outdated Keycloak Docker image. --- .github/workflows/centos7-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/centos7-ci.yml b/.github/workflows/centos7-ci.yml index 14b6067c8959..ae5e56683fbb 100644 --- a/.github/workflows/centos7-ci.yml +++ b/.github/workflows/centos7-ci.yml @@ -68,7 +68,7 @@ jobs: run: | docker run --rm -itd -p 6379:6379 --name apisix_redis redis:3.0-alpine docker run --rm -itd -e HTTP_PORT=8888 -e HTTPS_PORT=9999 -p 8888:8888 -p 9999:9999 mendhak/http-https-echo - docker run --rm -itd -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=123456 -p 8090:8080 -p 8443:8443 sshniro/keycloak-apisix + docker run --rm -itd -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=123456 -p 8090:8080 -p 8443:8443 jenskeiner/keycloak-apisix docker network create kafka-net --driver bridge docker run --name zookeeper-server -d -p 2181:2181 --network kafka-net -e ALLOW_ANONYMOUS_LOGIN=yes bitnami/zookeeper:3.6.0 docker run --name kafka-server1 -d --network kafka-net -e ALLOW_PLAINTEXT_LISTENER=yes -e KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper-server:2181 -e KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092 -p 9092:9092 -e KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true bitnami/kafka:latest From 3677dba7dccfd6e31634d9be81aee6bd13934001 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Tue, 19 Jan 2021 15:11:16 +0100 Subject: [PATCH 74/94] Revert nack to original image. --- .github/workflows/centos7-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/centos7-ci.yml b/.github/workflows/centos7-ci.yml index ae5e56683fbb..14b6067c8959 100644 --- a/.github/workflows/centos7-ci.yml +++ b/.github/workflows/centos7-ci.yml @@ -68,7 +68,7 @@ jobs: run: | docker run --rm -itd -p 6379:6379 --name apisix_redis redis:3.0-alpine docker run --rm -itd -e HTTP_PORT=8888 -e HTTPS_PORT=9999 -p 8888:8888 -p 9999:9999 mendhak/http-https-echo - docker run --rm -itd -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=123456 -p 8090:8080 -p 8443:8443 jenskeiner/keycloak-apisix + docker run --rm -itd -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=123456 -p 8090:8080 -p 8443:8443 sshniro/keycloak-apisix docker network create kafka-net --driver bridge docker run --name zookeeper-server -d -p 2181:2181 --network kafka-net -e ALLOW_ANONYMOUS_LOGIN=yes bitnami/zookeeper:3.6.0 docker run --name kafka-server1 -d --network kafka-net -e ALLOW_PLAINTEXT_LISTENER=yes -e KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper-server:2181 -e KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092 -p 9092:9092 -e KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true bitnami/kafka:latest From 3a399b8fc7c534db73d5fea42470f0719a37f9f1 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Tue, 19 Jan 2021 15:11:36 +0100 Subject: [PATCH 75/94] And back to new image again. --- .github/workflows/centos7-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/centos7-ci.yml b/.github/workflows/centos7-ci.yml index 14b6067c8959..ae5e56683fbb 100644 --- a/.github/workflows/centos7-ci.yml +++ b/.github/workflows/centos7-ci.yml @@ -68,7 +68,7 @@ jobs: run: | docker run --rm -itd -p 6379:6379 --name apisix_redis redis:3.0-alpine docker run --rm -itd -e HTTP_PORT=8888 -e HTTPS_PORT=9999 -p 8888:8888 -p 9999:9999 mendhak/http-https-echo - docker run --rm -itd -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=123456 -p 8090:8080 -p 8443:8443 sshniro/keycloak-apisix + docker run --rm -itd -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=123456 -p 8090:8080 -p 8443:8443 jenskeiner/keycloak-apisix docker network create kafka-net --driver bridge docker run --name zookeeper-server -d -p 2181:2181 --network kafka-net -e ALLOW_ANONYMOUS_LOGIN=yes bitnami/zookeeper:3.6.0 docker run --name kafka-server1 -d --network kafka-net -e ALLOW_PLAINTEXT_LISTENER=yes -e KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper-server:2181 -e KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092 -p 9092:9092 -e KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true bitnami/kafka:latest From ea0ce9077f5e950503c88b02bcd385c1e393a994 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Wed, 20 Jan 2021 09:39:05 +0100 Subject: [PATCH 76/94] Remove conflict markers that were left in unintentionally. --- .travis/linux_openresty_runner.sh | 149 ------------------------------ 1 file changed, 149 deletions(-) diff --git a/.travis/linux_openresty_runner.sh b/.travis/linux_openresty_runner.sh index 9a2e6b6469e8..eb1e9858899d 100755 --- a/.travis/linux_openresty_runner.sh +++ b/.travis/linux_openresty_runner.sh @@ -17,154 +17,5 @@ # -<<<<<<< HEAD -before_install() { - sudo cpanm --notest Test::Nginx >build.log 2>&1 || (cat build.log && exit 1) - docker pull redis:3.0-alpine - docker run --rm -itd -p 6379:6379 --name apisix_redis redis:3.0-alpine - docker run --rm -itd -e HTTP_PORT=8888 -e HTTPS_PORT=9999 -p 8888:8888 -p 9999:9999 mendhak/http-https-echo - # Runs Keycloak version 10.0.2 with inbuilt policies for unit tests - docker run --rm -itd -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=123456 -p 8090:8080 -p 8443:8443 jenskeiner/keycloak-apisix - # spin up kafka cluster for tests (1 zookeper and 1 kafka instance) - docker pull bitnami/zookeeper:3.6.0 - docker pull bitnami/kafka:latest - docker network create kafka-net --driver bridge - docker run --name zookeeper-server -d -p 2181:2181 --network kafka-net -e ALLOW_ANONYMOUS_LOGIN=yes bitnami/zookeeper:3.6.0 - docker run --name kafka-server1 -d --network kafka-net -e ALLOW_PLAINTEXT_LISTENER=yes -e KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper-server:2181 -e KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092 -p 9092:9092 -e KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true bitnami/kafka:latest - docker pull bitinit/eureka - docker run --name eureka -d -p 8761:8761 --env ENVIRONMENT=apisix --env spring.application.name=apisix-eureka --env server.port=8761 --env eureka.instance.ip-address=127.0.0.1 --env eureka.client.registerWithEureka=true --env eureka.client.fetchRegistry=false --env eureka.client.serviceUrl.defaultZone=http://127.0.0.1:8761/eureka/ bitinit/eureka - sleep 5 - docker exec -i kafka-server1 /opt/bitnami/kafka/bin/kafka-topics.sh --create --zookeeper zookeeper-server:2181 --replication-factor 1 --partitions 1 --topic test2 - - # start skywalking - docker run --rm --name skywalking -d -p 1234:1234 -p 11800:11800 -p 12800:12800 apache/skywalking-oap-server -} - -do_install() { - export_or_prefix - - ./utils/linux-install-openresty.sh - - ./utils/linux-install-luarocks.sh - sudo luarocks install luacheck > build.log 2>&1 || (cat build.log && exit 1) - - ./utils/linux-install-etcd-client.sh - - if [ ! -f "build-cache/apisix-master-0.rockspec" ]; then - create_lua_deps - - else - src=`md5sum rockspec/apisix-master-0.rockspec | awk '{print $1}'` - src_cp=`md5sum build-cache/apisix-master-0.rockspec | awk '{print $1}'` - if [ "$src" = "$src_cp" ]; then - echo "Use lua deps cache" - sudo cp -r build-cache/deps ./ - else - create_lua_deps - fi - fi - - # sudo apt-get install tree -y - # tree deps - - git clone https://github.com/iresty/test-nginx.git test-nginx - make utils - - git clone https://github.com/apache/openwhisk-utilities.git .travis/openwhisk-utilities - cp .travis/ASF* .travis/openwhisk-utilities/scancode/ - - ls -l ./ - if [ ! -f "build-cache/grpc_server_example" ]; then - wget https://github.com/iresty/grpc_server_example/releases/download/20200901/grpc_server_example-amd64.tar.gz - tar -xvf grpc_server_example-amd64.tar.gz - mv grpc_server_example build-cache/ - fi - - if [ ! -f "build-cache/proto/helloworld.proto" ]; then - if [ ! -f "grpc_server_example/main.go" ]; then - git clone https://github.com/iresty/grpc_server_example.git grpc_server_example - fi - - cd grpc_server_example/ - mv proto/ ../build-cache/ - cd .. - fi - - if [ ! -f "build-cache/grpcurl" ]; then - wget https://github.com/api7/grpcurl/releases/download/20200314/grpcurl-amd64.tar.gz - tar -xvf grpcurl-amd64.tar.gz - mv grpcurl build-cache/ - fi -} - -script() { - export_or_prefix - openresty -V - - ./build-cache/grpc_server_example & - - ./bin/apisix help - ./bin/apisix init - ./bin/apisix init_etcd - ./bin/apisix start - - #start again --> fial - res=`./bin/apisix start` - if [ "$res" != "APISIX is running..." ]; then - echo "failed: APISIX runs repeatedly" - exit 1 - fi - - #kill apisix - sudo kill -9 `ps aux | grep apisix | grep nginx | awk '{print $2}'` - - #start -> ok - res=`./bin/apisix start` - if [ "$res" == "APISIX is running..." ]; then - echo "failed: shouldn't stop APISIX running after kill the old process." - exit 1 - fi - - sleep 1 - cat logs/error.log - - sudo sh ./t/grpc-proxy-test.sh - sleep 1 - - ./bin/apisix stop - sleep 1 - - sudo bash ./utils/check-plugins-code.sh - - make lint && make license-check || exit 1 - # APISIX_ENABLE_LUACOV=1 PERL5LIB=.:$PERL5LIB prove -Itest-nginx/lib -r t - PERL5LIB=.:$PERL5LIB prove -Itest-nginx/lib -r t -} - -after_success() { - # cat luacov.stats.out - # luacov-coveralls - echo "done" -} - -case_opt=$1 -shift - -case ${case_opt} in -before_install) - before_install "$@" - ;; -do_install) - do_install "$@" - ;; -script) - script "$@" - ;; -after_success) - after_success "$@" - ;; -esac -======= export OPENRESTY_VERSION=source . ./.travis/linux_openresty_common_runner.sh ->>>>>>> master_upstream From 334d4e9775f6f95c6cf893a7bd2ebff6365b89eb Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Wed, 20 Jan 2021 15:50:11 +0100 Subject: [PATCH 77/94] Flip Keycloak image reference back to sshniro's repo. --- .travis/linux_openresty_common_runner.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis/linux_openresty_common_runner.sh b/.travis/linux_openresty_common_runner.sh index eb1cfb824f4b..128fb40305d8 100755 --- a/.travis/linux_openresty_common_runner.sh +++ b/.travis/linux_openresty_common_runner.sh @@ -24,7 +24,7 @@ before_install() { docker run --rm -itd -p 6379:6379 --name apisix_redis redis:3.0-alpine docker run --rm -itd -e HTTP_PORT=8888 -e HTTPS_PORT=9999 -p 8888:8888 -p 9999:9999 mendhak/http-https-echo # Runs Keycloak version 10.0.2 with inbuilt policies for unit tests - docker run --rm -itd -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=123456 -p 8090:8080 -p 8443:8443 jenskeiner/keycloak-apisix + docker run --rm -itd -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=123456 -p 8090:8080 -p 8443:8443 sshniro/keycloak-apisix:1.0.0 # spin up kafka cluster for tests (1 zookeper and 1 kafka instance) docker pull bitnami/zookeeper:3.6.0 docker pull bitnami/kafka:latest From d49dc53b65f70fafb39e52e86bec477ddaca8caa Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Wed, 20 Jan 2021 21:06:10 +0100 Subject: [PATCH 78/94] Change Docker repo back to sshniro's. --- .github/workflows/centos7-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/centos7-ci.yml b/.github/workflows/centos7-ci.yml index ae5e56683fbb..dc8e0da15f05 100644 --- a/.github/workflows/centos7-ci.yml +++ b/.github/workflows/centos7-ci.yml @@ -68,7 +68,7 @@ jobs: run: | docker run --rm -itd -p 6379:6379 --name apisix_redis redis:3.0-alpine docker run --rm -itd -e HTTP_PORT=8888 -e HTTPS_PORT=9999 -p 8888:8888 -p 9999:9999 mendhak/http-https-echo - docker run --rm -itd -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=123456 -p 8090:8080 -p 8443:8443 jenskeiner/keycloak-apisix + docker run --rm -itd -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=123456 -p 8090:8080 -p 8443:8443 sshniro/keycloak-apisix:1.0.0 docker network create kafka-net --driver bridge docker run --name zookeeper-server -d -p 2181:2181 --network kafka-net -e ALLOW_ANONYMOUS_LOGIN=yes bitnami/zookeeper:3.6.0 docker run --name kafka-server1 -d --network kafka-net -e ALLOW_PLAINTEXT_LISTENER=yes -e KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper-server:2181 -e KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092 -p 9092:9092 -e KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true bitnami/kafka:latest From 73d5e89e5b818b7b4f5971f957d3168977530df4 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Wed, 20 Jan 2021 21:37:46 +0100 Subject: [PATCH 79/94] Trivial change to kick off checks again. --- .github/workflows/centos7-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/centos7-ci.yml b/.github/workflows/centos7-ci.yml index dc8e0da15f05..f05a4b4b8cfa 100644 --- a/.github/workflows/centos7-ci.yml +++ b/.github/workflows/centos7-ci.yml @@ -68,7 +68,7 @@ jobs: run: | docker run --rm -itd -p 6379:6379 --name apisix_redis redis:3.0-alpine docker run --rm -itd -e HTTP_PORT=8888 -e HTTPS_PORT=9999 -p 8888:8888 -p 9999:9999 mendhak/http-https-echo - docker run --rm -itd -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=123456 -p 8090:8080 -p 8443:8443 sshniro/keycloak-apisix:1.0.0 + docker run --rm -itd -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=123456 -p 8090:8080 -p 8443:8443 jenskeiner/keycloak-apisix:1.0.0 docker network create kafka-net --driver bridge docker run --name zookeeper-server -d -p 2181:2181 --network kafka-net -e ALLOW_ANONYMOUS_LOGIN=yes bitnami/zookeeper:3.6.0 docker run --name kafka-server1 -d --network kafka-net -e ALLOW_PLAINTEXT_LISTENER=yes -e KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper-server:2181 -e KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092 -p 9092:9092 -e KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true bitnami/kafka:latest From fa72cc5c9c4a72fe59863630570d8d571f0307ef Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Wed, 20 Jan 2021 21:38:03 +0100 Subject: [PATCH 80/94] Trivial hange to kick off checks again. --- .github/workflows/centos7-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/centos7-ci.yml b/.github/workflows/centos7-ci.yml index f05a4b4b8cfa..dc8e0da15f05 100644 --- a/.github/workflows/centos7-ci.yml +++ b/.github/workflows/centos7-ci.yml @@ -68,7 +68,7 @@ jobs: run: | docker run --rm -itd -p 6379:6379 --name apisix_redis redis:3.0-alpine docker run --rm -itd -e HTTP_PORT=8888 -e HTTPS_PORT=9999 -p 8888:8888 -p 9999:9999 mendhak/http-https-echo - docker run --rm -itd -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=123456 -p 8090:8080 -p 8443:8443 jenskeiner/keycloak-apisix:1.0.0 + docker run --rm -itd -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=123456 -p 8090:8080 -p 8443:8443 sshniro/keycloak-apisix:1.0.0 docker network create kafka-net --driver bridge docker run --name zookeeper-server -d -p 2181:2181 --network kafka-net -e ALLOW_ANONYMOUS_LOGIN=yes bitnami/zookeeper:3.6.0 docker run --name kafka-server1 -d --network kafka-net -e ALLOW_PLAINTEXT_LISTENER=yes -e KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper-server:2181 -e KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092 -p 9092:9092 -e KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true bitnami/kafka:latest From 32cea9327c8352af897ca916e13524c77ab9e5ba Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Wed, 20 Jan 2021 22:14:54 +0100 Subject: [PATCH 81/94] Align comment indent. --- t/APISIX.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/t/APISIX.pm b/t/APISIX.pm index af650390e179..36218277e679 100644 --- a/t/APISIX.pm +++ b/t/APISIX.pm @@ -296,8 +296,8 @@ _EOC_ lua_shared_dict balancer_ewma_last_touched_at 1m; lua_shared_dict plugin-limit-count-redis-cluster-slot-lock 1m; lua_shared_dict tracing_buffer 10m; # plugin skywalking - lua_shared_dict access_tokens 1m; # plugin authz-keycloak - lua_shared_dict discovery 1m; # plugin authz-keycloak + lua_shared_dict access_tokens 1m; # plugin authz-keycloak + lua_shared_dict discovery 1m; # plugin authz-keycloak lua_shared_dict plugin-api-breaker 10m; lua_capture_error_log 1m; # plugin error-log-logger From ab555697bf7c203de476e6e36b93ec8a542cfea6 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Thu, 21 Jan 2021 09:19:32 +0100 Subject: [PATCH 82/94] Add documentation for cache_ttl_seconds attribute. --- doc/plugins/authz-keycloak.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/plugins/authz-keycloak.md b/doc/plugins/authz-keycloak.md index 75e5a4e461b2..19bbfa205c49 100644 --- a/doc/plugins/authz-keycloak.md +++ b/doc/plugins/authz-keycloak.md @@ -52,6 +52,7 @@ For more information on Keycloak, refer to [Keycloak Authorization Docs](https:/ | http_method_as_scope | boolean | optional | false | | Map HTTP request type to scope of same name and add to all permissions requested. | | timeout | integer | optional | 3000 | [1000, ...] | Timeout(ms) for the http connection with the Identity Server. | | ssl_verify | boolean | optional | true | | Verify if SSL cert matches hostname. | +| cache_ttl_seconds | integer | optional | 24 * 60 * 60, i.e. 24h | positive integer >= 1 | The maximum period in seconds up to which the plugin caches discovery documents and tokens, used by the plugin to authenticate to Keycloak. | ### Discovery and Endpoints From 59dccc90c21763b1608efdd0fe67ac898a6fb280 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Thu, 21 Jan 2021 09:22:14 +0100 Subject: [PATCH 83/94] Fix incorrect usage of boolean value. --- apisix/plugins/authz-keycloak.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index 047af2c91e28..abf66f0a77c0 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -219,7 +219,7 @@ local function authz_keycloak_discover(url, ssl_verify, keepalive, timeout, authz_keycloak_configure_proxy(httpc, proxy_opts) local res, error = httpc:request_uri(url, decorate_request(http_request_decorator, { ssl_verify = (ssl_verify ~= "no"), - keepalive = (keepalive ~= "no") + keepalive = keepalive })) if not res then err = "Accessing discovery url (" .. url .. ") failed: " .. error From 5ca93a52a9940d9a074833163599c02898bdb617 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Thu, 21 Jan 2021 10:34:58 +0100 Subject: [PATCH 84/94] Temporarily disable some unit tests to speed up checks. --- .travis/linux_openresty_common_runner.sh | 2 +- utils/centos7-ci.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis/linux_openresty_common_runner.sh b/.travis/linux_openresty_common_runner.sh index 128fb40305d8..ef49f63f9098 100755 --- a/.travis/linux_openresty_common_runner.sh +++ b/.travis/linux_openresty_common_runner.sh @@ -139,7 +139,7 @@ script() { make lint && make license-check || exit 1 # APISIX_ENABLE_LUACOV=1 PERL5LIB=.:$PERL5LIB prove -Itest-nginx/lib -r t - PERL5LIB=.:$PERL5LIB prove -Itest-nginx/lib -r t + PERL5LIB=.:$PERL5LIB prove -Itest-nginx/lib -r t/plugin/authz-keycloak.t } after_success() { diff --git a/utils/centos7-ci.sh b/utils/centos7-ci.sh index 7395518ff5d8..28c4dd427256 100755 --- a/utils/centos7-ci.sh +++ b/utils/centos7-ci.sh @@ -71,7 +71,7 @@ run_case() { cd apisix # run test cases - prove -Itest-nginx/lib -I./ -r t/ + prove -Itest-nginx/lib -I./ -r t/plugin/authz-keycloak.t } case_opt=$1 From 450ce05641b4e9c2cf6451e5a49af9c81925229f Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Thu, 21 Jan 2021 10:35:21 +0100 Subject: [PATCH 85/94] Cleanup documentation, JSON schema, and HTTP handling. --- apisix/plugins/authz-keycloak.lua | 286 ++++++++++++++++-------------- doc/plugins/authz-keycloak.md | 22 ++- t/plugin/authz-keycloak.t | 117 ++++++++---- 3 files changed, 251 insertions(+), 174 deletions(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index abf66f0a77c0..89045de278a4 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -30,37 +30,37 @@ local schema = { discovery = {type = "string", minLength = 1, maxLength = 4096}, token_endpoint = {type = "string", minLength = 1, maxLength = 4096}, resource_registration_endpoint = {type = "string", minLength = 1, maxLength = 4096}, - permissions = { - type = "array", - items = { - type = "string", - minLength = 1, maxLength = 100 - }, - uniqueItems = true - }, + client_id = {type = "string", minLength = 1, maxLength = 100}, + audience = {type = "string", minLength = 1, maxLength = 100, + description = "Deprecated, use `client_id` instead."}, + client_secret = {type = "string", minLength = 1, maxLength = 100}, grant_type = { type = "string", default="urn:ietf:params:oauth:grant-type:uma-ticket", enum = {"urn:ietf:params:oauth:grant-type:uma-ticket"}, minLength = 1, maxLength = 100 }, - timeout = {type = "integer", minimum = 1000, default = 3000}, policy_enforcement_mode = { type = "string", enum = {"ENFORCING", "PERMISSIVE"}, default = "ENFORCING" }, - keepalive = {type = "boolean", default = true}, - keepalive_timeout = {type = "integer", minimum = 1000, default = 60000}, - keepalive_pool = {type = "integer", minimum = 1, default = 5}, - ssl_verify = {type = "boolean", default = true}, - client_id = {type = "string", minLength = 1, maxLength = 100}, - audience = {type = "string", minLength = 1, maxLength = 100, - description = "Deprecated, use `client_id` instead."}, - client_secret = {type = "string", minLength = 1, maxLength = 100}, + permissions = { + type = "array", + items = { + type = "string", + minLength = 1, maxLength = 100 + }, + uniqueItems = true + }, lazy_load_paths = {type = "boolean", default = false}, http_method_as_scope = {type = "boolean", default = false}, + timeout = {type = "integer", minimum = 1000, default = 3000}, + ssl_verify = {type = "boolean", default = true}, cache_ttl_seconds = {type = "integer", minimum = 1, default = 24 * 60 * 60} + keepalive = {type = "boolean", default = true}, + keepalive_timeout = {type = "integer", minimum = 1000, default = 60000}, + keepalive_pool = {type = "integer", minimum = 1, default = 5}, }, allOf = { -- Require discovery or token endpoint. @@ -70,6 +70,13 @@ local schema = { {required = {"token_endpoint"}} } }, + -- Require client_id or audience. + { + anyOf = { + {required = {"client_id"}}, + {required = {"audience"}} + } + }, -- If lazy_load_paths is true, require discovery or resource registration endpoint. { anyOf = { @@ -130,132 +137,151 @@ end -- Retrieve value from server-wide cache, if available. local function authz_keycloak_cache_get(type, key) - local dict = ngx.shared[type] - local value - if dict then - value = dict:get(key) - if value then log.debug("cache hit: type=", type, " key=", key) end - end - return value + local dict = ngx.shared[type] + local value + if dict then + value = dict:get(key) + if value then log.debug("cache hit: type=", type, " key=", key) end + end + return value end -- Set value in server-wide cache, if available. local function authz_keycloak_cache_set(type, key, value, exp) - local dict = ngx.shared[type] - if dict and (exp > 0) then - local success, err, forcible = dict:set(key, value, exp) - if err then - log.error("cache set: success=", success, " err=", err, " forcible=", forcible) + local dict = ngx.shared[type] + if dict and (exp > 0) then + local success, err, forcible = dict:set(key, value, exp) + if err then + log.error("cache set: success=", success, " err=", err, " forcible=", forcible) + else + log.debug("cache set: success=", success, " err=", err, " forcible=", forcible) + end + end +end + + +-- Configure request parameters. +local function authz_keycloak_configure_params(params, conf): + -- Keepalive options. + if conf.keepalive then + params.keepalive_timeout = conf.keepalive_timeout + params.keepalive_pool = conf.keepalive_pool else - log.debug("cache set: success=", success, " err=", err, " forcible=", forcible) + params.keepalive = conf.keepalive end - end + + -- TLS verification. + params.ssl_verify = conf.ssl_verify + + -- Decorate parameters, maybe, and return. + return conf.http_request_decorator and conf.http_request_decorator(params) or params end -- Configure timeouts. local function authz_keycloak_configure_timeouts(httpc, timeout) - if timeout then - if type(timeout) == "table" then - httpc:set_timeouts(timeout.connect or 0, timeout.send or 0, timeout.read or 0) - else - httpc:set_timeout(timeout) + if timeout then + if type(timeout) == "table" then + httpc:set_timeouts(timeout.connect or 0, timeout.send or 0, timeout.read or 0) + else + httpc:set_timeout(timeout) + end end - end end -- Set outgoing proxy options. local function authz_keycloak_configure_proxy(httpc, proxy_opts) - if httpc and proxy_opts and type(proxy_opts) == "table" then - log.debug("authz_keycloak_configure_proxy : use http proxy") - httpc:set_proxy_options(proxy_opts) - else - log.debug("authz_keycloak_configure_proxy : don't use http proxy") - end + if httpc and proxy_opts and type(proxy_opts) == "table" then + log.debug("authz_keycloak_configure_proxy : use http proxy") + httpc:set_proxy_options(proxy_opts) + else + log.debug("authz_keycloak_configure_proxy : don't use http proxy") + end +end + + +-- Get and configure HTTP client. +local function authz_keycloak_get_http_client(conf) + local httpc = http.new() + authz_keycloak_configure_timeouts(httpc, conf.timeout) + authz_keycloak_configure_proxy(httpc, conf.proxy_opts) + return httpc end -- Parse the JSON result from a call to the OP. local function authz_keycloak_parse_json_response(response) - local err - local res + local err + local res - -- Check the response from the OP. - if response.status ~= 200 then - err = "response indicates failure, status=" .. response.status .. ", body=" .. response.body - else - -- Decode the response and extract the JSON object. - res, err = core.json.decode(response.body) + -- Check the response from the OP. + if response.status ~= 200 then + err = "response indicates failure, status=" .. response.status .. ", body=" .. response.body + else + -- Decode the response and extract the JSON object. + res, err = core.json.decode(response.body) - if not res then - err = "JSON decoding failed: " .. err + if not res then + err = "JSON decoding failed: " .. err + end end - end - return res, err + return res, err end -local function decorate_request(http_request_decorator, req) - return http_request_decorator and http_request_decorator(req) or req -end +-- Get the Discovery metadata from the specified URL. +local function authz_keycloak_discover(conf): + log.debug("authz_keycloak_discover: URL is: " .. conf.discovery) + local json, err + local v = authz_keycloak_cache_get("discovery", conf.discovery) --- Get the Discovery metadata from the specified URL. -local function authz_keycloak_discover(url, ssl_verify, keepalive, timeout, - exptime, proxy_opts, http_request_decorator) - log.debug("authz_keycloak_discover: URL is: " .. url) + if not v then + log.debug("Discovery data not in cache, making call to discovery endpoint.") - local json, err - local v = authz_keycloak_cache_get("discovery", url) - if not v then + -- Make the call to the discovery endpoint. + local httpc = authz_keycloak_get_http_client(conf) - log.debug("Discovery data not in cache, making call to discovery endpoint.") - -- Make the call to the discovery endpoint. - local httpc = http.new() - authz_keycloak_configure_timeouts(httpc, timeout) - authz_keycloak_configure_proxy(httpc, proxy_opts) - local res, error = httpc:request_uri(url, decorate_request(http_request_decorator, { - ssl_verify = (ssl_verify ~= "no"), - keepalive = keepalive - })) - if not res then - err = "Accessing discovery url (" .. url .. ") failed: " .. error - log.error(err) + local params = authz_keycloak_configure_params({}, conf) + + local res, error = httpc:request_uri(conf.discovery, params) + + if not res then + err = "Accessing discovery URL (" .. conf.discovery .. ") failed: " .. error + log.error(err) + else + log.debug("Response data: " .. res.body) + json, err = authz_keycloak_parse_json_response(res) + if json then + authz_keycloak_cache_set("discovery", conf.discovery, core.json.encode(json), + conf.cache_ttl_seconds) + else + err = "could not decode JSON from Discovery data" .. (err and (": " .. err) or '') + log.error(err) + end + end else - log.debug("Response data: " .. res.body) - json, err = authz_keycloak_parse_json_response(res) - if json then - authz_keycloak_cache_set("discovery", url, core.json.encode(json), exptime) - else - err = "could not decode JSON from Discovery data" .. (err and (": " .. err) or '') - log.error(err) - end + json = core.json.decode(v) end - else - json = core.json.decode(v) - end - - return json, err + return json, err end --- Turn a discovery url set in the opts dictionary into the discovered information. -local function authz_keycloak_ensure_discovered_data(opts) - local err - if type(opts.discovery) == "string" then - local discovery - discovery, err = authz_keycloak_discover(opts.discovery, opts.ssl_verify, opts.keepalive, - opts.timeout, opts.cache_ttl_seconds, - opts.proxy_opts, opts.http_request_decorator) - if not err then - opts.discovery = discovery +-- Turn a discovery url set in the conf dictionary into the discovered information. +local function authz_keycloak_ensure_discovered_data(conf) + local err + if type(conf.discovery) == "string" then + local discovery + discovery, err = authz_keycloak_discover(conf) + if not err then + conf.discovery = discovery + end end - end - return err + return err end @@ -281,16 +307,16 @@ end -- computes access_token expires_in value (in seconds) -local function authz_keycloak_access_token_expires_in(opts, expires_in) - return (expires_in or opts.access_token_expires_in or 300) - - 1 - (opts.access_token_expires_leeway or 0) +local function authz_keycloak_access_token_expires_in(conf, expires_in) + return (expires_in or conf.access_token_expires_in or 300) + - 1 - (conf.access_token_expires_leeway or 0) end -- computes refresh_token expires_in value (in seconds) -local function authz_keycloak_refresh_token_expires_in(opts, expires_in) - return (expires_in or opts.refresh_token_expires_in or 3600) - - 1 - (opts.refresh_token_expires_leeway or 0) +local function authz_keycloak_refresh_token_expires_in(conf, expires_in) + return (expires_in or conf.refresh_token_expires_in or 3600) + - 1 - (conf.refresh_token_expires_leeway or 0) end @@ -300,8 +326,8 @@ local function authz_keycloak_ensure_sa_access_token(conf) local token_endpoint = authz_keycloak_get_token_endpoint(conf) if not token_endpoint then - log.error("Unable to determine token endpoint.") - return 500, "Unable to determine token endpoint." + log.error("Unable to determine token endpoint.") + return 500, "Unable to determine token endpoint." end local session = authz_keycloak_cache_get("access_tokens", token_endpoint .. ":" @@ -332,8 +358,7 @@ local function authz_keycloak_ensure_sa_access_token(conf) -- Try to get a new access token, using the refresh token. log.debug("Trying to get new access token using refresh token.") - local httpc = http.new() - httpc:set_timeout(conf.timeout) + local httpc = authz_keycloak_get_http_client(conf) local params = { method = "POST", @@ -343,12 +368,13 @@ local function authz_keycloak_ensure_sa_access_token(conf) client_secret = conf.client_secret, refresh_token = session.refresh_token, }), - ssl_verify = conf.ssl_verify, headers = { ["Content-Type"] = "application/x-www-form-urlencoded" } } + params = authz_keycloak_configure_params(params, conf) + local res, err = httpc:request_uri(token_endpoint, params) if not res then @@ -408,8 +434,7 @@ local function authz_keycloak_ensure_sa_access_token(conf) -- No session available. Create a new one. core.log.debug("Getting access token for Protection API from token endpoint.") - local httpc = http.new() - httpc:set_timeout(conf.timeout) + local httpc = authz_keycloak_get_http_client(conf) local params = { method = "POST", @@ -418,15 +443,16 @@ local function authz_keycloak_ensure_sa_access_token(conf) client_id = client_id, client_secret = conf.client_secret, }), - ssl_verify = conf.ssl_verify, headers = { ["Content-Type"] = "application/x-www-form-urlencoded" } } + params = authz_keycloak_configure_params(params, conf) + local current_time = ngx.time() - local res, err = httpc:request_uri(token_endpoint, params) + local res, err = httpc:request_uri(token_endpoint, conf.http_request_decorator, params) if not res then err = "Accessing token endpoint URL (" .. token_endpoint .. ") failed: " .. err @@ -487,24 +513,17 @@ local function authz_keycloak_resolve_permission(conf, uri, sa_access_token) log.debug("Resource registration endpoint: ", resource_registration_endpoint) - local httpc = http.new() - httpc:set_timeout(conf.timeout) + local httpc = authz_keycloak_get_http_client(conf) local params = { method = "GET", query = {uri = uri, matchingUri = "true"}, - ssl_verify = conf.ssl_verify, headers = { ["Authorization"] = "Bearer " .. sa_access_token } } - if conf.keepalive then - params.keepalive_timeout = conf.keepalive_timeout - params.keepalive_pool = conf.keepalive_pool - else - params.keepalive = conf.keepalive - end + params = authz_keycloak_configure_params(params, conf) local res, err = httpc:request_uri(resource_registration_endpoint, params) @@ -534,7 +553,7 @@ local function evaluate_permissions(conf, ctx, token) -- Ensure discovered data. local err = authz_keycloak_ensure_discovered_data(conf) if err then - return 500, err + return 500, err end local permission @@ -543,7 +562,7 @@ local function evaluate_permissions(conf, ctx, token) -- Ensure service account access token. local sa_access_token, err = authz_keycloak_ensure_sa_access_token(conf) if err then - return 500, err + return 500, err end -- Resolve URI to resource(s). @@ -598,8 +617,7 @@ local function evaluate_permissions(conf, ctx, token) end log.debug("Token endpoint: ", token_endpoint) - local httpc = http.new() - httpc:set_timeout(conf.timeout) + local httpc = authz_keycloak_get_http_client(conf) local params = { method = "POST", @@ -609,19 +627,13 @@ local function evaluate_permissions(conf, ctx, token) response_mode = "decision", permission = permission }), - ssl_verify = conf.ssl_verify, headers = { ["Content-Type"] = "application/x-www-form-urlencoded", ["Authorization"] = token } } - if conf.keepalive then - params.keepalive_timeout = conf.keepalive_timeout - params.keepalive_pool = conf.keepalive_pool - else - params.keepalive = conf.keepalive - end + params = authz_keycloak_configure_params(params, conf) local res, err = httpc:request_uri(token_endpoint, params) diff --git a/doc/plugins/authz-keycloak.md b/doc/plugins/authz-keycloak.md index 19bbfa205c49..436e3a8d42fa 100644 --- a/doc/plugins/authz-keycloak.md +++ b/doc/plugins/authz-keycloak.md @@ -43,16 +43,20 @@ For more information on Keycloak, refer to [Keycloak Authorization Docs](https:/ | discovery | string | optional | | https://host.domain/auth/realms/foo/.well-known/uma2-configuration | URL to discovery document for Keycloak Authorization Services. | | token_endpoint | string | optional | | https://host.domain/auth/realms/foo/protocol/openid-connect/token | A OAuth2-compliant Token Endpoint that supports the `urn:ietf:params:oauth:grant-type:uma-ticket` grant type. Overrides value from discovery, if given. | | resource_registration_endpoint | string | optional | | https://host.domain/auth/realms/foo/authz/protection/resource_set | A Keycloak Protection API-compliant resource registration endpoint. Overrides value from discovery, if given. | -| grant_type | string | optional | "urn:ietf:params:oauth:grant-type:uma-ticket" | ["urn:ietf:params:oauth:grant-type:uma-ticket"] | | -| client_id | string | required | | | The client identifier of the resource server to which the client is seeking access.
This parameter is mandatory when parameter permission is defined. | +| client_id | string | optional | | | The client identifier of the resource server to which the client is seeking access. One of `client_id` or `audience` is required. | +| audience | string | optional | | | Legacy parameter now replaced by `client_id`. Kept for backwards compatibility. One of `client_id` or `audience` is required. | | client_secret | string | optional | | | The client secret, if required. | +| grant_type | string | optional | "urn:ietf:params:oauth:grant-type:uma-ticket" | ["urn:ietf:params:oauth:grant-type:uma-ticket"] | | | policy_enforcement_mode | string | optional | "ENFORCING" | ["ENFORCING", "PERMISSIVE"] | | | permissions | array[string] | optional | | | Static permission to request, an array of strings each representing a resources and optionally one or more scopes the client is seeking access. | | lazy_load_paths | boolean | optional | false | | Dynamically resolve the request URI to resource(s) using the resource registration endpoint instead of using the static permission. | | http_method_as_scope | boolean | optional | false | | Map HTTP request type to scope of same name and add to all permissions requested. | | timeout | integer | optional | 3000 | [1000, ...] | Timeout(ms) for the http connection with the Identity Server. | -| ssl_verify | boolean | optional | true | | Verify if SSL cert matches hostname. | +| ssl_verify | boolean | optional | true | | Verify if TLS certificate matches hostname. | | cache_ttl_seconds | integer | optional | 24 * 60 * 60, i.e. 24h | positive integer >= 1 | The maximum period in seconds up to which the plugin caches discovery documents and tokens, used by the plugin to authenticate to Keycloak. | +| keepalive | boolean | optional | true | | Enable HTTP keep-alive to keep connections open after use. Set to `true` if you expect a lot of requests to Keycloak. | +| keepalive_timeout | integer | optional | 60000 | positive integer >= 1000 | Idle timeout after which established HTTP connections will be closed. | +| keepalive_pool | integer | optional | 5 | positive integer >= 1 | Maximum number of connections in the connection pool. | ### Discovery and Endpoints @@ -68,8 +72,16 @@ Analogously, the plugin determines the registration endpoint from the discovery ### Client ID and Secret -The `client_id` attribute is needed to identify the plugin when interacting with Keycloak. -If the access is confidential, the `client_secret` attribute needs to contain the client secret. +The plugin needs the `client_id` attribute to identify itself when interacting with Keycloak. +For backwards compatibility, you can still use the `audience` attribute as well instead. The plugin +prefers `client_id` over `audience` if both are configured. + +The plugin always needs the `client_id` or `audience` to specify the context in which Keycloak +should evaluate permissions. + +If `lazy_load_paths` is `true` then the plugin additionally needs to obtain an access token for +itself from Keycloak. In this case, if the client access to Keycloak is confidential, the plugin +needs the `client_secret` attribute as well. ### Policy Enforcement Mode diff --git a/t/plugin/authz-keycloak.t b/t/plugin/authz-keycloak.t index 607758f3623b..e1e0be086f94 100644 --- a/t/plugin/authz-keycloak.t +++ b/t/plugin/authz-keycloak.t @@ -30,6 +30,7 @@ __DATA__ content_by_lua_block { local plugin = require("apisix.plugins.authz-keycloak") local ok, err = plugin.check_schema({ + client_id = "foo", token_endpoint = "https://host.domain/auth/realms/foo/protocol/openid-connect/token" }) if not ok then @@ -54,6 +55,7 @@ done content_by_lua_block { local plugin = require("apisix.plugins.authz-keycloak") local ok, err = plugin.check_schema({ + client_id = "foo", discovery = "https://host.domain/auth/realms/foo/.well-known/uma2-configuration" }) if not ok then @@ -72,12 +74,38 @@ done -=== TEST 3: minimal valid configuration w/o discovery when lazy_load_paths=true +=== TEST 3: minimal valid configuration with audience --- config location /t { content_by_lua_block { local plugin = require("apisix.plugins.authz-keycloak") local ok, err = plugin.check_schema({ + audience = "foo", + discovery = "https://host.domain/auth/realms/foo/.well-known/uma2-configuration" + }) + if not ok then + ngx.say(err) + end + + ngx.say("done") + } + } +--- request +GET /t +--- response_body +done +--- no_error_log +[error] + + + +=== TEST 4: minimal valid configuration w/o discovery when lazy_load_paths=true +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.authz-keycloak") + local ok, err = plugin.check_schema({ + client_id = "foo", lazy_load_paths = true, token_endpoint = "https://host.domain/auth/realms/foo/protocol/openid-connect/token", resource_registration_endpoint = "https://host.domain/auth/realms/foo/authz/protection/resource_set" @@ -98,12 +126,13 @@ done -=== TEST 4: minimal valid configuration with discovery when lazy_load_paths=true +=== TEST 5: minimal valid configuration with discovery when lazy_load_paths=true --- config location /t { content_by_lua_block { local plugin = require("apisix.plugins.authz-keycloak") local ok, err = plugin.check_schema({ + client_id = "foo", lazy_load_paths = true, discovery = "https://host.domain/auth/realms/foo/.well-known/uma2-configuration" }) @@ -123,7 +152,7 @@ done -=== TEST 5: full schema check +=== TEST 6: full schema check --- config location /t { content_by_lua_block { @@ -132,20 +161,20 @@ done discovery = "https://host.domain/auth/realms/foo/.well-known/uma2-configuration", token_endpoint = "https://host.domain/auth/realms/foo/protocol/openid-connect/token", resource_registration_endpoint = "https://host.domain/auth/realms/foo/authz/protection/resource_set", - permissions = {"res:customer#scopes:view"}, - grant_type = "urn:ietf:params:oauth:grant-type:uma-ticket", - timeout = 1000, - policy_enforcement_mode = "ENFORCING", - keepalive = true, - keepalive_timeout = 10000, - keepalive_pool = 5, - ssl_verify = false, client_id = "University", audience = "University", client_secret = "secret", + grant_type = "urn:ietf:params:oauth:grant-type:uma-ticket", + policy_enforcement_mode = "ENFORCING", + permissions = {"res:customer#scopes:view"}, lazy_load_paths = false, http_method_as_scope = false, + timeout = 1000, + ssl_verify = false, cache_ttl_seconds = 1000 + keepalive = true, + keepalive_timeout = 10000, + keepalive_pool = 5 }) if not ok then ngx.say(err) @@ -163,12 +192,12 @@ done -=== TEST 6: token_endpoint and discovery both missing +=== TEST 7: token_endpoint and discovery both missing --- config location /t { content_by_lua_block { local plugin = require("apisix.plugins.authz-keycloak") - local ok, err = plugin.check_schema({}) + local ok, err = plugin.check_schema({client_id = "foo"}) if not ok then ngx.say(err) end @@ -186,12 +215,36 @@ done -=== TEST 7: resource_registration_endpoint and discovery both missing and lazy_load_paths is true +=== TEST 8: client_id and audience both missing +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.authz-keycloak") + local ok, err = plugin.check_schema({discovery = "https://host.domain/auth/realms/foo/.well-known/uma2-configuration"}) + if not ok then + ngx.say(err) + end + + ngx.say("done") + } + } +--- request +GET /t +--- response_body +allOf 2 failed: object matches none of the requireds: ["client_id"] or ["audience"] +done +--- no_error_log +[error] + + + +=== TEST 9: resource_registration_endpoint and discovery both missing and lazy_load_paths is true --- config location /t { content_by_lua_block { local plugin = require("apisix.plugins.authz-keycloak") local ok, err = plugin.check_schema({ + client_id = "foo", token_endpoint = "https://host.domain/auth/realms/foo/protocol/openid-connect/token", lazy_load_paths = true }) @@ -205,14 +258,14 @@ done --- request GET /t --- response_body -allOf 2 failed: object matches none of the requireds +allOf 3 failed: object matches none of the requireds done --- no_error_log [error] -=== TEST 8: add plugin with view course permissions (using token endpoint) +=== TEST 10: add plugin with view course permissions (using token endpoint) --- config location /t { content_by_lua_block { @@ -278,7 +331,7 @@ passed -=== TEST 9: Get access token for teacher and access view course route +=== TEST 11: Get access token for teacher and access view course route --- config location /t { content_by_lua_block { @@ -326,7 +379,7 @@ true -=== TEST 10: invalid access token +=== TEST 12: invalid access token --- config location /t { content_by_lua_block { @@ -353,7 +406,7 @@ Invalid bearer token -=== TEST 11: add plugin with view course permissions (using discovery) +=== TEST 13: add plugin with view course permissions (using discovery) --- config location /t { content_by_lua_block { @@ -419,7 +472,7 @@ passed -=== TEST 12: Get access token for teacher and access view course route +=== TEST 14: Get access token for teacher and access view course route --- config location /t { content_by_lua_block { @@ -467,7 +520,7 @@ true -=== TEST 13: invalid access token +=== TEST 15: invalid access token --- config location /t { content_by_lua_block { @@ -494,7 +547,7 @@ Invalid bearer token -=== TEST 14: add plugin for delete course route +=== TEST 16: add plugin for delete course route --- config location /t { content_by_lua_block { @@ -560,7 +613,7 @@ passed -=== TEST 15: Get access token for student and delete course +=== TEST 17: Get access token for student and delete course --- config location /t { content_by_lua_block { @@ -608,7 +661,7 @@ true -=== TEST 16: Add https endpoint with ssl_verify true (default) +=== TEST 18: Add https endpoint with ssl_verify true (default) --- config location /t { content_by_lua_block { @@ -674,7 +727,7 @@ passed -=== TEST 17: TEST with fake token and https endpoint +=== TEST 19: TEST with fake token and https endpoint --- config location /t { content_by_lua_block { @@ -704,7 +757,7 @@ Error while sending authz request to https://127.0.0.1:8443/auth/realms/Universi -=== TEST 18: Add htttps endpoint with ssl_verify false +=== TEST 20: Add htttps endpoint with ssl_verify false --- config location /t { content_by_lua_block { @@ -772,7 +825,7 @@ passed -=== TEST 19: TEST for https based token verification with ssl_verify false +=== TEST 21: TEST for https based token verification with ssl_verify false --- config location /t { content_by_lua_block { @@ -802,7 +855,7 @@ Request denied: HTTP 401 Unauthorized. Body: {"error":"HTTP 401 Unauthorized"} -=== TEST 20: add plugin with lazy_load_paths and http_method_as_scope +=== TEST 22: add plugin with lazy_load_paths and http_method_as_scope --- config location /t { content_by_lua_block { @@ -868,7 +921,7 @@ passed -=== TEST 21: Get access token for teacher and access view course route. +=== TEST 23: Get access token for teacher and access view course route. --- config location /t { content_by_lua_block { @@ -916,7 +969,7 @@ true -=== TEST 22: Get access token for student and access view course route. +=== TEST 24: Get access token for student and access view course route. --- config location /t { content_by_lua_block { @@ -964,7 +1017,7 @@ true -=== TEST 23: Get access token for teacher and delete course. +=== TEST 25: Get access token for teacher and delete course. --- config location /t { content_by_lua_block { @@ -1012,7 +1065,7 @@ true -=== TEST 24: Get access token for student and try to delete course. Should fail. +=== TEST 26: Get access token for student and try to delete course. Should fail. --- config location /t { content_by_lua_block { From e08224b1b43be5767124d1b0f77c27636ae3a9bd Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Thu, 21 Jan 2021 10:58:20 +0100 Subject: [PATCH 86/94] Fix syntax error. --- apisix/plugins/authz-keycloak.lua | 4 +- t/plugin/authz-keycloak.t | 162 ++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+), 2 deletions(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index 89045de278a4..d4148cae5e47 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -57,10 +57,10 @@ local schema = { http_method_as_scope = {type = "boolean", default = false}, timeout = {type = "integer", minimum = 1000, default = 3000}, ssl_verify = {type = "boolean", default = true}, - cache_ttl_seconds = {type = "integer", minimum = 1, default = 24 * 60 * 60} + cache_ttl_seconds = {type = "integer", minimum = 1, default = 24 * 60 * 60}, keepalive = {type = "boolean", default = true}, keepalive_timeout = {type = "integer", minimum = 1000, default = 60000}, - keepalive_pool = {type = "integer", minimum = 1, default = 5}, + keepalive_pool = {type = "integer", minimum = 1, default = 5} }, allOf = { -- Require discovery or token endpoint. diff --git a/t/plugin/authz-keycloak.t b/t/plugin/authz-keycloak.t index e1e0be086f94..6535f482dcbc 100644 --- a/t/plugin/authz-keycloak.t +++ b/t/plugin/authz-keycloak.t @@ -1110,3 +1110,165 @@ GET /t true --- error_log {"error":"access_denied","error_description":"not_authorized"} + + + +=== TEST 27: add plugin with lazy_load_paths and http_method_as_scope (using audience) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "authz-keycloak": { + "discovery": "http://127.0.0.1:8090/auth/realms/University/.well-known/uma2-configuration", + "audience": "course_management", + "client_secret": "d1ec69e9-55d2-4109-a3ea-befa071579d5", + "lazy_load_paths": true, + "http_method_as_scope": true + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "uri": "/course/foo" + }]], + [[{ + "node": { + "value": { + "plugins": { + "authz-keycloak": { + "discovery": "http://127.0.0.1:8090/auth/realms/University/.well-known/uma2-configuration", + "audience": "course_management", + "client_secret": "d1ec69e9-55d2-4109-a3ea-befa071579d5", + "lazy_load_paths": true, + "http_method_as_scope": true + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "uri": "/course/foo" + }, + "key": "/apisix/routes/1" + }, + "action": "set" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 28: Get access token for teacher and access view course route. +--- config + location /t { + content_by_lua_block { + local json_decode = require("toolkit.json").decode + local http = require "resty.http" + local httpc = http.new() + local uri = "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token" + local res, err = httpc:request_uri(uri, { + method = "POST", + body = "grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=teacher@gmail.com&password=123456", + headers = { + ["Content-Type"] = "application/x-www-form-urlencoded" + } + }) + + if res.status == 200 then + local body = json_decode(res.body) + local accessToken = body["access_token"] + + + uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/course/foo" + local res, err = httpc:request_uri(uri, { + method = "GET", + headers = { + ["Authorization"] = "Bearer " .. accessToken, + } + }) + + if res.status == 200 then + ngx.say(true) + else + ngx.say(false) + end + else + ngx.say(false) + end + } + } +--- request +GET /t +--- response_body +true +--- no_error_log +[error] + + + +=== TEST 29: Get access token for student and access view course route. +--- config + location /t { + content_by_lua_block { + local json_decode = require("toolkit.json").decode + local http = require "resty.http" + local httpc = http.new() + local uri = "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token" + local res, err = httpc:request_uri(uri, { + method = "POST", + body = "grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=student@gmail.com&password=123456", + headers = { + ["Content-Type"] = "application/x-www-form-urlencoded" + } + }) + + if res.status == 200 then + local body = json_decode(res.body) + local accessToken = body["access_token"] + + + uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/course/foo" + local res, err = httpc:request_uri(uri, { + method = "GET", + headers = { + ["Authorization"] = "Bearer " .. accessToken, + } + }) + + if res.status == 200 then + ngx.say(true) + else + ngx.say(false) + end + else + ngx.say(false) + end + } + } +--- request +GET /t +--- response_body +true +--- no_error_log +[error] From f2eabeeff847817dd3fb0e3cead5ce0d9510ede4 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Thu, 21 Jan 2021 11:12:16 +0100 Subject: [PATCH 87/94] Fix syntax error. --- apisix/plugins/authz-keycloak.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index d4148cae5e47..548bc5ca67dc 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -162,7 +162,7 @@ end -- Configure request parameters. -local function authz_keycloak_configure_params(params, conf): +local function authz_keycloak_configure_params(params, conf) -- Keepalive options. if conf.keepalive then params.keepalive_timeout = conf.keepalive_timeout From ce2eb72badf85e6b3714de1b57d7ebdfa2005a99 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Thu, 21 Jan 2021 11:31:35 +0100 Subject: [PATCH 88/94] Cleanup. --- apisix/plugins/authz-keycloak.lua | 18 +++++++++++++----- doc/plugins/authz-keycloak.md | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index 548bc5ca67dc..5ba7710d252f 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -285,41 +285,48 @@ local function authz_keycloak_ensure_discovered_data(conf) end +-- Get an endpoint from the configuration. local function authz_keycloak_get_endpoint(conf, endpoint) if conf and conf[endpoint] then + -- Use explicit entry. return conf[endpoint] elseif conf and conf.discovery and type(conf.discovery) == "table" then + -- Use discovery data. return conf.discovery[endpoint] end + -- Unable to obtain endpoint. return nil end +-- Return the token endpoint from the configuration. local function authz_keycloak_get_token_endpoint(conf) return authz_keycloak_get_endpoint(conf, "token_endpoint") end +-- Return the resource registration endpoint from the configuration. local function authz_keycloak_get_resource_registration_endpoint(conf) return authz_keycloak_get_endpoint(conf, "resource_registration_endpoint") end --- computes access_token expires_in value (in seconds) +-- Return access_token expires_in value (in seconds). local function authz_keycloak_access_token_expires_in(conf, expires_in) return (expires_in or conf.access_token_expires_in or 300) - 1 - (conf.access_token_expires_leeway or 0) end --- computes refresh_token expires_in value (in seconds) +-- Return refresh_token expires_in value (in seconds). local function authz_keycloak_refresh_token_expires_in(conf, expires_in) return (expires_in or conf.refresh_token_expires_in or 3600) - 1 - (conf.refresh_token_expires_leeway or 0) end +-- Ensure a valid service account access token is available for the configured client. local function authz_keycloak_ensure_sa_access_token(conf) local client_id = authz_keycloak_get_client_id(conf) local ttl = conf.cache_ttl_seconds @@ -501,7 +508,8 @@ local function authz_keycloak_ensure_sa_access_token(conf) end -local function authz_keycloak_resolve_permission(conf, uri, sa_access_token) +-- Resolve a URI to one or more resource IDs. +local function authz_keycloak_resolve_resource(conf, uri, sa_access_token) -- Get resource registration endpoint URL. local resource_registration_endpoint = authz_keycloak_get_resource_registration_endpoint(conf) @@ -566,8 +574,8 @@ local function evaluate_permissions(conf, ctx, token) end -- Resolve URI to resource(s). - permission, err = authz_keycloak_resolve_permission(conf, ctx.var.request_uri, - sa_access_token) + permission, err = authz_keycloak_resolve_resource(conf, ctx.var.request_uri, + sa_access_token) -- Check result. if permission == nil then diff --git a/doc/plugins/authz-keycloak.md b/doc/plugins/authz-keycloak.md index 436e3a8d42fa..f2979c7477eb 100644 --- a/doc/plugins/authz-keycloak.md +++ b/doc/plugins/authz-keycloak.md @@ -53,7 +53,7 @@ For more information on Keycloak, refer to [Keycloak Authorization Docs](https:/ | http_method_as_scope | boolean | optional | false | | Map HTTP request type to scope of same name and add to all permissions requested. | | timeout | integer | optional | 3000 | [1000, ...] | Timeout(ms) for the http connection with the Identity Server. | | ssl_verify | boolean | optional | true | | Verify if TLS certificate matches hostname. | -| cache_ttl_seconds | integer | optional | 24 * 60 * 60, i.e. 24h | positive integer >= 1 | The maximum period in seconds up to which the plugin caches discovery documents and tokens, used by the plugin to authenticate to Keycloak. | +| cache_ttl_seconds | integer | optional | 86400 (equivalent to 24h) | positive integer >= 1 | The maximum period in seconds up to which the plugin caches discovery documents and tokens, used by the plugin to authenticate to Keycloak. | | keepalive | boolean | optional | true | | Enable HTTP keep-alive to keep connections open after use. Set to `true` if you expect a lot of requests to Keycloak. | | keepalive_timeout | integer | optional | 60000 | positive integer >= 1000 | Idle timeout after which established HTTP connections will be closed. | | keepalive_pool | integer | optional | 5 | positive integer >= 1 | Maximum number of connections in the connection pool. | From caffeee2d8c72c70354de71b624a4b097b8673f1 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Thu, 21 Jan 2021 11:44:07 +0100 Subject: [PATCH 89/94] Fix syntax error. --- apisix/plugins/authz-keycloak.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index 5ba7710d252f..f8b003c9326a 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -233,7 +233,7 @@ end -- Get the Discovery metadata from the specified URL. -local function authz_keycloak_discover(conf): +local function authz_keycloak_discover(conf) log.debug("authz_keycloak_discover: URL is: " .. conf.discovery) local json, err From 1f658d2bc4146d8543cd764eecce1aae2de0b9e0 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Thu, 21 Jan 2021 12:29:08 +0100 Subject: [PATCH 90/94] Fix test case. --- t/plugin/authz-keycloak.t | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/t/plugin/authz-keycloak.t b/t/plugin/authz-keycloak.t index 6535f482dcbc..799988398dfa 100644 --- a/t/plugin/authz-keycloak.t +++ b/t/plugin/authz-keycloak.t @@ -171,7 +171,7 @@ done http_method_as_scope = false, timeout = 1000, ssl_verify = false, - cache_ttl_seconds = 1000 + cache_ttl_seconds = 1000, keepalive = true, keepalive_timeout = 10000, keepalive_pool = 5 From 67a7fe7dbc3c8495a5f6dd12078dca21bc5ea3c6 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Thu, 21 Jan 2021 13:07:53 +0100 Subject: [PATCH 91/94] Fix stray conf.http_request_decorator. --- apisix/plugins/authz-keycloak.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apisix/plugins/authz-keycloak.lua b/apisix/plugins/authz-keycloak.lua index f8b003c9326a..60240a513046 100644 --- a/apisix/plugins/authz-keycloak.lua +++ b/apisix/plugins/authz-keycloak.lua @@ -459,7 +459,7 @@ local function authz_keycloak_ensure_sa_access_token(conf) local current_time = ngx.time() - local res, err = httpc:request_uri(token_endpoint, conf.http_request_decorator, params) + local res, err = httpc:request_uri(token_endpoint, params) if not res then err = "Accessing token endpoint URL (" .. token_endpoint .. ") failed: " .. err From 977759f7867f4539ce073a2b767cc19ffdfa69f8 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Thu, 21 Jan 2021 13:48:10 +0100 Subject: [PATCH 92/94] Split test into two files. --- t/plugin/authz-keycloak.t | 824 +----------------------------------- t/plugin/authz-keycloak2.t | 839 +++++++++++++++++++++++++++++++++++++ 2 files changed, 843 insertions(+), 820 deletions(-) create mode 100644 t/plugin/authz-keycloak2.t diff --git a/t/plugin/authz-keycloak.t b/t/plugin/authz-keycloak.t index 799988398dfa..02c9c5cec13f 100644 --- a/t/plugin/authz-keycloak.t +++ b/t/plugin/authz-keycloak.t @@ -265,403 +265,7 @@ done -=== TEST 10: add plugin with view course permissions (using token endpoint) ---- config - location /t { - content_by_lua_block { - local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/routes/1', - ngx.HTTP_PUT, - [[{ - "plugins": { - "authz-keycloak": { - "token_endpoint": "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token", - "permissions": ["course_resource#view"], - "client_id": "course_management", - "grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket", - "timeout": 3000 - } - }, - "upstream": { - "nodes": { - "127.0.0.1:1982": 1 - }, - "type": "roundrobin" - }, - "uri": "/hello1" - }]], - [[{ - "node": { - "value": { - "plugins": { - "authz-keycloak": { - "token_endpoint": "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token", - "permissions": ["course_resource#view"], - "client_id": "course_management", - "grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket", - "timeout": 3000 - } - }, - "upstream": { - "nodes": { - "127.0.0.1:1982": 1 - }, - "type": "roundrobin" - }, - "uri": "/hello1" - }, - "key": "/apisix/routes/1" - }, - "action": "set" - }]] - ) - - if code >= 300 then - ngx.status = code - end - ngx.say(body) - } - } ---- request -GET /t ---- response_body -passed ---- no_error_log -[error] - - - -=== TEST 11: Get access token for teacher and access view course route ---- config - location /t { - content_by_lua_block { - local json_decode = require("toolkit.json").decode - local http = require "resty.http" - local httpc = http.new() - local uri = "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token" - local res, err = httpc:request_uri(uri, { - method = "POST", - body = "grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=teacher@gmail.com&password=123456", - headers = { - ["Content-Type"] = "application/x-www-form-urlencoded" - } - }) - - if res.status == 200 then - local body = json_decode(res.body) - local accessToken = body["access_token"] - - - uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello1" - local res, err = httpc:request_uri(uri, { - method = "GET", - headers = { - ["Authorization"] = "Bearer " .. accessToken, - } - }) - - if res.status == 200 then - ngx.say(true) - else - ngx.say(false) - end - else - ngx.say(false) - end - } - } ---- request -GET /t ---- response_body -true ---- no_error_log -[error] - - - -=== TEST 12: invalid access token ---- config - location /t { - content_by_lua_block { - local http = require "resty.http" - local httpc = http.new() - local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello1" - local res, err = httpc:request_uri(uri, { - method = "GET", - headers = { - ["Authorization"] = "Bearer wrong_token", - } - }) - if res.status == 401 then - ngx.say(true) - end - } - } ---- request -GET /t ---- response_body -true ---- error_log -Invalid bearer token - - - -=== TEST 13: add plugin with view course permissions (using discovery) ---- config - location /t { - content_by_lua_block { - local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/routes/1', - ngx.HTTP_PUT, - [[{ - "plugins": { - "authz-keycloak": { - "discovery": "http://127.0.0.1:8090/auth/realms/University/.well-known/uma2-configuration", - "permissions": ["course_resource#view"], - "client_id": "course_management", - "grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket", - "timeout": 3000 - } - }, - "upstream": { - "nodes": { - "127.0.0.1:1982": 1 - }, - "type": "roundrobin" - }, - "uri": "/hello1" - }]], - [[{ - "node": { - "value": { - "plugins": { - "authz-keycloak": { - "discovery": "http://127.0.0.1:8090/auth/realms/University/.well-known/uma2-configuration", - "permissions": ["course_resource#view"], - "client_id": "course_management", - "grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket", - "timeout": 3000 - } - }, - "upstream": { - "nodes": { - "127.0.0.1:1982": 1 - }, - "type": "roundrobin" - }, - "uri": "/hello1" - }, - "key": "/apisix/routes/1" - }, - "action": "set" - }]] - ) - - if code >= 300 then - ngx.status = code - end - ngx.say(body) - } - } ---- request -GET /t ---- response_body -passed ---- no_error_log -[error] - - - -=== TEST 14: Get access token for teacher and access view course route ---- config - location /t { - content_by_lua_block { - local json_decode = require("toolkit.json").decode - local http = require "resty.http" - local httpc = http.new() - local uri = "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token" - local res, err = httpc:request_uri(uri, { - method = "POST", - body = "grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=teacher@gmail.com&password=123456", - headers = { - ["Content-Type"] = "application/x-www-form-urlencoded" - } - }) - - if res.status == 200 then - local body = json_decode(res.body) - local accessToken = body["access_token"] - - - uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello1" - local res, err = httpc:request_uri(uri, { - method = "GET", - headers = { - ["Authorization"] = "Bearer " .. accessToken, - } - }) - - if res.status == 200 then - ngx.say(true) - else - ngx.say(false) - end - else - ngx.say(false) - end - } - } ---- request -GET /t ---- response_body -true ---- no_error_log -[error] - - - -=== TEST 15: invalid access token ---- config - location /t { - content_by_lua_block { - local http = require "resty.http" - local httpc = http.new() - local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello1" - local res, err = httpc:request_uri(uri, { - method = "GET", - headers = { - ["Authorization"] = "Bearer wrong_token", - } - }) - if res.status == 401 then - ngx.say(true) - end - } - } ---- request -GET /t ---- response_body -true ---- error_log -Invalid bearer token - - - -=== TEST 16: add plugin for delete course route ---- config - location /t { - content_by_lua_block { - local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/routes/1', - ngx.HTTP_PUT, - [[{ - "plugins": { - "authz-keycloak": { - "token_endpoint": "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token", - "permissions": ["course_resource#delete"], - "client_id": "course_management", - "grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket", - "timeout": 3000 - } - }, - "upstream": { - "nodes": { - "127.0.0.1:1982": 1 - }, - "type": "roundrobin" - }, - "uri": "/hello1" - }]], - [[{ - "node": { - "value": { - "plugins": { - "authz-keycloak": { - "token_endpoint": "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token", - "permissions": ["course_resource#delete"], - "client_id": "course_management", - "grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket", - "timeout": 3000 - } - }, - "upstream": { - "nodes": { - "127.0.0.1:1982": 1 - }, - "type": "roundrobin" - }, - "uri": "/hello1" - }, - "key": "/apisix/routes/1" - }, - "action": "set" - }]] - ) - - if code >= 300 then - ngx.status = code - end - ngx.say(body) - } - } ---- request -GET /t ---- response_body -passed ---- no_error_log -[error] - - - -=== TEST 17: Get access token for student and delete course ---- config - location /t { - content_by_lua_block { - local json_decode = require("toolkit.json").decode - local http = require "resty.http" - local httpc = http.new() - local uri = "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token" - local res, err = httpc:request_uri(uri, { - method = "POST", - body = "grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=student@gmail.com&password=123456", - headers = { - ["Content-Type"] = "application/x-www-form-urlencoded" - } - }) - - if res.status == 200 then - local body = json_decode(res.body) - local accessToken = body["access_token"] - - - uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello1" - local res, err = httpc:request_uri(uri, { - method = "GET", - headers = { - ["Authorization"] = "Bearer " .. accessToken, - } - }) - - if res.status == 403 then - ngx.say(true) - else - ngx.say(false) - end - else - ngx.say(false) - end - } - } ---- request -GET /t ---- response_body -true ---- error_log -{"error":"access_denied","error_description":"not_authorized"} - - - -=== TEST 18: Add https endpoint with ssl_verify true (default) +=== TEST 10: Add https endpoint with ssl_verify true (default) --- config location /t { content_by_lua_block { @@ -727,7 +331,7 @@ passed -=== TEST 19: TEST with fake token and https endpoint +=== TEST 11: TEST with fake token and https endpoint --- config location /t { content_by_lua_block { @@ -757,7 +361,7 @@ Error while sending authz request to https://127.0.0.1:8443/auth/realms/Universi -=== TEST 20: Add htttps endpoint with ssl_verify false +=== TEST 12: Add https endpoint with ssl_verify false --- config location /t { content_by_lua_block { @@ -825,7 +429,7 @@ passed -=== TEST 21: TEST for https based token verification with ssl_verify false +=== TEST 13: TEST for https based token verification with ssl_verify false --- config location /t { content_by_lua_block { @@ -852,423 +456,3 @@ GET /t false --- error_log Request denied: HTTP 401 Unauthorized. Body: {"error":"HTTP 401 Unauthorized"} - - - -=== TEST 22: add plugin with lazy_load_paths and http_method_as_scope ---- config - location /t { - content_by_lua_block { - local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/routes/1', - ngx.HTTP_PUT, - [[{ - "plugins": { - "authz-keycloak": { - "discovery": "http://127.0.0.1:8090/auth/realms/University/.well-known/uma2-configuration", - "client_id": "course_management", - "client_secret": "d1ec69e9-55d2-4109-a3ea-befa071579d5", - "lazy_load_paths": true, - "http_method_as_scope": true - } - }, - "upstream": { - "nodes": { - "127.0.0.1:1982": 1 - }, - "type": "roundrobin" - }, - "uri": "/course/foo" - }]], - [[{ - "node": { - "value": { - "plugins": { - "authz-keycloak": { - "discovery": "http://127.0.0.1:8090/auth/realms/University/.well-known/uma2-configuration", - "client_id": "course_management", - "client_secret": "d1ec69e9-55d2-4109-a3ea-befa071579d5", - "lazy_load_paths": true, - "http_method_as_scope": true - } - }, - "upstream": { - "nodes": { - "127.0.0.1:1982": 1 - }, - "type": "roundrobin" - }, - "uri": "/course/foo" - }, - "key": "/apisix/routes/1" - }, - "action": "set" - }]] - ) - - if code >= 300 then - ngx.status = code - end - ngx.say(body) - } - } ---- request -GET /t ---- response_body -passed ---- no_error_log -[error] - - - -=== TEST 23: Get access token for teacher and access view course route. ---- config - location /t { - content_by_lua_block { - local json_decode = require("toolkit.json").decode - local http = require "resty.http" - local httpc = http.new() - local uri = "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token" - local res, err = httpc:request_uri(uri, { - method = "POST", - body = "grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=teacher@gmail.com&password=123456", - headers = { - ["Content-Type"] = "application/x-www-form-urlencoded" - } - }) - - if res.status == 200 then - local body = json_decode(res.body) - local accessToken = body["access_token"] - - - uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/course/foo" - local res, err = httpc:request_uri(uri, { - method = "GET", - headers = { - ["Authorization"] = "Bearer " .. accessToken, - } - }) - - if res.status == 200 then - ngx.say(true) - else - ngx.say(false) - end - else - ngx.say(false) - end - } - } ---- request -GET /t ---- response_body -true ---- no_error_log -[error] - - - -=== TEST 24: Get access token for student and access view course route. ---- config - location /t { - content_by_lua_block { - local json_decode = require("toolkit.json").decode - local http = require "resty.http" - local httpc = http.new() - local uri = "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token" - local res, err = httpc:request_uri(uri, { - method = "POST", - body = "grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=student@gmail.com&password=123456", - headers = { - ["Content-Type"] = "application/x-www-form-urlencoded" - } - }) - - if res.status == 200 then - local body = json_decode(res.body) - local accessToken = body["access_token"] - - - uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/course/foo" - local res, err = httpc:request_uri(uri, { - method = "GET", - headers = { - ["Authorization"] = "Bearer " .. accessToken, - } - }) - - if res.status == 200 then - ngx.say(true) - else - ngx.say(false) - end - else - ngx.say(false) - end - } - } ---- request -GET /t ---- response_body -true ---- no_error_log -[error] - - - -=== TEST 25: Get access token for teacher and delete course. ---- config - location /t { - content_by_lua_block { - local json_decode = require("toolkit.json").decode - local http = require "resty.http" - local httpc = http.new() - local uri = "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token" - local res, err = httpc:request_uri(uri, { - method = "POST", - body = "grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=teacher@gmail.com&password=123456", - headers = { - ["Content-Type"] = "application/x-www-form-urlencoded" - } - }) - - if res.status == 200 then - local body = json_decode(res.body) - local accessToken = body["access_token"] - - - uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/course/foo" - local res, err = httpc:request_uri(uri, { - method = "DELETE", - headers = { - ["Authorization"] = "Bearer " .. accessToken, - } - }) - - if res.status == 200 then - ngx.say(true) - else - ngx.say(false) - end - else - ngx.say(false) - end - } - } ---- request -GET /t ---- response_body -true ---- no_error_log -[error] - - - -=== TEST 26: Get access token for student and try to delete course. Should fail. ---- config - location /t { - content_by_lua_block { - local json_decode = require("toolkit.json").decode - local http = require "resty.http" - local httpc = http.new() - local uri = "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token" - local res, err = httpc:request_uri(uri, { - method = "POST", - body = "grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=student@gmail.com&password=123456", - headers = { - ["Content-Type"] = "application/x-www-form-urlencoded" - } - }) - - if res.status == 200 then - local body = json_decode(res.body) - local accessToken = body["access_token"] - - - uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/course/foo" - local res, err = httpc:request_uri(uri, { - method = "DELETE", - headers = { - ["Authorization"] = "Bearer " .. accessToken, - } - }) - - if res.status == 403 then - ngx.say(true) - else - ngx.say(false) - end - else - ngx.say(false) - end - } - } ---- request -GET /t ---- response_body -true ---- error_log -{"error":"access_denied","error_description":"not_authorized"} - - - -=== TEST 27: add plugin with lazy_load_paths and http_method_as_scope (using audience) ---- config - location /t { - content_by_lua_block { - local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/routes/1', - ngx.HTTP_PUT, - [[{ - "plugins": { - "authz-keycloak": { - "discovery": "http://127.0.0.1:8090/auth/realms/University/.well-known/uma2-configuration", - "audience": "course_management", - "client_secret": "d1ec69e9-55d2-4109-a3ea-befa071579d5", - "lazy_load_paths": true, - "http_method_as_scope": true - } - }, - "upstream": { - "nodes": { - "127.0.0.1:1982": 1 - }, - "type": "roundrobin" - }, - "uri": "/course/foo" - }]], - [[{ - "node": { - "value": { - "plugins": { - "authz-keycloak": { - "discovery": "http://127.0.0.1:8090/auth/realms/University/.well-known/uma2-configuration", - "audience": "course_management", - "client_secret": "d1ec69e9-55d2-4109-a3ea-befa071579d5", - "lazy_load_paths": true, - "http_method_as_scope": true - } - }, - "upstream": { - "nodes": { - "127.0.0.1:1982": 1 - }, - "type": "roundrobin" - }, - "uri": "/course/foo" - }, - "key": "/apisix/routes/1" - }, - "action": "set" - }]] - ) - - if code >= 300 then - ngx.status = code - end - ngx.say(body) - } - } ---- request -GET /t ---- response_body -passed ---- no_error_log -[error] - - - -=== TEST 28: Get access token for teacher and access view course route. ---- config - location /t { - content_by_lua_block { - local json_decode = require("toolkit.json").decode - local http = require "resty.http" - local httpc = http.new() - local uri = "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token" - local res, err = httpc:request_uri(uri, { - method = "POST", - body = "grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=teacher@gmail.com&password=123456", - headers = { - ["Content-Type"] = "application/x-www-form-urlencoded" - } - }) - - if res.status == 200 then - local body = json_decode(res.body) - local accessToken = body["access_token"] - - - uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/course/foo" - local res, err = httpc:request_uri(uri, { - method = "GET", - headers = { - ["Authorization"] = "Bearer " .. accessToken, - } - }) - - if res.status == 200 then - ngx.say(true) - else - ngx.say(false) - end - else - ngx.say(false) - end - } - } ---- request -GET /t ---- response_body -true ---- no_error_log -[error] - - - -=== TEST 29: Get access token for student and access view course route. ---- config - location /t { - content_by_lua_block { - local json_decode = require("toolkit.json").decode - local http = require "resty.http" - local httpc = http.new() - local uri = "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token" - local res, err = httpc:request_uri(uri, { - method = "POST", - body = "grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=student@gmail.com&password=123456", - headers = { - ["Content-Type"] = "application/x-www-form-urlencoded" - } - }) - - if res.status == 200 then - local body = json_decode(res.body) - local accessToken = body["access_token"] - - - uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/course/foo" - local res, err = httpc:request_uri(uri, { - method = "GET", - headers = { - ["Authorization"] = "Bearer " .. accessToken, - } - }) - - if res.status == 200 then - ngx.say(true) - else - ngx.say(false) - end - else - ngx.say(false) - end - } - } ---- request -GET /t ---- response_body -true ---- no_error_log -[error] diff --git a/t/plugin/authz-keycloak2.t b/t/plugin/authz-keycloak2.t new file mode 100644 index 000000000000..c2c3e978e9b7 --- /dev/null +++ b/t/plugin/authz-keycloak2.t @@ -0,0 +1,839 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +use t::APISIX 'no_plan'; + +log_level('debug'); +repeat_each(1); +no_long_string(); +no_root_location(); +run_tests; + +__DATA__ + +=== TEST 14: add plugin with view course permissions (using token endpoint) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "authz-keycloak": { + "token_endpoint": "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token", + "permissions": ["course_resource#view"], + "client_id": "course_management", + "grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket", + "timeout": 3000 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello1" + }]], + [[{ + "node": { + "value": { + "plugins": { + "authz-keycloak": { + "token_endpoint": "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token", + "permissions": ["course_resource#view"], + "client_id": "course_management", + "grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket", + "timeout": 3000 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello1" + }, + "key": "/apisix/routes/1" + }, + "action": "set" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 15: Get access token for teacher and access view course route +--- config + location /t { + content_by_lua_block { + local json_decode = require("toolkit.json").decode + local http = require "resty.http" + local httpc = http.new() + local uri = "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token" + local res, err = httpc:request_uri(uri, { + method = "POST", + body = "grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=teacher@gmail.com&password=123456", + headers = { + ["Content-Type"] = "application/x-www-form-urlencoded" + } + }) + + if res.status == 200 then + local body = json_decode(res.body) + local accessToken = body["access_token"] + + + uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello1" + local res, err = httpc:request_uri(uri, { + method = "GET", + headers = { + ["Authorization"] = "Bearer " .. accessToken, + } + }) + + if res.status == 200 then + ngx.say(true) + else + ngx.say(false) + end + else + ngx.say(false) + end + } + } +--- request +GET /t +--- response_body +true +--- no_error_log +[error] + + + +=== TEST 16: invalid access token +--- config + location /t { + content_by_lua_block { + local http = require "resty.http" + local httpc = http.new() + local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello1" + local res, err = httpc:request_uri(uri, { + method = "GET", + headers = { + ["Authorization"] = "Bearer wrong_token", + } + }) + if res.status == 401 then + ngx.say(true) + end + } + } +--- request +GET /t +--- response_body +true +--- error_log +Invalid bearer token + + + +=== TEST 17: add plugin with view course permissions (using discovery) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "authz-keycloak": { + "discovery": "http://127.0.0.1:8090/auth/realms/University/.well-known/uma2-configuration", + "permissions": ["course_resource#view"], + "client_id": "course_management", + "grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket", + "timeout": 3000 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello1" + }]], + [[{ + "node": { + "value": { + "plugins": { + "authz-keycloak": { + "discovery": "http://127.0.0.1:8090/auth/realms/University/.well-known/uma2-configuration", + "permissions": ["course_resource#view"], + "client_id": "course_management", + "grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket", + "timeout": 3000 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello1" + }, + "key": "/apisix/routes/1" + }, + "action": "set" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 18: Get access token for teacher and access view course route +--- config + location /t { + content_by_lua_block { + local json_decode = require("toolkit.json").decode + local http = require "resty.http" + local httpc = http.new() + local uri = "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token" + local res, err = httpc:request_uri(uri, { + method = "POST", + body = "grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=teacher@gmail.com&password=123456", + headers = { + ["Content-Type"] = "application/x-www-form-urlencoded" + } + }) + + if res.status == 200 then + local body = json_decode(res.body) + local accessToken = body["access_token"] + + + uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello1" + local res, err = httpc:request_uri(uri, { + method = "GET", + headers = { + ["Authorization"] = "Bearer " .. accessToken, + } + }) + + if res.status == 200 then + ngx.say(true) + else + ngx.say(false) + end + else + ngx.say(false) + end + } + } +--- request +GET /t +--- response_body +true +--- no_error_log +[error] + + + +=== TEST 19: invalid access token +--- config + location /t { + content_by_lua_block { + local http = require "resty.http" + local httpc = http.new() + local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello1" + local res, err = httpc:request_uri(uri, { + method = "GET", + headers = { + ["Authorization"] = "Bearer wrong_token", + } + }) + if res.status == 401 then + ngx.say(true) + end + } + } +--- request +GET /t +--- response_body +true +--- error_log +Invalid bearer token + + + +=== TEST 20: add plugin for delete course route +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "authz-keycloak": { + "token_endpoint": "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token", + "permissions": ["course_resource#delete"], + "client_id": "course_management", + "grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket", + "timeout": 3000 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello1" + }]], + [[{ + "node": { + "value": { + "plugins": { + "authz-keycloak": { + "token_endpoint": "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token", + "permissions": ["course_resource#delete"], + "client_id": "course_management", + "grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket", + "timeout": 3000 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello1" + }, + "key": "/apisix/routes/1" + }, + "action": "set" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 21: Get access token for student and delete course +--- config + location /t { + content_by_lua_block { + local json_decode = require("toolkit.json").decode + local http = require "resty.http" + local httpc = http.new() + local uri = "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token" + local res, err = httpc:request_uri(uri, { + method = "POST", + body = "grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=student@gmail.com&password=123456", + headers = { + ["Content-Type"] = "application/x-www-form-urlencoded" + } + }) + + if res.status == 200 then + local body = json_decode(res.body) + local accessToken = body["access_token"] + + + uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello1" + local res, err = httpc:request_uri(uri, { + method = "GET", + headers = { + ["Authorization"] = "Bearer " .. accessToken, + } + }) + + if res.status == 403 then + ngx.say(true) + else + ngx.say(false) + end + else + ngx.say(false) + end + } + } +--- request +GET /t +--- response_body +true +--- error_log +{"error":"access_denied","error_description":"not_authorized"} + + + +=== TEST 22: add plugin with lazy_load_paths and http_method_as_scope +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "authz-keycloak": { + "discovery": "http://127.0.0.1:8090/auth/realms/University/.well-known/uma2-configuration", + "client_id": "course_management", + "client_secret": "d1ec69e9-55d2-4109-a3ea-befa071579d5", + "lazy_load_paths": true, + "http_method_as_scope": true + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "uri": "/course/foo" + }]], + [[{ + "node": { + "value": { + "plugins": { + "authz-keycloak": { + "discovery": "http://127.0.0.1:8090/auth/realms/University/.well-known/uma2-configuration", + "client_id": "course_management", + "client_secret": "d1ec69e9-55d2-4109-a3ea-befa071579d5", + "lazy_load_paths": true, + "http_method_as_scope": true + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "uri": "/course/foo" + }, + "key": "/apisix/routes/1" + }, + "action": "set" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 23: Get access token for teacher and access view course route. +--- config + location /t { + content_by_lua_block { + local json_decode = require("toolkit.json").decode + local http = require "resty.http" + local httpc = http.new() + local uri = "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token" + local res, err = httpc:request_uri(uri, { + method = "POST", + body = "grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=teacher@gmail.com&password=123456", + headers = { + ["Content-Type"] = "application/x-www-form-urlencoded" + } + }) + + if res.status == 200 then + local body = json_decode(res.body) + local accessToken = body["access_token"] + + + uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/course/foo" + local res, err = httpc:request_uri(uri, { + method = "GET", + headers = { + ["Authorization"] = "Bearer " .. accessToken, + } + }) + + if res.status == 200 then + ngx.say(true) + else + ngx.say(false) + end + else + ngx.say(false) + end + } + } +--- request +GET /t +--- response_body +true +--- no_error_log +[error] + + + +=== TEST 24: Get access token for student and access view course route. +--- config + location /t { + content_by_lua_block { + local json_decode = require("toolkit.json").decode + local http = require "resty.http" + local httpc = http.new() + local uri = "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token" + local res, err = httpc:request_uri(uri, { + method = "POST", + body = "grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=student@gmail.com&password=123456", + headers = { + ["Content-Type"] = "application/x-www-form-urlencoded" + } + }) + + if res.status == 200 then + local body = json_decode(res.body) + local accessToken = body["access_token"] + + + uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/course/foo" + local res, err = httpc:request_uri(uri, { + method = "GET", + headers = { + ["Authorization"] = "Bearer " .. accessToken, + } + }) + + if res.status == 200 then + ngx.say(true) + else + ngx.say(false) + end + else + ngx.say(false) + end + } + } +--- request +GET /t +--- response_body +true +--- no_error_log +[error] + + + +=== TEST 25: Get access token for teacher and delete course. +--- config + location /t { + content_by_lua_block { + local json_decode = require("toolkit.json").decode + local http = require "resty.http" + local httpc = http.new() + local uri = "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token" + local res, err = httpc:request_uri(uri, { + method = "POST", + body = "grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=teacher@gmail.com&password=123456", + headers = { + ["Content-Type"] = "application/x-www-form-urlencoded" + } + }) + + if res.status == 200 then + local body = json_decode(res.body) + local accessToken = body["access_token"] + + + uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/course/foo" + local res, err = httpc:request_uri(uri, { + method = "DELETE", + headers = { + ["Authorization"] = "Bearer " .. accessToken, + } + }) + + if res.status == 200 then + ngx.say(true) + else + ngx.say(false) + end + else + ngx.say(false) + end + } + } +--- request +GET /t +--- response_body +true +--- no_error_log +[error] + + + +=== TEST 26: Get access token for student and try to delete course. Should fail. +--- config + location /t { + content_by_lua_block { + local json_decode = require("toolkit.json").decode + local http = require "resty.http" + local httpc = http.new() + local uri = "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token" + local res, err = httpc:request_uri(uri, { + method = "POST", + body = "grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=student@gmail.com&password=123456", + headers = { + ["Content-Type"] = "application/x-www-form-urlencoded" + } + }) + + if res.status == 200 then + local body = json_decode(res.body) + local accessToken = body["access_token"] + + + uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/course/foo" + local res, err = httpc:request_uri(uri, { + method = "DELETE", + headers = { + ["Authorization"] = "Bearer " .. accessToken, + } + }) + + if res.status == 403 then + ngx.say(true) + else + ngx.say(false) + end + else + ngx.say(false) + end + } + } +--- request +GET /t +--- response_body +true +--- error_log +{"error":"access_denied","error_description":"not_authorized"} + + + +=== TEST 27: add plugin with lazy_load_paths and http_method_as_scope (using audience) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "authz-keycloak": { + "discovery": "http://127.0.0.1:8090/auth/realms/University/.well-known/uma2-configuration", + "audience": "course_management", + "client_secret": "d1ec69e9-55d2-4109-a3ea-befa071579d5", + "lazy_load_paths": true, + "http_method_as_scope": true + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "uri": "/course/foo" + }]], + [[{ + "node": { + "value": { + "plugins": { + "authz-keycloak": { + "discovery": "http://127.0.0.1:8090/auth/realms/University/.well-known/uma2-configuration", + "audience": "course_management", + "client_secret": "d1ec69e9-55d2-4109-a3ea-befa071579d5", + "lazy_load_paths": true, + "http_method_as_scope": true + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "uri": "/course/foo" + }, + "key": "/apisix/routes/1" + }, + "action": "set" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 28: Get access token for teacher and access view course route. +--- config + location /t { + content_by_lua_block { + local json_decode = require("toolkit.json").decode + local http = require "resty.http" + local httpc = http.new() + local uri = "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token" + local res, err = httpc:request_uri(uri, { + method = "POST", + body = "grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=teacher@gmail.com&password=123456", + headers = { + ["Content-Type"] = "application/x-www-form-urlencoded" + } + }) + + if res.status == 200 then + local body = json_decode(res.body) + local accessToken = body["access_token"] + + + uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/course/foo" + local res, err = httpc:request_uri(uri, { + method = "GET", + headers = { + ["Authorization"] = "Bearer " .. accessToken, + } + }) + + if res.status == 200 then + ngx.say(true) + else + ngx.say(false) + end + else + ngx.say(false) + end + } + } +--- request +GET /t +--- response_body +true +--- no_error_log +[error] + + + +=== TEST 29: Get access token for student and access view course route. +--- config + location /t { + content_by_lua_block { + local json_decode = require("toolkit.json").decode + local http = require "resty.http" + local httpc = http.new() + local uri = "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token" + local res, err = httpc:request_uri(uri, { + method = "POST", + body = "grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=student@gmail.com&password=123456", + headers = { + ["Content-Type"] = "application/x-www-form-urlencoded" + } + }) + + if res.status == 200 then + local body = json_decode(res.body) + local accessToken = body["access_token"] + + + uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/course/foo" + local res, err = httpc:request_uri(uri, { + method = "GET", + headers = { + ["Authorization"] = "Bearer " .. accessToken, + } + }) + + if res.status == 200 then + ngx.say(true) + else + ngx.say(false) + end + else + ngx.say(false) + end + } + } +--- request +GET /t +--- response_body +true +--- no_error_log +[error] From c4c4449c06e36f4f4667a25f27265897d27162a5 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Thu, 21 Jan 2021 13:52:12 +0100 Subject: [PATCH 93/94] Re-enable all tests. --- .travis/linux_openresty_common_runner.sh | 2 +- utils/centos7-ci.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis/linux_openresty_common_runner.sh b/.travis/linux_openresty_common_runner.sh index ef49f63f9098..128fb40305d8 100755 --- a/.travis/linux_openresty_common_runner.sh +++ b/.travis/linux_openresty_common_runner.sh @@ -139,7 +139,7 @@ script() { make lint && make license-check || exit 1 # APISIX_ENABLE_LUACOV=1 PERL5LIB=.:$PERL5LIB prove -Itest-nginx/lib -r t - PERL5LIB=.:$PERL5LIB prove -Itest-nginx/lib -r t/plugin/authz-keycloak.t + PERL5LIB=.:$PERL5LIB prove -Itest-nginx/lib -r t } after_success() { diff --git a/utils/centos7-ci.sh b/utils/centos7-ci.sh index 28c4dd427256..7395518ff5d8 100755 --- a/utils/centos7-ci.sh +++ b/utils/centos7-ci.sh @@ -71,7 +71,7 @@ run_case() { cd apisix # run test cases - prove -Itest-nginx/lib -I./ -r t/plugin/authz-keycloak.t + prove -Itest-nginx/lib -I./ -r t/ } case_opt=$1 From 7a8460ce63e187ea64340d39d43f8b5f2f8c90e4 Mon Sep 17 00:00:00 2001 From: Jens Keiner Date: Thu, 21 Jan 2021 14:52:40 +0100 Subject: [PATCH 94/94] Fix test case numbering scheme. --- t/plugin/authz-keycloak2.t | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/t/plugin/authz-keycloak2.t b/t/plugin/authz-keycloak2.t index c2c3e978e9b7..953828d96943 100644 --- a/t/plugin/authz-keycloak2.t +++ b/t/plugin/authz-keycloak2.t @@ -24,7 +24,7 @@ run_tests; __DATA__ -=== TEST 14: add plugin with view course permissions (using token endpoint) +=== TEST 1: add plugin with view course permissions (using token endpoint) --- config location /t { content_by_lua_block { @@ -90,7 +90,7 @@ passed -=== TEST 15: Get access token for teacher and access view course route +=== TEST 2: Get access token for teacher and access view course route --- config location /t { content_by_lua_block { @@ -138,7 +138,7 @@ true -=== TEST 16: invalid access token +=== TEST 3: invalid access token --- config location /t { content_by_lua_block { @@ -165,7 +165,7 @@ Invalid bearer token -=== TEST 17: add plugin with view course permissions (using discovery) +=== TEST 4: add plugin with view course permissions (using discovery) --- config location /t { content_by_lua_block { @@ -231,7 +231,7 @@ passed -=== TEST 18: Get access token for teacher and access view course route +=== TEST 5: Get access token for teacher and access view course route --- config location /t { content_by_lua_block { @@ -279,7 +279,7 @@ true -=== TEST 19: invalid access token +=== TEST 6: invalid access token --- config location /t { content_by_lua_block { @@ -306,7 +306,7 @@ Invalid bearer token -=== TEST 20: add plugin for delete course route +=== TEST 7: add plugin for delete course route --- config location /t { content_by_lua_block { @@ -372,7 +372,7 @@ passed -=== TEST 21: Get access token for student and delete course +=== TEST 8: Get access token for student and delete course --- config location /t { content_by_lua_block { @@ -420,7 +420,7 @@ true -=== TEST 22: add plugin with lazy_load_paths and http_method_as_scope +=== TEST 9: add plugin with lazy_load_paths and http_method_as_scope --- config location /t { content_by_lua_block { @@ -486,7 +486,7 @@ passed -=== TEST 23: Get access token for teacher and access view course route. +=== TEST 10: Get access token for teacher and access view course route. --- config location /t { content_by_lua_block { @@ -534,7 +534,7 @@ true -=== TEST 24: Get access token for student and access view course route. +=== TEST 11: Get access token for student and access view course route. --- config location /t { content_by_lua_block { @@ -582,7 +582,7 @@ true -=== TEST 25: Get access token for teacher and delete course. +=== TEST 12: Get access token for teacher and delete course. --- config location /t { content_by_lua_block { @@ -630,7 +630,7 @@ true -=== TEST 26: Get access token for student and try to delete course. Should fail. +=== TEST 13: Get access token for student and try to delete course. Should fail. --- config location /t { content_by_lua_block { @@ -678,7 +678,7 @@ true -=== TEST 27: add plugin with lazy_load_paths and http_method_as_scope (using audience) +=== TEST 14: add plugin with lazy_load_paths and http_method_as_scope (using audience) --- config location /t { content_by_lua_block { @@ -744,7 +744,7 @@ passed -=== TEST 28: Get access token for teacher and access view course route. +=== TEST 15: Get access token for teacher and access view course route. --- config location /t { content_by_lua_block { @@ -792,7 +792,7 @@ true -=== TEST 29: Get access token for student and access view course route. +=== TEST 16: Get access token for student and access view course route. --- config location /t { content_by_lua_block {