Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conditional policy #812

Merged
merged 6 commits into from
Jul 18, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is not exactly what we need.

}
}
},
"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