Skip to content

Commit

Permalink
v0.0.2-1 - Distributed cache
Browse files Browse the repository at this point in the history
  • Loading branch information
carnei-ro committed Oct 26, 2020
1 parent 91e0b64 commit e90aabc
Show file tree
Hide file tree
Showing 7 changed files with 202 additions and 50 deletions.
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ populate-opa-server:
config:
@curl -s -X POST http://localhost:8001/services/ -d 'name=httpbin' -d url=http://httpbin.org/anything
@curl -s -X POST http://localhost:8001/services/httpbin/routes -d 'paths[]=/' -d 'name=some_route_name_here'
@curl -i -X POST http://localhost:8001/routes/some_route_name_here/plugins -d "name=${NAME}" -d "config.opa_host=opa_server" -d "config.opa_port=8181" -d "config.policy_uri=/v1/data/carneiro/policy1" -d "config.opa_result_boolean_key=deny" -d "config.opa_result_boolean_value=false"
@curl -i -X POST http://localhost:8001/routes/some_route_name_here/plugins -d "name=${NAME}" -d "config.opa_host=opa_server" -d "config.opa_port=8181" -d "config.policy_uri=/v1/data/carneiro/policy1" -d "config.opa_result_boolean_key=deny" -d "config.opa_result_boolean_value=false" -d "config.use_redis_cache=true" -d "config.redis_host=redis"

config-plugin-remove:
@curl -i -X DELETE http://localhost:8001/plugins/$$(curl -s http://localhost:8001/plugins/ | jq -r ".data[] | select (.name|test(\"${NAME}\")) .id")
Expand All @@ -106,5 +106,13 @@ config-plugin-disable-debug:
@curl -i -X PATCH http://localhost:8001/plugins/$$(curl -s http://localhost:8001/plugins/ | jq -r ".data[] | select (.name|test(\"${NAME}\")) .id") -F "name=${NAME}" -F "config.debug=false"
@echo " "

config-plugin-enable-cache:
@curl -i -X PATCH http://localhost:8001/plugins/$$(curl -s http://localhost:8001/plugins/ | jq -r ".data[] | select (.name|test(\"${NAME}\")) .id") -F "name=${NAME}" -F "config.use_redis_cache=true"
@echo " "

config-plugin-disable-cache:
@curl -i -X PATCH http://localhost:8001/plugins/$$(curl -s http://localhost:8001/plugins/ | jq -r ".data[] | select (.name|test(\"${NAME}\")) .id") -F "name=${NAME}" -F "config.use_redis_cache=false"
@echo " "

