Skip to content
Open
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
4 changes: 3 additions & 1 deletion lib/resty/ldap/client.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
local bunpack = require("lua_pack").unpack
local protocol = require("resty.ldap.protocol")
local to_hex = require("resty.string").to_hex
local ok, rasn = pcall(require, "rasn")

if not ok then
Expand All @@ -13,6 +12,9 @@ local tcp = ngx.socket.tcp
local table_insert = table.insert
local string_char = string.char
local rasn_decode = rasn.decode_ldap
local to_hex = function(s)
return (s:gsub(".", function(c) return ("%02x"):format(c:byte()) end))
end


local _M = {}
Expand Down
41 changes: 14 additions & 27 deletions lib/resty/ldap/filter.lua
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
local lpeg = require("lpeg")
local P, R, S, V = lpeg.P, lpeg.R, lpeg.S, lpeg.V
local C, Ct, Cmt, Cg, Cp, Cc, Cf = lpeg.C, lpeg.Ct, lpeg.Cmt, lpeg.Cg, lpeg.Cp, lpeg.Cc, lpeg.Cf
local C, Ct, Cg, Cp, Cc, Cf = lpeg.C, lpeg.Ct, lpeg.Cg, lpeg.Cp, lpeg.Cc, lpeg.Cf

local string_char = string.char
local string_sub = string.sub
local table_insert = table.insert
local table_concat = table.concat

