Skip to content

Commit

Permalink
Merge pull request #812 from 3scale/conditional-policy
Browse files Browse the repository at this point in the history
Conditional policy
  • Loading branch information
davidor authored Jul 18, 2018
2 parents f1f0f15 + f516d80 commit 9d79365
Show file tree
Hide file tree
Showing 8 changed files with 310 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- `GC` module that implements a workaround to be able to define `__gc` on tables [PR #790](https://github.com/3scale/apicast/pull/790)
- Policies can define `__gc` metamethod that gets called when they are garbage collected to do cleanup [PR #688](https://github.com/3scale/apicast/pull/688)
- Keycloak Role Check policy [PR #773](https://github.com/3scale/apicast/pull/773)
- Conditional policy. This policy includes a condition and a policy chain, and only executes the chain when the condition is true [PR #812](https://github.com/3scale/apicast/pull/812)

### Changed

Expand Down
26 changes: 26 additions & 0 deletions gateway/src/apicast/policy/conditional/apicast-policy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"$schema": "http://apicast.io/policy-v1/schema#manifest#",
"name": "Conditional",
"summary": "Executes a policy chain conditionally",
"description": [
"Evaluates a condition, and when it's true, it calls its policy chain."
],
"version": "builtin",
"configuration": {
"type": "object",
"properties": {
"condition": {
"description": "condition to be evaluated",
"type": "string"
},
"policy_chain": {
"description": "the policy chain to execute when the condition is true",
"type": "array",
"items": {
"type": "object"
}
}
},
"required": ["condition"]
}
}
57 changes: 57 additions & 0 deletions gateway/src/apicast/policy/conditional/conditional.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
local policy = require('apicast.policy')
local policy_phases = require('apicast.policy').phases
local PolicyChain = require('apicast.policy_chain')
local Engine = require('apicast.policy.conditional.engine')

local _M = policy.new('Conditional policy')

local new = _M.new

local function build_policy_chain(chain)
if not chain then return {} end

local policies = {}

for i=1, #chain do
policies[i] = PolicyChain.load_policy(
chain[i].name,
chain[i].version,
chain[i].configuration
)
end

return PolicyChain.new(policies)
end

function _M.new(config)
local self = new(config)
self.condition = config.condition or true
self.policy_chain = build_policy_chain(config.policy_chain)
return self
end

local function condition_is_true(condition)
return Engine.evaluate(condition)
end

function _M:export()
return self.policy_chain:export()
end

-- Forward policy phases to chain
for _, phase in policy_phases() do
_M[phase] = function(self, context)
if condition_is_true(self.condition) then
ngx.log(ngx.DEBUG, 'Condition met in conditional policy')
self.policy_chain[phase](self.policy_chain, context)
else
ngx.log(ngx.DEBUG, 'Condition not met in conditional policy')
end
end
end

-- To avoid calling init and init_worker more than once in the policies
_M.init = function() end
_M.init_worker = function() end

return _M
28 changes: 28 additions & 0 deletions gateway/src/apicast/policy/conditional/engine.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
local _M = {}

local value_of = {
request_method = function() return ngx.req.get_method() end,
request_host = function() return ngx.var.host end,
request_path = function() return ngx.var.uri end
}

function _M.evaluate(expression)
local match_attr = ngx.re.match(expression, [[^([\w]+)$]], 'oj')

if match_attr then
return value_of[match_attr[1]]()
end

local match_attr_and_value = ngx.re.match(expression, [[^([\w]+) == "([\w/]+)"$]], 'oj')

if not match_attr_and_value then
return nil, 'Error while parsing the condition'
end

local entity = match_attr_and_value[1]
local value = match_attr_and_value[2]

return value_of[entity]() == value
end

return _M
1 change: 1 addition & 0 deletions gateway/src/apicast/policy/conditional/init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
return require('conditional')
76 changes: 76 additions & 0 deletions spec/policy/conditional/conditional_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
local ConditionalPolicy = require('apicast.policy.conditional')
local Policy = require('apicast.policy')
local PolicyChain = require('apicast.policy_chain')
local Engine = require('apicast.policy.conditional.engine')

describe('Conditional policy', function()
local test_policy_chain
local context
local condition = "request_path == '/some_path'"

before_each(function()
test_policy_chain = PolicyChain.build({})

for _, phase in Policy.phases() do
test_policy_chain[phase] = spy.new(function() end)
end

context = {}
end)

describe('when the condition is true', function()
before_each(function()
stub(Engine, 'evaluate').returns(true)
end)

it('forwards the policy phases (except init and init_worker) to the chain', function()
local conditional = ConditionalPolicy.new({ condition = condition })

-- .new() will try to load the chain, set it here to avoid that and
-- control which one to use.
conditional.policy_chain = test_policy_chain

for _, phase in Policy.phases() do
if phase ~= 'init' and phase ~= 'init_worker' then
conditional[phase](conditional, context)

assert.spy(test_policy_chain[phase]).was_called(1)
assert.spy(test_policy_chain[phase]).was_called_with(
test_policy_chain,
context
)
end
end
end)
end)

describe('when the condition is false', function()
before_each(function()
stub(Engine, 'evaluate').returns(false)
end)

it('does not forward the policy phases to the chain', function()
local conditional = ConditionalPolicy.new({ condition = condition })
conditional.policy_chain = test_policy_chain

for _, phase in Policy.phases() do
conditional[phase](conditional, context)

assert.spy(test_policy_chain[phase]).was_not_called()
end
end)
end)

describe('.export', function()
it('forwards the method to the policy chain', function()
local exported_by_chain = { a = 1, b = 2 }

stub(test_policy_chain, 'export').returns(exported_by_chain)

local conditional = ConditionalPolicy.new({ condition = condition })
conditional.policy_chain = test_policy_chain

assert.same(exported_by_chain, conditional:export())
end)
end)
end)
34 changes: 34 additions & 0 deletions spec/policy/conditional/engine_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
local Engine = require('apicast.policy.conditional.engine')

describe('Engine', function()
describe('.evaluate', function()
it('evaluates "request_method"', function()
stub(ngx.req, 'get_method', function () return 'GET' end)

assert.equals('GET', Engine.evaluate('request_method'))
end)

it('evaluates "request_host"', function()
ngx.var = { host = 'localhost' }

assert.equals('localhost', Engine.evaluate('request_host'))
end)

it('evaluates "request_path"', function()
ngx.var = { uri = '/some_path' }

assert.equals('/some_path', Engine.evaluate('request_path'))
end)

it('evaluates "=="', function()
stub(ngx.req, 'get_method', function () return 'GET' end)

assert.is_true(Engine.evaluate('request_method == "GET"'))
assert.is_false(Engine.evaluate('request_method == "POST"'))
end)

it('returns nil for expressions that cannot be evaluated', function()
assert.is_nil(Engine.evaluate('request_method <> "GET"'))
end)
end)
end)
87 changes: 87 additions & 0 deletions t/apicast-policy-conditional.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use lib 't';
use Test::APIcast::Blackbox 'no_plan';

run_tests();

__DATA__
=== TEST 1: Conditional policy calls its chain when the condition is true
In order to test this, we define a conditional policy that only runs the
phase_logger policy when the request path is /log.
We know that the policy outputs "running phase: some_phase" for each of the
phases it runs, so we can use that to verify it was executed.
--- configuration
{
"services": [
{
"id": 42,
"proxy": {
"policy_chain": [
{
"name": "apicast.policy.conditional",
"configuration": {
"condition": "request_path == \"/log\"",
"policy_chain": [
{
"name": "apicast.policy.phase_logger"
}
]
}
},
{
"name": "apicast.policy.echo"
}
]
}
}
]
}
--- request
GET /log
--- response_body
GET /log HTTP/1.1
--- error_code: 200
--- no_error_log
[error]
--- error_log chomp
running phase: rewrite
=== TEST 2: Conditional policy does not call its chain when the condition is false
In order to test this, we define a conditional policy that only runs the
phase_logger policy when the request path is /log.
We know that the policy outputs "running phase: some_phase" for each of the
phases it runs, so we can use that to verify that it was not executed.
--- configuration
{
"services": [
{
"id": 42,
"proxy": {
"policy_chain": [
{
"name": "apicast.policy.conditional",
"configuration": {
"condition": "request_path == \"/log\"",
"policy_chain": [
{
"name": "apicast.policy.phase_logger"
}
]
}
},
{
"name": "apicast.policy.echo"
}
]
}
}
]
}
--- request
GET /
--- response_body
GET / HTTP/1.1
--- error_code: 200
--- no_error_log
[error]
running phase: rewrite

0 comments on commit 9d79365

Please sign in to comment.