remove-all:
@for i in plugins consumers routes services upstreams; do for j in $$(curl -s --url http://127.0.0.1:8001/$${i} | jq -r ".data[].id"); do curl -s -i -X DELETE --url http://127.0.0.1:8001/$${i}/$${j}; done; done
19 changes: 15 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# OPA Kong Plugin
summary: Custom Kong plugin to allow for fine grained Authorization through Open Policy Agent

_Created to work with Kong 2.0.x
_Created to work with Kong >= 2.0.x

Inspired by https://github.com/TravelNest/kong-authorization-opa
Connection based on https://github.com/Kong/kong-plugin-aws-lambda
Expand All @@ -12,6 +12,8 @@ Plugin will continue the request to the upstream target if OPA responds with `tr

Requests will add the header `X-Kong-Authz-Latency` to requests which have been impacted by the plugin.

Plugin priority: `799`

## Setup

### Config
Expand All @@ -33,8 +35,16 @@ Requests will add the header `X-Kong-Authz-Latency` to requests which have been
|`forward_request_body` |flag to forward request body |`boolean`| true |
|`forward_request_cookies` |flag to forward request cookies (will remove headers.cookie) |`boolean`| true |
|`debug` |flag to return the request/response to/from OPA - not the upstream target (used for testing purposes) |`boolean`| false |
|`config.proxy_url` |An optional value that defines whether the plugin should connect through the given proxy server URL. This value is required if `proxy_scheme` is defined. | `string` | |
|`config.proxy_scheme` |An optional value that defines which HTTP protocol scheme to use in order to connect through the proxy server. The schemes supported are: `http` and `https`. This value is required if `proxy_url` is defined. | `string` | |
|`proxy_url` |An optional value that defines whether the plugin should connect through the given proxy server URL. This value is required if `proxy_scheme` is defined. | `string` | |
|`proxy_scheme` |An optional value that defines which HTTP protocol scheme to use in order to connect through the proxy server. The schemes supported are: `http` and `https`. This value is required if `proxy_url` is defined. | `string` | |
|`use_redis_cache` |flag to cache OPA response in Redis |`boolean`| false |
|`redis_cache_ttl` |Redis Key TTL (in seconds) |`integer`| 15 |
|`redis_host` |Redis Host to connect |`string`| |
|`redis_port` |Redis Port to connect |`integer`| 6379 |
|`redis_password` |Redis Password to connect |`string`| |
|`redis_timeout_in_ms` |Redis Timeout (in miliseconds) |`integer`| 500 |
|`redis_database` |Redis Database to Use |`integer`| 0 |


#### Example

Expand Down Expand Up @@ -82,4 +92,5 @@ $ curl -i -X POST \
## Roadmap

- Recreate the connection part based on the AWS Lambda plugin (OK)
- Implement toggle to use cache
- Implement toggle to use distributed cache (OK)
- Use pongo to create a test suit
18 changes: 9 additions & 9 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ services:
environment:
- POSTGRES_USER=kong
- POSTGRES_DB=kong
- POSTGRES_PASSWORD=kong
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 10s
Expand All @@ -24,6 +25,7 @@ services:
restart: on-failure
environment:
KONG_PG_HOST: kong-database
KONG_PG_PASSWORD: kong
links:
- kong-database
depends_on:
Expand All @@ -37,6 +39,12 @@ services:
command:
run --server --log-level debug

redis:
image: redis
restart: on-failure
ports:
- "6379:6379"

kong:
image: kong:2.0.3-centos
depends_on:
Expand All @@ -46,14 +54,12 @@ services:
- KONG_LUA_SSL_TRUSTED_CERTIFICATE=/etc/pki/ca-trust/extracted/openssl/ca-bundle.trust.crt
- KONG_DATABASE=postgres
- KONG_PG_HOST=kong-database
- KONG_PG_PASSWORD=kong
- KONG_PROXY_ACCESS_LOG=/dev/stdout
- KONG_ADMIN_ACCESS_LOG=/dev/stdout
- KONG_PROXY_ERROR_LOG=/dev/stderr
- KONG_ADMIN_ERROR_LOG=/dev/stderr
- KONG_ADMIN_LISTEN=0.0.0.0:8001
- KONG_ADMIN_LISTEN_SSL=0.0.0.0:8444
- KONG_VITALS=off
- KONG_PORTAL=off
- KONG_LOG_LEVEL=debug
- KONG_PLUGINS=bundled,${NAME}
volumes:
Expand All @@ -73,13 +79,7 @@ services:
/usr/local/bin/kong start --run-migrations --vv
ports:
- "8000:8000"
- "8443:8443"
- "8001:8001"
- "8444:8444"
- "8002:8002"
- "8445:8445"
- "8003:8003"
- "8004:8004"

volumes:

Expand Down
61 changes: 37 additions & 24 deletions opa/access.lua
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
local plugin_name = ({...})[1]:match("^kong%.plugins%.([^%.]+)")
local http = require("kong.plugins." .. plugin_name .. ".connect-better")
local http = require("kong.plugins." .. plugin_name .. ".connect-better")
local redis = require("kong.plugins." .. plugin_name .. ".redis")

local kong = kong
local cjson = require "cjson.safe"
local cjson = require("cjson.safe").new()
local resty_cookie = require('resty.cookie')
local table_insert = table.insert
local string_find = string.find
local pairs = pairs
local ngx_encode_base64 = ngx.encode_base64

cjson.decode_array_with_array_mt(true)

local _M = {}

Expand Down Expand Up @@ -85,18 +87,8 @@ local function prepare_payload(conf)
return payload
end

--- access
function _M.execute(conf)
local start_time = ngx.now()
local opa_body = {}
opa_body = prepare_payload(conf)

local opa_body_json, err = cjson.encode(opa_body)
if not opa_body_json then
kong.log.err("[opa] could not JSON encode upstream body",
" to forward request values: ", err)
end

local function request_to_opa(conf, opa_body_json)
kong.log.debug(" => Request to OPA")
local method = conf.opa_method
local scheme = conf.opa_scheme
local host = conf.opa_host
Expand All @@ -107,7 +99,7 @@ function _M.execute(conf)
local client = http.new()
client:set_timeout(conf.timeout)

local ok
local ok, err
ok, err = client:connect_better {
scheme = scheme,
host = host,
Expand All @@ -119,7 +111,7 @@ function _M.execute(conf)
}
if not ok then
kong.log.err(err)
return kong.response.exit(500, { message = "An unexpected error occurred", error = err })
return kong.response.exit(500, { message = "An unexpected error occurred 0", error = err })
end

local res, err = client:request {
Expand All @@ -143,31 +135,52 @@ function _M.execute(conf)
return kong.response.exit(500, { message = "An unexpected error occurred", error = err })
end

local body, err = cjson.decode(content)
if not body then
return kong.response.exit(500, { message = "An unexpected error occurred", error = err })
return content
end

--- access
function _M.execute(conf)
local start_time = ngx.now()
local opa_body = {}
opa_body = prepare_payload(conf)

local opa_body_json, err = cjson.encode(opa_body)
if not opa_body_json then
kong.log.err("[opa] could not JSON encode upstream body",
" to forward request values: ", err)
end

kong.response.set_header("X-Kong-Authz-Latency", (ngx.now() - start_time) )
local body, err
if (conf.use_redis_cache) then
local red = redis:connection(conf)
body, err = redis:get(red, opa_body_json, conf.redis_cache_ttl, request_to_opa, conf, opa_body_json)
else
body, err = request_to_opa(conf, opa_body_json)
end
if (not body) or (err) then
return kong.response.exit(500, { message = "An unexpected error occurred", error = err })
end
body = cjson.decode(body)

if conf.debug then
kong.response.exit(200, { request = opa_body, response = body } )
kong.response.exit(200, { request = opa_body, response = body }, {["X-Kong-Authz-Latency"] = (ngx.now() - start_time)} )
end

local result = body.result
if not result then
return kong.response.exit(400, { message = "Could not get result from OPA", opa_response = body })
return kong.response.exit(400, { message = "Could not get result from OPA", opa_response = body } , {["X-Kong-Authz-Latency"] = (ngx.now() - start_time)})
end

local evaluation_result_key_value = result[conf.opa_result_boolean_key]
if not (type(evaluation_result_key_value) == "boolean") then
return kong.response.exit(400, { message = "OPA response body does not contains boolean key: " .. conf.opa_result_boolean_key, opa_response_result = result })
return kong.response.exit(400, { message = "OPA response body does not contains boolean key: " .. conf.opa_result_boolean_key, opa_response_result = result } , {["X-Kong-Authz-Latency"] = (ngx.now() - start_time)})
end

if not (evaluation_result_key_value == conf.opa_result_boolean_value) then
return kong.response.exit(403, { message = "Unauthorized by OPA", opa_result = result })
return kong.response.exit(403, { message = "Unauthorized by OPA", opa_result = result }, {["X-Kong-Authz-Latency"] = (ngx.now() - start_time)})
end

kong.response.set_header("X-Kong-Authz-Latency", (ngx.now() - start_time))
end

return _M
18 changes: 6 additions & 12 deletions opa/handler.lua
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
local BasePlugin = require "kong.plugins.base_plugin"
local plugin_name = ({...})[1]:match("^kong%.plugins%.([^%.]+)")
local access = require("kong.plugins." .. plugin_name .. ".access")

local plugin = BasePlugin:extend()
local plugin = {
PRIORITY = 799,
VERSION = "0.0.2-1"
}

function plugin:new()
plugin.super.new(self, plugin_name)
function plugin:access(plugin_conf)
access.execute(plugin_conf)
end

function plugin:access(conf)
plugin.super.access(self)
access.execute(conf)
end

plugin.PRIORITY = 899
plugin.VERSION = "0.0.1-1"

return plugin
92 changes: 92 additions & 0 deletions opa/redis.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
local kong = kong
local reports = require "kong.reports"
local redis = require "resty.redis"
local cjson = require("cjson.safe").new()
cjson.decode_array_with_array_mt(true)

local _M = {}
local sock_opts = {}

local function is_present(str)
return str and str ~= "" and str ~= ngx.null
end

function _M:connection(conf)
-- https://github.com/openresty/lua-resty-redis
local redis = redis:new()
redis:set_timeout(conf.redis_timeout_in_ms)
-- use a special pool name only if redis_database is set to non-zero
-- otherwise use the default pool name host:port
sock_opts.pool = conf.redis_database and
conf.redis_host .. ":" .. conf.redis_port ..
":" .. conf.redis_database
sock_opts.backlog = 10
local ok, err = redis:connect(conf.redis_host, conf.redis_port, sock_opts)
if not ok then
kong.log.err("failed to connect to Redis: ", err)
return nil, err
end
local times, err = redis:get_reused_times()
if err then
kong.log.err("failed to get connect reused times: ", err)
return nil, err
end
if times == 0 then
if is_present(conf.redis_password) then
local ok, err = redis:auth(conf.redis_password)
if not ok then
kong.log.err("failed to auth Redis: ", err)
return nil, err
end
end
if conf.redis_database ~= 0 then
-- only calls select first time, since we know the connection is shared
-- between instances that use the same redis database
local ok, err = redis:select(conf.redis_database)
if not ok then
kong.log.err("failed to change Redis database: ", err)
return nil, err
end
end
end
return redis
end

function _M:get(redis, key, ttl, func, ...)
reports.retrieve_redis_version(redis)
local value, err = redis:get(key)
if err then
kong.log.err(" => Could not get key from Redis ... ", err)
return nil, err, nil
end

if value and value ~= ngx.null then -- if retreive value from cache and it is not null
ttl = redis:ttl(key) -- TTL -2 == key expired (the key may expire right just after the get)
end

if (value == ngx.null) or (ttl == -2) then
kong.log.debug(" => Key does not exists on Redis")
-- execute function and put it into the cache
value = func(...)

local ok, err = redis:set(key, value, "NX", "EX", ttl)
if not ok then
kong.log.err(" => Failed to set key in Redis ... ", err)
return nil, err, nil
end

if ok == ngx.null then
kong.log.err(" => Could not set key in Redis. ", err)
return nil, err, nil
end
end

local ok, err = redis:set_keepalive(10000, 100)
if not ok then
kong.log.err(" => Failed to set Redis keepalive: ", err)
end

return value, nil, ttl
end

return _M
Loading

0 comments on commit e90aabc

Please sign in to comment.