Expand Down Expand Up @@ -108,7 +107,7 @@ local filter = P{
ATTRIBUTE_DESCRIPTION = V'RAW_VALUE' / function(value)
return { attribute_description = value }
end,
ATTRIBUTE_VALUE = V'RAW_VALUE' / function(value)
ATTRIBUTE_VALUE = V'ASSERTION_VALUE' / function(value)
return { attribute_value = value }
end,
ATTRIBUTE_VALUE_SUBSTRING = (
Expand All @@ -135,6 +134,15 @@ local filter = P{
end,

RAW_VALUE = list(V'ESCAPED_CHARACTER' + V'UTF8_FILTERED_CHARACTER') / table_concat, -- UTF8 sequence
-- RFC 4515 UTF1SUBSET (%x01-27 / %x2B-5B / %x5D-7F) plus multi-byte UTF-8.
-- Unlike RAW_VALUE, this permits =, <, >, ~ in assertion values, as the RFC allows.
ASSERTION_VALUE = list(
V'ESCAPED_CHARACTER'
+ R("\1\39", "\43\91", "\93\127") / "%0" -- 1-byte UTF1SUBSET
+ R("\194\223") * V'UTF8_CONT' / "%0" -- 2-byte UTF-8
+ R("\224\239") * V'UTF8_CONT' * V'UTF8_CONT' / "%0" -- 3-byte UTF-8
+ R("\240\244") * V'UTF8_CONT' * V'UTF8_CONT' * V'UTF8_CONT' / "%0" -- 4-byte UTF-8
) / table_concat,
DIGIT = R'09',
WILDCARD = P'*' / function()
return { attribute_value = "*" }
Expand All @@ -153,30 +161,9 @@ local filter = P{

UTF8_CONT = R'\128\191', -- continuation byte
UTF8_FILTERED_CHARACTER =
-- Handle some cases where exclusion symbols are not need.
-- If attribute and value contain struck-out characters, but they are not part of any
-- operator, capture and skip it without enabling subsequent HEX-based matches
-- (since these characters are part of value, but HEX matches will unconditionally
-- strike them out)
Cmt(R('\0\127'), function(s, i, prev) -- match all ASCII character
-- Make sure that this character is not the last character, only then it is
-- possible to compose the operator.
if #s <= i + 1 then
return false
end

-- Ensures that the value is parsed properly if there is a separate ~ symbol in the value.
if string_sub(s, i, i) == '~' and string_sub(s, i + 1, i + 1) ~= "=" then
-- Captures and skips that ~ symbol. The previous capture element needs to be
-- added back manually.
return i + 1, prev .. '~'
end

return false -- No capture by default
end)
+ R("\0\39", "\43\59", "\63\125", "\127\127") / "%0" -- 1-byte character, remove (, ), <, >, *, =, ~
+ R("\194\223") * V'UTF8_CONT' / "%0" -- 2-byte character
+ R("\224\239") * V'UTF8_CONT' * V'UTF8_CONT' / "%0" -- 3-byte character
R("\0\39", "\43\59", "\63\125", "\127\127") / "%0" -- 1-byte character, remove (, ), *, =, <, >, ~
+ R("\194\223") * V'UTF8_CONT' / "%0" -- 2-byte character
+ R("\224\239") * V'UTF8_CONT' * V'UTF8_CONT' / "%0" -- 3-byte character
+ R("\240\244") * V'UTF8_CONT' * V'UTF8_CONT' * V'UTF8_CONT' / "%0", -- 4-byte character
}

Expand Down
69 changes: 68 additions & 1 deletion t/filter.t
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ removing the extensible match implementation from it.
local result, err = filter.compile(case.filter)
if not result then
if case.expect_err then
assert(case.expect_err == err,
assert(case.expect_err == err,
'case#' .. i .. ' error content does not match expectations, expect: '
.. case.expect_err .. ', actual: ' .. err)
else
Expand All @@ -449,3 +449,70 @@ GET /t
--- no_error_log
[error]
--- error_code: 200



=== TEST 5: assertion values containing =, <, >, ~
RFC 4515 UTF1SUBSET permits =, <, >, ~ inside assertion values even though
they are used as filter operators at the outer level. The most common
real-world case is a DN-valued attribute (e.g. member=uid=john,...).
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local filter = require("resty.ldap.filter")

local cases = {
{-- #1: DN assertion value with embedded '='
filter = '(member=uid=john,dc=example,dc=com)',
test = (function(f, m)
assert(f.item_type == 'simple', m .. 'item_type != simple, ' .. f.item_type)
assert(f.attribute_description == 'member', m .. 'attribute_description != member, ' .. f.attribute_description)
assert(f.attribute_value == 'uid=john,dc=example,dc=com', m .. 'attribute_value != uid=john,dc=example,dc=com, ' .. f.attribute_value)
end),
},
{-- #2: assertion value with embedded '<'
filter = '(cn=a<b)',
test = (function(f, m)
assert(f.item_type == 'simple', m .. 'item_type != simple, ' .. f.item_type)
assert(f.attribute_value == 'a<b', m .. 'attribute_value != a<b, ' .. f.attribute_value)
end),
},
{-- #3: assertion value with embedded '>'
filter = '(cn=a>b)',
test = (function(f, m)
assert(f.item_type == 'simple', m .. 'item_type != simple, ' .. f.item_type)
assert(f.attribute_value == 'a>b', m .. 'attribute_value != a>b, ' .. f.attribute_value)
end),
},
{-- #4: assertion value with embedded '~'
filter = '(mail=user~name@example.com)',
test = (function(f, m)
assert(f.item_type == 'simple', m .. 'item_type != simple, ' .. f.item_type)
assert(f.attribute_value == 'user~name@example.com', m .. 'attribute_value != user~name@example.com, ' .. f.attribute_value)
end),
},
{-- #5: DN assertion value inside a compound filter
filter = '(&(objectClass=groupOfNames)(member=cn=alice,ou=people,dc=example,dc=com))',
test = (function(f, m)
assert(f.op_type == 'and', m .. 'op_type != and, ' .. f.op_type)
assert(f.items[2].attribute_description == 'member', m .. 'items[2].attribute_description != member, ' .. f.items[2].attribute_description)
assert(f.items[2].attribute_value == 'cn=alice,ou=people,dc=example,dc=com', m .. 'items[2].attribute_value != cn=alice,ou=people,dc=example,dc=com, ' .. f.items[2].attribute_value)
end),
},
}

for i, case in ipairs(cases) do
local result, err = filter.compile(case.filter)
if not result then
assert(false, 'case#' .. i .. ' compile error: ' .. err)
end
case.test(result, 'case#' .. i .. ' error: ')
end
}
}
--- request
GET /t
--- no_error_log
[error]
--- error_code: 200