Skip to content

Commit

Permalink
Merge pull request #724 from 3scale/querystring-ops-url-rewriting-policy
Browse files Browse the repository at this point in the history
URL rewriting policy: Allow to modify query parameters
  • Loading branch information
davidor authored May 29, 2018
2 parents 28736b4 + bf094e5 commit 12bc55e
Show file tree
Hide file tree
Showing 7 changed files with 819 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Generate new policy scaffold from the CLI [PR #682](https://github.com/3scale/apicast/pull/682)
- 3scale batcher policy [PR #685](https://github.com/3scale/apicast/pull/685), [PR #710](https://github.com/3scale/apicast/pull/710)
- Liquid templating support in the headers policy configuration [PR #716](https://github.com/3scale/apicast/pull/716)
- Ability to modify query parameters in the URL rewriting policy [PR #724](https://github.com/3scale/apicast/pull/724)

### Changed

Expand Down
55 changes: 55 additions & 0 deletions gateway/src/apicast/policy/url_rewriting/apicast-policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,61 @@
},
"required": ["op", "regex", "replace"]
}
},
"query_args_commands": {
"description": "List of commands to apply to the query string args",
"type": "array",
"items": {
"type": "object",
"properties": {
"op": {
"description": "Operation to apply to the query argument",
"type": "string",
"oneOf": [
{
"enum": ["add"],
"title": "Add a value to an existing argument"
},
{
"enum": ["set"],
"title": "Create the arg when not set, replace its value when set"
},
{
"enum": ["push"],
"title": "Create the arg when not set, add the value when set"
},
{
"enum": ["delete"],
"title": "Delete an arg"
}
]
},
"arg": {
"description": "Query argument",
"type": "string"
},
"value": {
"description": "Value",
"type": "string"
},
"value_type": {
"description": "How to evaluate 'value'",
"type": "string",
"oneOf": [
{
"enum": ["plain"],
"title": "Evaluate 'value' as plain text."
},
{
"enum": ["liquid"],
"title": "Evaluate 'value' as liquid."
}
],
"default": "plain"
}
}
},
"required": ["op", "arg", "value"]
}
}
}
Expand Down
89 changes: 89 additions & 0 deletions gateway/src/apicast/policy/url_rewriting/query_params.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
local setmetatable = setmetatable
local insert = table.insert
local type = type

local _M = {}

local mt = { __index = _M }

-- Note: This module calls 'ngx.req.set_uri_args()' on each operation
-- If this becomes too costly, we can change it by exposing a method that calls
-- 'ngx.req.set_uri_args()' leaving that responsibility to the caller.

--- Initialize a new QueryStringParams
-- @tparam[opt] table uri_args URI arguments
function _M.new(uri_args)
local self = setmetatable({}, mt)

if uri_args then
self.args = uri_args
else
local get_args_err
self.args, get_args_err = ngx.req.get_uri_args()

if not self.args then
ngx.log(ngx.ERR, 'Error while getting URI args: ', get_args_err)
return nil, get_args_err
end
end

return self
end

--- Updates the URI args
local function update_uri_args(args)
ngx.req.set_uri_args(args)
end

local function add_to_existing_arg(self, arg, value)
-- When the argument has a single value, it is a string and needs to be
-- converted to table so we can add a second one.
if type(self.args[arg]) ~= 'table' then
self.args[arg] = { self.args[arg] }
end

insert(self.args[arg], value)
end

--- Pushes a value to an argument
-- 1) When the arg is not set, creates it with the given value.
-- 2) When the arg is set, it adds a new value for it (becomes an array if it
-- was not one already).
function _M:push(arg, value)
if not self.args[arg] then
self.args[arg] = value
else
add_to_existing_arg(self, arg, value)
end

update_uri_args(self.args)
end

--- Set a value for an argument
-- 1) When the arg is not set, creates it with the given value.
-- 2) When the arg is set, replaces its value with the given one.
function _M:set(arg, value)
self.args[arg] = value
update_uri_args(self.args)
end

--- Adds a value for an argument
-- 1) When the arg is not set, it does nothing.
-- 2) When the arg is set, it adds a new value for it (becomes an array if it
-- was not one already).
function _M:add(arg, value)
if self.args[arg] then
add_to_existing_arg(self, arg, value)
update_uri_args(self.args)
end
end

--- Deletes an argument
function _M:delete(arg)
if self.args[arg] then
self.args[arg] = nil
update_uri_args(self.args)
end
end

return _M
48 changes: 46 additions & 2 deletions gateway/src/apicast/policy/url_rewriting/url_rewriting.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ local ipairs = ipairs
local sub = ngx.re.sub
local gsub = ngx.re.gsub

local QueryParams = require 'apicast.policy.url_rewriting.query_params'
local TemplateString = require 'apicast.template_string'
local default_value_type = 'plain'

local policy = require('apicast.policy')
local _M = policy.new('URL rewriting policy')

Expand Down Expand Up @@ -42,8 +46,31 @@ local function apply_rewrite_command(command)
return changed
end

local function apply_query_arg_command(command, query_args, context)
-- Possible values of command.op match the methods defined in QueryArgsParams
local func = query_args[command.op]

if not func then
ngx.log(ngx.ERR, 'Invalid query args operation: ', command.op)
return
end

local value = (command.template_string and command.template_string:render(context)) or nil
func(query_args, command.arg, value)
end

local function build_template(query_arg_command)
if query_arg_command.value then -- The 'delete' op does not have a value
query_arg_command.template_string = TemplateString.new(
query_arg_command.value,
query_arg_command.value_type or default_value_type
)
end
end

--- Initialize a URL rewriting policy
-- @tparam[opt] table config Contains the rewrite commands.
-- @tparam[opt] table config Contains two tables: the rewrite commands and the
-- query args commands.
-- The rewrite commands are based on the 'ngx.re.sub' and 'ngx.re.gsub'
-- functions provided by OpenResty. Please check
-- https://github.com/openresty/lua-nginx-module for more details.
Expand All @@ -56,20 +83,37 @@ end
-- Accepted options are the ones in 'ngx.re.sub' and 'ngx.re.gsub'.
-- - break[opt]: defaults to false. When set to true, if the command rewrote
-- the URL, it will be the last command applied.
--
-- Each query arg command is a table with the following fields:
--
-- - op: can be 'push', 'set', 'add', and 'delete'.
-- - arg: query argument.
-- - value: value to be added, replaced, or set.
function _M.new(config)
local self = new(config)
self.commands = (config and config.commands) or {}

self.query_args_commands = (config and config.query_args_commands) or {}
for _, query_arg_command in ipairs(self.query_args_commands) do
build_template(query_arg_command)
end

return self
end

function _M:rewrite()
function _M:rewrite(context)
for _, command in ipairs(self.commands) do
local rewritten = apply_rewrite_command(command)

if rewritten and command['break'] then
break
end
end

self.query_args = QueryParams.new()
for _, query_arg_command in ipairs(self.query_args_commands) do
apply_query_arg_command(query_arg_command, self.query_args, context)
end
end

return _M
127 changes: 127 additions & 0 deletions spec/policy/url_rewriting/query_params_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
local QueryParams = require('apicast.policy.url_rewriting.query_params')

describe('QueryParams', function()
before_each(function()
stub(ngx.req, 'set_uri_args')
end)

describe('.push', function()
describe('if the arg is not in the query', function()
it('creates it with the given value', function()
local params = QueryParams.new({ a = '1' })

params:push('b', '2')

local expected_args = { a = '1', b = '2' }
assert.stub(ngx.req.set_uri_args).was_called_with(expected_args)
end)
end)

describe('if the arg is in the query', function()
describe('and it has a single value', function()
it('adds a new value for it', function()
local params = QueryParams.new({ a = '1' })

params:push('a', '2')

local expected_args = { a = { '1', '2' } }
assert.stub(ngx.req.set_uri_args).was_called_with(expected_args)
end)
end)

describe('and it is an array', function()
it('adds a new value for it', function()
local params = QueryParams.new({ a = { '1', '2' } })

params:push('a', '3')

local expected_args = { a = { '1', '2', '3' } }
assert.stub(ngx.req.set_uri_args).was_called_with(expected_args)
end)
end)
end)
end)

describe('.set', function()
describe('if the arg is not in the query', function()
it('creates it with the given value', function()
local params = QueryParams.new({ a = '1' })

params:set('b', '2')

local expected_args = { a = '1', b = '2' }
assert.stub(ngx.req.set_uri_args).was_called_with(expected_args)
end)
end)

describe('if the arg is in the query', function()
it('replaces its value with the given one', function()
local params = QueryParams.new({ a = { '1', '2' } })

params:set('a', '3')

local expected_args = { a = '3' }
assert.stub(ngx.req.set_uri_args).was_called_with(expected_args)
end)
end)
end)

describe('.add', function()
describe('if the arg is not in the query', function()
it('does nothing', function()
local params = QueryParams.new({ a = '1' })

params:add('b', '2')

assert.stub(ngx.req.set_uri_args).was_not_called()
end)
end)

describe('if the arg is in the query', function()
describe('and it has a single value', function()
it('adds a new value for it', function()
local params = QueryParams.new({ a = '1' })

params:add('a', '2')

local expected_args = { a = { '1', '2' } }
assert.stub(ngx.req.set_uri_args).was_called_with(expected_args)
end)
end)

describe('and it is an array', function()
it('adds a new value for it', function()
local params = QueryParams.new({ a = { '1', '2' } })

params:add('a', '3')

local expected_args = { a = { '1', '2', '3' } }
assert.stub(ngx.req.set_uri_args).was_called_with(expected_args)
end)
end)
end)
end)

describe('.delete', function()
describe('if the argument is in the query', function()
it('deletes it', function()
local params = QueryParams.new({ a = '1', b = '2' })

params:delete('a')

local expected_args = { b = '2' }
assert.stub(ngx.req.set_uri_args).was_called_with(expected_args)
end)
end)

describe('if the argument is not in the query', function()
it('does not delete anything', function()
local params = QueryParams.new({ a = '1' })

params:delete('b')

assert.stub(ngx.req.set_uri_args).was_not_called()
end)
end)
end)
end)
Loading

0 comments on commit 12bc55e

Please sign in to comment.