blob: e55f22712e41d716fdc1a975b10cb99bb13d2223 [file]
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
use t::APISIX 'no_plan';
repeat_each(1);
no_long_string();
no_root_location();
no_shuffle();
add_block_preprocessor(sub {
my ($block) = @_;
if (!$block->request) {
$block->set_value("request", "GET /t");
}
});
run_tests();
__DATA__
=== TEST 1: minimal valid conf (ldap_uri + base_dn) passes
--- config
location /t {
content_by_lua_block {
local plugin = require("apisix.plugins.ldap-auth-advanced")
local ok, err = plugin.check_schema({
ldap_uri = "127.0.0.1:1389",
base_dn = "ou=users,dc=example,dc=org",
})
ngx.say(ok and "passed" or err)
}
}
--- response_body
passed
=== TEST 2: missing ldap_uri is rejected
--- config
location /t {
content_by_lua_block {
local plugin = require("apisix.plugins.ldap-auth-advanced")
local ok, err = plugin.check_schema({
base_dn = "ou=users,dc=example,dc=org",
})
ngx.say(ok and "passed" or err)
}
}
--- response_body_like eval
qr/property "ldap_uri" is required/
=== TEST 3: missing base_dn is rejected
--- config
location /t {
content_by_lua_block {
local plugin = require("apisix.plugins.ldap-auth-advanced")
local ok, err = plugin.check_schema({
ldap_uri = "127.0.0.1:1389",
})
ngx.say(ok and "passed" or err)
}
}
--- response_body_like eval
qr/property "base_dn" is required/
=== TEST 4: use_ldaps and use_starttls are mutually exclusive
--- config
location /t {
content_by_lua_block {
local plugin = require("apisix.plugins.ldap-auth-advanced")
local ok, err = plugin.check_schema({
ldap_uri = "127.0.0.1:1389",
base_dn = "ou=users,dc=example,dc=org",
use_ldaps = true,
use_starttls = true,
})
ngx.say(ok and "passed" or err)
}
}
--- response_body
use_ldaps and use_starttls are mutually exclusive
=== TEST 5: use_ldaps alone (port-less uri) passes
--- config
location /t {
content_by_lua_block {
local plugin = require("apisix.plugins.ldap-auth-advanced")
local ok, err = plugin.check_schema({
ldap_uri = "ldap.example.org",
base_dn = "ou=users,dc=example,dc=org",
use_ldaps = true,
})
ngx.say(ok and "passed" or err)
}
}
--- response_body
passed
=== TEST 6: bind_dn set without ldap_password is rejected
--- config
location /t {
content_by_lua_block {
local plugin = require("apisix.plugins.ldap-auth-advanced")
local ok, err = plugin.check_schema({
ldap_uri = "127.0.0.1:1389",
base_dn = "ou=users,dc=example,dc=org",
bind_dn = "cn=admin,dc=example,dc=org",
})
ngx.say(ok and "passed" or err)
}
}
--- response_body
ldap_password is required when bind_dn is set
=== TEST 7: bind_dn with ldap_password passes
--- config
location /t {
content_by_lua_block {
local plugin = require("apisix.plugins.ldap-auth-advanced")
local ok, err = plugin.check_schema({
ldap_uri = "127.0.0.1:1389",
base_dn = "ou=users,dc=example,dc=org",
bind_dn = "cn=admin,dc=example,dc=org",
ldap_password = "adminpassword",
})
ngx.say(ok and "passed" or err)
}
}
--- response_body
passed
=== TEST 8: attribute with a bad pattern is rejected
--- config
location /t {
content_by_lua_block {
local plugin = require("apisix.plugins.ldap-auth-advanced")
local ok, err = plugin.check_schema({
ldap_uri = "127.0.0.1:1389",
base_dn = "ou=users,dc=example,dc=org",
attribute = "1abc",
})
ngx.say(ok and "passed" or "rejected")
}
}
--- response_body
rejected
=== TEST 9: attribute containing a space is rejected
--- config
location /t {
content_by_lua_block {
local plugin = require("apisix.plugins.ldap-auth-advanced")
local ok, err = plugin.check_schema({
ldap_uri = "127.0.0.1:1389",
base_dn = "ou=users,dc=example,dc=org",
attribute = "a b",
})
ngx.say(ok and "passed" or "rejected")
}
}
--- response_body
rejected
=== TEST 10: valid attribute (uid) passes
--- config
location /t {
content_by_lua_block {
local plugin = require("apisix.plugins.ldap-auth-advanced")
local ok, err = plugin.check_schema({
ldap_uri = "127.0.0.1:1389",
base_dn = "ou=users,dc=example,dc=org",
attribute = "uid",
})
ngx.say(ok and "passed" or err)
}
}
--- response_body
passed
=== TEST 11: size_limit below the floor of 2 is rejected
--- config
location /t {
content_by_lua_block {
local plugin = require("apisix.plugins.ldap-auth-advanced")
local ok, err = plugin.check_schema({
ldap_uri = "127.0.0.1:1389",
base_dn = "ou=users,dc=example,dc=org",
size_limit = 1,
})
ngx.say(ok and "passed" or "rejected")
}
}
--- response_body
rejected
=== TEST 12: consumer schema with user_dn passes
--- config
location /t {
content_by_lua_block {
local core = require("apisix.core")
local plugin = require("apisix.plugins.ldap-auth-advanced")
local ok, err = plugin.check_schema(
{ user_dn = "cn=user01,ou=users,dc=example,dc=org" },
core.schema.TYPE_CONSUMER)
ngx.say(ok and "passed" or err)
}
}
--- response_body
passed
=== TEST 13: empty consumer schema is rejected
--- config
location /t {
content_by_lua_block {
local core = require("apisix.core")
local plugin = require("apisix.plugins.ldap-auth-advanced")
local ok, err = plugin.check_schema({}, core.schema.TYPE_CONSUMER)
ngx.say(ok and "passed" or "rejected")
}
}
--- response_body
rejected
=== TEST 14: set up a route protected by ldap-auth-advanced (live LDAP)
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/routes/1',
ngx.HTTP_PUT,
[[{
"plugins": {
"ldap-auth-advanced": {
"ldap_uri": "127.0.0.1:1389",
"base_dn": "ou=users,dc=example,dc=org",
"attribute": "uid"
}
},
"upstream": {
"nodes": {
"127.0.0.1:1980": 1
},
"type": "roundrobin"
},
"uri": "/hello"
}]]
)
if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- response_body
passed
=== TEST 15: no credential header -> 401 with WWW-Authenticate ldap realm
--- request
GET /hello
--- error_code: 401
--- response_headers
WWW-Authenticate: ldap realm="ldap"
=== TEST 16: malformed base64 payload ("aca_a" does not decode) -> 401
--- request
GET /hello
--- more_headers
Authorization: ldap aca_a
--- error_code: 401
=== TEST 17: base64 payload without a ':' separator (decodes to "useronly") -> 401
--- request
GET /hello
--- more_headers
Authorization: ldap dXNlcm9ubHk=
--- error_code: 401
=== TEST 18: empty password (payload decodes to "user01:") rejected before any bind (RFC 4513 5.1.2 unauthenticated bind)
--- request
GET /hello
--- more_headers
Authorization: ldap dXNlcjAxOg==
--- error_code: 401
--- grep_error_log eval
qr/empty password/
--- grep_error_log_out
empty password
=== TEST 19: scheme word parsed case-insensitively (uppercase), empty password still rejected (creds: user01:)
--- request
GET /hello
--- more_headers
Authorization: LDAP dXNlcjAxOg==
--- error_code: 401
--- grep_error_log eval
qr/empty password/
--- grep_error_log_out
empty password
=== TEST 20: scheme word parsed case-insensitively (mixed case), empty password still rejected (creds: user01:)
--- request
GET /hello
--- more_headers
Authorization: lDaP dXNlcjAxOg==
--- error_code: 401
--- grep_error_log eval
qr/empty password/
--- grep_error_log_out
empty password
=== TEST 21: set up a route with header_type basic
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/routes/1',
ngx.HTTP_PUT,
[[{
"plugins": {
"ldap-auth-advanced": {
"ldap_uri": "127.0.0.1:1389",
"base_dn": "ou=users,dc=example,dc=org",
"attribute": "uid",
"header_type": "basic"
}
},
"upstream": {
"nodes": {
"127.0.0.1:1980": 1
},
"type": "roundrobin"
},
"uri": "/hello"
}]]
)
if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- response_body
passed
=== TEST 22: header_type basic emits a Basic-scheme WWW-Authenticate
--- request
GET /hello
--- error_code: 401
--- response_headers
WWW-Authenticate: Basic realm="ldap"
=== TEST 23: inbound identity headers are cleared before any auth work, on every path
--- config
location /t {
content_by_lua_block {
local core = require("apisix.core")
local plugin = require("apisix.plugins.ldap-auth-advanced")
local headers = {
"X-Authenticated-Groups", "X-Consumer-Username",
"X-Credential-Identifier", "X-Consumer-Custom-ID",
}
local ctx = { var = {} }
local before = {}
for i, h in ipairs(headers) do
before[i] = core.request.header(ctx, h) or "nil"
end
-- no credential header -> auth_failed path; the strip must still happen
plugin.rewrite({ header_type = "ldap", realm = "ldap" }, ctx)
local after = {}
for i, h in ipairs(headers) do
after[i] = core.request.header(ctx, h) or "nil"
end
ngx.say("before: ", table.concat(before, " "))
ngx.say("after: ", table.concat(after, " "))
}
}
--- more_headers
X-Authenticated-Groups: injected
X-Consumer-Username: spoofed-user
X-Credential-Identifier: spoofed-cred
X-Consumer-Custom-ID: spoofed-id
--- response_body
before: injected spoofed-user spoofed-cred spoofed-id
after: nil nil nil nil
=== TEST 24: set up a route with multi-auth wrapping ldap-auth-advanced and basic-auth
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/routes/1',
ngx.HTTP_PUT,
[[{
"plugins": {
"multi-auth": {
"auth_plugins": [
{
"ldap-auth-advanced": {
"ldap_uri": "127.0.0.1:1389",
"base_dn": "ou=users,dc=example,dc=org",
"attribute": "uid"
}
},
{
"basic-auth": {}
}
]
}
},
"upstream": {
"nodes": {
"127.0.0.1:1980": 1
},
"type": "roundrobin"
},
"uri": "/hello"
}]]
)
if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- response_body
passed
=== TEST 25: under multi-auth ldap-auth-advanced declines quietly (creds: user01:)
--- request
GET /hello
--- more_headers
Authorization: ldap dXNlcjAxOg==
--- error_code: 401
--- response_body
{"message":"Authorization Failed"}
--- response_headers
WWW-Authenticate: Basic realm="basic"
--- no_error_log
empty password
=== TEST 26: set up the plain search-then-bind route (uid attribute)
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/routes/1',
ngx.HTTP_PUT,
[[{
"plugins": {
"ldap-auth-advanced": {
"ldap_uri": "127.0.0.1:1389",
"base_dn": "ou=users,dc=example,dc=org",
"attribute": "uid",
"keepalive_pool_size": 4
}
},
"upstream": {
"nodes": {
"127.0.0.1:1980": 1
},
"type": "roundrobin"
},
"uri": "/hello"
}]]
)
if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- response_body
passed
=== TEST 27: consumer_required (default true) with NO matching Consumer -> 401 (fails closed) (creds: user01:password1)
--- request
GET /hello
--- more_headers
Authorization: ldap dXNlcjAxOnBhc3N3b3JkMQ==
--- error_code: 401
--- grep_error_log eval
qr/no Consumer is configured/
--- grep_error_log_out
no Consumer is configured
=== TEST 28: create the ldap-auth-advanced Consumers (user_dn associations)
--- config
location /t {
content_by_lua_block {
local core = require("apisix.core")
local t = require("lib.test_admin").test
local users = {
{ name = "ldapadvuser01", dn = "cn=user01,ou=users,dc=example,dc=org" },
{ name = "ldapadvuser02", dn = "cn=user02,ou=users,dc=example,dc=org" },
{ name = "ldapadvjdoe", dn = "cn=Jane Doe,ou=users,dc=example,dc=org" },
{ name = "ldapadvsecret", dn = "cn=Secret User,ou=users,dc=example,dc=org" },
}
for _, u in ipairs(users) do
local code, body = t('/apisix/admin/consumers',
ngx.HTTP_PUT,
core.json.encode({
username = u.name,
plugins = {
["ldap-auth-advanced"] = { user_dn = u.dn },
},
}))
if code >= 300 then
ngx.status = code
ngx.say(body)
return
end
end
ngx.say("passed")
}
}
--- response_body
passed
=== TEST 29: happy path -- uid=user01 (cn=user01) matches Consumer ldapadvuser01 (200 + attached) (creds: user01:password1)
--- request
GET /hello
--- more_headers
Authorization: ldap dXNlcjAxOnBhc3N3b3JkMQ==
--- error_code: 200
--- response_body
hello world
--- error_log
find consumer ldapadvuser01
=== TEST 30: AD-shape happy path -- uid=jdoe (cn=Jane Doe) matches Consumer ldapadvjdoe (200) (creds: jdoe:janesecret)
--- request
GET /hello
--- more_headers
Authorization: ldap amRvZTpqYW5lc2VjcmV0
--- error_code: 200
--- response_body
hello world
--- error_log
find consumer ldapadvjdoe
=== TEST 31: wrong password -> 401 (a result-code failure, not a transport error) (creds: user01:wrong)
--- request
GET /hello
--- more_headers
Authorization: ldap dXNlcjAxOndyb25n
--- error_code: 401
=== TEST 32: unknown user -> 401 (the user search returns 0 entries) (creds: nouser:x)
--- request
GET /hello
--- more_headers
Authorization: ldap bm91c2VyOng=
--- error_code: 401
=== TEST 33: ambiguous match (two uid=dupuser entries) -> 401 + "ambiguous" warn (creds: dupuser:duppass1)
--- request
GET /hello
--- more_headers
Authorization: ldap ZHVwdXNlcjpkdXBwYXNzMQ==
--- error_code: 401
--- grep_error_log eval
qr/ambiguous user match/
--- grep_error_log_out
ambiguous user match
=== TEST 34: set up the search-then-bind route with consumer_required=false
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/routes/1',
ngx.HTTP_PUT,
[[{
"plugins": {
"ldap-auth-advanced": {
"ldap_uri": "127.0.0.1:1389",
"base_dn": "ou=users,dc=example,dc=org",
"attribute": "uid",
"consumer_required": false
}
},
"upstream": {
"nodes": {
"127.0.0.1:1980": 1
},
"type": "roundrobin"
},
"uri": "/hello"
}]]
)
if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- response_body
passed
=== TEST 35: consumer_required=false -> user01 authenticated, no Consumer attached (200) (creds: user01:password1)
--- request
GET /hello
--- more_headers
Authorization: ldap dXNlcjAxOnBhc3N3b3JkMQ==
--- error_code: 200
--- response_body
hello world
--- no_error_log
find consumer
=== TEST 36: point the route at a dead LDAP port (transport-error case)
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/routes/1',
ngx.HTTP_PUT,
[[{
"plugins": {
"ldap-auth-advanced": {
"ldap_uri": "127.0.0.1:1390",
"base_dn": "ou=users,dc=example,dc=org",
"attribute": "uid",
"timeout": 1000
}
},
"upstream": {
"nodes": {
"127.0.0.1:1980": 1
},
"type": "roundrobin"
},
"uri": "/hello"
}]]
)
if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- response_body
passed
=== TEST 37: LDAP unreachable -> 500 (a transport error is never an auth failure) (creds: user01:password1)
--- request
GET /hello
--- more_headers
Authorization: ldap dXNlcjAxOnBhc3N3b3JkMQ==
--- error_code: 500
--- error_log
LDAP connect failed
=== TEST 38: set up an LDAPS route on 1636 (use_ldaps, ssl_verify off)
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/routes/1',
ngx.HTTP_PUT,
[[{
"plugins": {
"ldap-auth-advanced": {
"ldap_uri": "127.0.0.1:1636",
"base_dn": "ou=users,dc=example,dc=org",
"attribute": "uid",
"use_ldaps": true,
"ssl_verify": false
}
},
"upstream": {
"nodes": {
"127.0.0.1:1980": 1
},
"type": "roundrobin"
},
"uri": "/hello"
}]]
)
if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- response_body
passed
=== TEST 39: happy path over LDAPS (200) (creds: user01:password1)
--- request
GET /hello
--- more_headers
Authorization: ldap dXNlcjAxOnBhc3N3b3JkMQ==
--- error_code: 200
--- response_body
hello world
=== TEST 40: set up a StartTLS route on 1389 (use_starttls, ssl_verify off)
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/routes/1',
ngx.HTTP_PUT,
[[{
"plugins": {
"ldap-auth-advanced": {
"ldap_uri": "127.0.0.1:1389",
"base_dn": "ou=users,dc=example,dc=org",
"attribute": "uid",
"use_starttls": true,
"ssl_verify": false
}
},
"upstream": {
"nodes": {
"127.0.0.1:1980": 1
},
"type": "roundrobin"
},
"uri": "/hello"
}]]
)
if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- response_body
passed
=== TEST 41: happy path over StartTLS (200) (creds: user01:password1)
--- request
GET /hello
--- more_headers
Authorization: ldap dXNlcjAxOnBhc3N3b3JkMQ==
--- error_code: 200
--- response_body
hello world
=== TEST 42: restore the plain search-then-bind route for the injection suite
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/routes/1',
ngx.HTTP_PUT,
[[{
"plugins": {
"ldap-auth-advanced": {
"ldap_uri": "127.0.0.1:1389",
"base_dn": "ou=users,dc=example,dc=org",
"attribute": "uid"
}
},
"upstream": {
"nodes": {
"127.0.0.1:1980": 1
},
"type": "roundrobin"
},
"uri": "/hello"
}]]
)
if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- response_body
passed
=== TEST 43: filter-injection usernames each 401 (none widens the search)
--- config
location /t {
content_by_lua_block {
local http = require("resty.http")
local port = ngx.var.server_port
-- RFC 4515 s3 specials plus a NUL byte. Once escaped, every payload
-- is a literal that matches NO user, so each 401s via the "user not
-- found" path. The log assertions below are the real proof of
-- escaping: an UNescaped "*" would build the presence filter (uid=*),
-- match >1 entry, and 401 via the "ambiguous user match" path instead
-- -- so we assert "user not found" IS logged and "ambiguous user
-- match" is NOT (status 401 alone cannot tell the two paths apart).
local injections = { "*", "*)(objectClass=*", "(", ")", "\\", "\0" }
local all_401 = true
local statuses = {}
for _, u in ipairs(injections) do
local hc = http.new()
local cred = ngx.encode_base64(u .. ":x")
local res, err = hc:request_uri(
"http://127.0.0.1:" .. port .. "/hello",
{ headers = { ["Authorization"] = "ldap " .. cred } })
local st = res and res.status or ("ERR:" .. tostring(err))
statuses[#statuses + 1] = tostring(st)
if st ~= 401 then all_401 = false end
end
ngx.say("all_401: ", tostring(all_401))
ngx.say("statuses: ", table.concat(statuses, ","))
}
}
--- response_body
all_401: true
statuses: 401,401,401,401,401,401
--- error_log
user not found
--- no_error_log
ambiguous user match
=== TEST 44: username with an invalid UTF-8 byte -> clean 401, never a 500
--- config
location /t {
content_by_lua_block {
local http = require("resty.http")
local port = ngx.var.server_port
-- 0xFF is a lone high byte: never valid UTF-8. filter.escape leaves it
-- untouched, so if it reached the search the library's filter grammar would
-- reject it as a "syntax error", which the plugin's non-result-code
-- branch turns into HTTP 500. A malformed username is a bad credential
-- (a client error), so it must be rejected up front as a clean 401 --
-- never surfaced as a server-side 500.
local hc = http.new()
local cred = ngx.encode_base64(string.char(0xff) .. ":x")
local res, err = hc:request_uri(
"http://127.0.0.1:" .. port .. "/hello",
{ headers = { ["Authorization"] = "ldap " .. cred } })
ngx.say("status: ", res and res.status or ("ERR:" .. tostring(err)))
}
}
--- response_body
status: 401
--- error_log
invalid username
--- no_error_log
LDAP user search failed
=== TEST 45: well-formed multibyte UTF-8 username reaches the search and 401s as not-found
--- config
location /t {
content_by_lua_block {
local http = require("resty.http")
local port = ngx.var.server_port
-- "h" + U+00E9 (e-acute, bytes 0xC3 0xA9) + "llo": valid 2-byte UTF-8.
-- It must pass the encoding check and reach the search, where it
-- matches no user -> the "user not found" 401 path. This proves valid
-- multibyte input is NOT rejected as bad encoding (only invalid byte
-- sequences are).
local hc = http.new()
local username = "h" .. string.char(0xc3, 0xa9) .. "llo"
local cred = ngx.encode_base64(username .. ":x")
local res, err = hc:request_uri(
"http://127.0.0.1:" .. port .. "/hello",
{ headers = { ["Authorization"] = "ldap " .. cred } })
ngx.say("status: ", res and res.status or ("ERR:" .. tostring(err)))
}
}
--- response_body
status: 401
--- error_log
user not found
--- no_error_log
invalid username
=== TEST 46: username with a trailing '~' reaches the search and 401s as not-found
--- config
location /t {
content_by_lua_block {
local http = require("resty.http")
local port = ngx.var.server_port
-- RFC 4515 UTF1SUBSET (%x5D-7F) includes '~' (0x7e), so "admin~" is a
-- legal assertion value and a legal directory username. The filter
-- grammar only tolerates a raw '~' mid-value, so filter.escape emits
-- it as "\7e"; the compiler un-escapes that back to '~' and the search
-- goes out with the exact username. It must therefore reach the search
-- and take the "user not found" 401 path -- never be turned away by the
-- compile pre-check, which is reserved for input the grammar genuinely
-- cannot express (e.g. invalid UTF-8, TEST 44).
local hc = http.new()
local cred = ngx.encode_base64("admin~:x")
local res, err = hc:request_uri(
"http://127.0.0.1:" .. port .. "/hello",
{ headers = { ["Authorization"] = "ldap " .. cred } })
ngx.say("status: ", res and res.status or ("ERR:" .. tostring(err)))
}
}
--- response_body
status: 401
--- error_log
user not found
--- no_error_log
invalid username
=== TEST 47: set up the three concurrent-probe routes (churn + anon-secret + svc-secret)
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
-- route 1 (/hello): bind_dn UNSET, the pool-churn route.
local code = t('/apisix/admin/routes/1', ngx.HTTP_PUT, [[{
"plugins": { "ldap-auth-advanced": {
"ldap_uri": "127.0.0.1:1389",
"base_dn": "ou=users,dc=example,dc=org",
"attribute": "uid",
"keepalive_pool_size": 4
} },
"upstream": { "nodes": { "127.0.0.1:1980": 1 }, "type": "roundrobin" },
"uri": "/hello"
}]])
-- route 2 (/uri): bind_dn UNSET -- searches anonymously (Route A).
local code2 = t('/apisix/admin/routes/2', ngx.HTTP_PUT, [[{
"plugins": { "ldap-auth-advanced": {
"ldap_uri": "127.0.0.1:1389",
"base_dn": "ou=users,dc=example,dc=org",
"attribute": "uid",
"keepalive_pool_size": 4
} },
"upstream": { "nodes": { "127.0.0.1:1980": 1 }, "type": "roundrobin" },
"uri": "/uri"
}]])
-- route 3 (/hello1): bind_dn SET -- searches as the service account (Route B).
local code3 = t('/apisix/admin/routes/3', ngx.HTTP_PUT, [[{
"plugins": { "ldap-auth-advanced": {
"ldap_uri": "127.0.0.1:1389",
"base_dn": "ou=users,dc=example,dc=org",
"attribute": "uid",
"bind_dn": "cn=admin,dc=example,dc=org",
"ldap_password": "adminpassword",
"keepalive_pool_size": 4
} },
"upstream": { "nodes": { "127.0.0.1:1980": 1 }, "type": "roundrobin" },
"uri": "/hello1"
}]])
local ok = code < 300 and code2 < 300 and code3 < 300
ngx.say(ok and "passed" or "failed")
}
}
--- response_body
passed
=== TEST 48: CONCURRENT bind-state-leak probe: anon re-bind must not leak
--- config
location /probe {
content_by_lua_block {
local http = require("resty.http")
local args = ngx.req.get_uri_args()
local hc = http.new()
local cred = ngx.encode_base64(args.u .. ":" .. args.p)
local res, err = hc:request_uri(
"http://127.0.0.1:" .. ngx.var.server_port .. args.path,
{ headers = { ["Authorization"] = "ldap " .. cred } })
ngx.print(res and tostring(res.status) or ("ERR:" .. tostring(err)))
}
}
location /t {
content_by_lua_block {
-- Phase 1: CONCURRENTLY authenticate several end users on the
-- bind_dn-UNSET churn route so multiple pooled sockets end up
-- bound as DIFFERENT end users.
-- keepalive_pool_size=4 (>1) and all routes share pool 127.0.0.1:1389.
local churn = {
{u="user01", p="password1"}, {u="user02", p="password2"},
{u="jdoe", p="janesecret"},
}
local reqs = {}
for i = 1, 9 do
local c = churn[((i - 1) % 3) + 1]
reqs[i] = { "/probe", { args = { path = "/hello", u = c.u, p = c.p } } }
end
-- ngx.thread.spawn drives the concurrent capture_multi wave.
local th = assert(ngx.thread.spawn(function()
return { ngx.location.capture_multi(reqs) }
end))
local _, churn_resps = ngx.thread.wait(th)
local churn_all_200 = true
for _, r in ipairs(churn_resps) do
if r.body ~= "200" then churn_all_200 = false end
end
-- Phase 2: Route A (bind_dn UNSET) authenticating `secretuser`, which
-- is hidden from an anonymous search. Its anonymous simple_bind("","")
-- MUST reset any reused (end-user-bound) socket to anonymous -> the
-- search finds nothing -> 401. A leaked end-user bind would resolve
-- secretuser and 200.
local a = {}
for i = 1, 5 do
a[i] = { "/probe", { args = { path = "/uri", u = "secretuser", p = "secretpass" } } }
end
local ares = { ngx.location.capture_multi(a) }
local routeA_all_401 = true
for _, r in ipairs(ares) do
if r.body ~= "401" then routeA_all_401 = false end
end
-- Phase 3: Route B (bind_dn SET) authenticating `secretuser`. Its
-- search bind as the service account resolves secretuser -> 200.
local b = {}
for i = 1, 5 do
b[i] = { "/probe", { args = { path = "/hello1", u = "secretuser", p = "secretpass" } } }
end
local bres = { ngx.location.capture_multi(b) }
local routeB_all_200 = true
for _, r in ipairs(bres) do
if r.body ~= "200" then routeB_all_200 = false end
end
ngx.say("churn_all_200: ", tostring(churn_all_200))
ngx.say("routeA_anon_all_401: ", tostring(routeA_all_401))
ngx.say("routeB_svc_all_200: ", tostring(routeB_all_200))
}
}
--- timeout: 15
--- response_body
churn_all_200: true
routeA_anon_all_401: true
routeB_svc_all_200: true
=== TEST 49: swap in a Consumer whose user_dn is an $ENV:// secret reference
--- main_config
env ADV_LDAP_USER_DN=cn=user02,ou=users,dc=example,dc=org;
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/consumers/ldapadvuser02', ngx.HTTP_DELETE)
if code >= 300 then
ngx.status = code
return ngx.say(body)
end
code, body = t('/apisix/admin/consumers',
ngx.HTTP_PUT,
[[{
"username": "ldapadvenvref",
"plugins": {
"ldap-auth-advanced": {
"user_dn": "$ENV://ADV_LDAP_USER_DN"
}
}
}]]
)
if code >= 300 then
ngx.status = code
return ngx.say(body)
end
-- wait until the watcher has synced BOTH writes: the resolved
-- env-ref consumer must be the one the runtime map returns
local find_consumer = require("apisix.consumer").find_consumer
for _ = 1, 100 do
local consumer = find_consumer("ldap-auth-advanced", "user_dn",
"cn=user02,ou=users,dc=example,dc=org")
if consumer and consumer.consumer_name == "ldapadvenvref" then
break
end
ngx.sleep(0.05)
end
ngx.say("passed")
}
}
--- response_body
passed
=== TEST 50: env-ref user_dn resolves and matches the Consumer (200) (creds: user02:password2)
--- main_config
env ADV_LDAP_USER_DN=cn=user02,ou=users,dc=example,dc=org;
--- request
GET /hello
--- more_headers
Authorization: ldap dXNlcjAyOnBhc3N3b3JkMg==
--- error_code: 200
--- response_body
hello world
--- error_log
find consumer ldapadvenvref
=== TEST 51: set the /uri header-printing route to consumer_required=false
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/routes/2',
ngx.HTTP_PUT,
[[{
"plugins": {
"ldap-auth-advanced": {
"ldap_uri": "127.0.0.1:1389",
"base_dn": "ou=users,dc=example,dc=org",
"attribute": "uid",
"consumer_required": false
}
},
"upstream": {
"nodes": {
"127.0.0.1:1980": 1
},
"type": "roundrobin"
},
"uri": "/uri"
}]]
)
if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- response_body
passed
=== TEST 52: spoofed identity headers never reach the upstream without a Consumer (creds: user01:password1)
--- request
GET /uri
--- more_headers
Authorization: ldap dXNlcjAxOnBhc3N3b3JkMQ==
X-Consumer-Username: spoofed-user
X-Credential-Identifier: spoofed-cred
X-Consumer-Custom-ID: spoofed-id
X-Authenticated-Groups: spoofed-groups
--- error_code: 200
--- response_body
uri: /uri
authorization: ldap dXNlcjAxOnBhc3N3b3JkMQ==
host: localhost
x-real-ip: 127.0.0.1
=== TEST 53: RFC 4512 attribute forms are accepted (descriptor, OID, options)
--- config
location /t {
content_by_lua_block {
local plugin = require("apisix.plugins.ldap-auth-advanced")
local cases = {
"sAMAccountName",
"cn;lang-en",
"1.2.840.113556.1.4.656",
"0.9.2342.19200300.100.1.1;binary",
}
for _, attr in ipairs(cases) do
local ok = plugin.check_schema({
ldap_uri = "127.0.0.1:1389",
base_dn = "ou=users,dc=example,dc=org",
attribute = attr,
})
ngx.say(attr, ": ", ok and "passed" or "rejected")
end
}
}
--- response_body
sAMAccountName: passed
cn;lang-en: passed
1.2.840.113556.1.4.656: passed
0.9.2342.19200300.100.1.1;binary: passed
=== TEST 54: malformed attribute forms are rejected
--- config
location /t {
content_by_lua_block {
local plugin = require("apisix.plugins.ldap-auth-advanced")
local cases = {
"1", -- a bare number is neither a descriptor nor an OID
"1.2.", -- trailing dot
"01.2", -- leading zero in an arc
"cn;", -- empty option
";binary", -- missing attribute type
}
for _, attr in ipairs(cases) do
local ok = plugin.check_schema({
ldap_uri = "127.0.0.1:1389",
base_dn = "ou=users,dc=example,dc=org",
attribute = attr,
})
ngx.say(attr, ": ", ok and "passed" or "rejected")
end
}
}
--- response_body
1: rejected
1.2.: rejected
01.2: rejected
cn;: rejected
;binary: rejected
=== TEST 55: set up the search-then-bind route with the uid attribute's numeric OID
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/routes/1',
ngx.HTTP_PUT,
[[{
"plugins": {
"ldap-auth-advanced": {
"ldap_uri": "127.0.0.1:1389",
"base_dn": "ou=users,dc=example,dc=org",
"attribute": "0.9.2342.19200300.100.1.1"
}
},
"upstream": {
"nodes": {
"127.0.0.1:1980": 1
},
"type": "roundrobin"
},
"uri": "/hello"
}]]
)
if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- response_body
passed
=== TEST 56: OID attribute resolves the user end-to-end (200) (creds: user01:password1)
--- request
GET /hello
--- more_headers
Authorization: ldap dXNlcjAxOnBhc3N3b3JkMQ==
--- error_code: 200
--- response_body
hello world
--- error_log
find consumer ldapadvuser01
=== TEST 57: a proxy's own Basic Proxy-Authorization must not mask a valid Authorization
--- request
GET /hello
--- more_headers
Proxy-Authorization: Basic cHJveHk6aHVudGVyMg==
Authorization: ldap dXNlcjAxOnBhc3N3b3JkMQ==
--- error_code: 200
--- response_body
hello world
--- error_log
find consumer ldapadvuser01
=== TEST 58: Proxy-Authorization alone carries the credential (200) (creds: user01:password1)
--- request
GET /hello
--- more_headers
Proxy-Authorization: ldap dXNlcjAxOnBhc3N3b3JkMQ==
--- error_code: 200
--- response_body
hello world
--- error_log
find consumer ldapadvuser01
=== TEST 59: when both headers parse, Proxy-Authorization wins (proxy: user01, auth: jdoe)
--- request
GET /hello
--- more_headers
Proxy-Authorization: ldap dXNlcjAxOnBhc3N3b3JkMQ==
Authorization: ldap amRvZTpqYW5lc2VjcmV0
--- error_code: 200
--- response_body
hello world
--- error_log
find consumer ldapadvuser01
=== TEST 60: undecodable Proxy-Authorization payload falls back to Authorization
--- request
GET /hello
--- more_headers
Proxy-Authorization: ldap !!!not-base64!!!
Authorization: ldap dXNlcjAxOnBhc3N3b3JkMQ==
--- error_code: 200
--- response_body
hello world
--- error_log
find consumer ldapadvuser01
=== TEST 61: unusable Proxy-Authorization with no Authorization still 401s
--- request
GET /hello
--- more_headers
Proxy-Authorization: Basic cHJveHk6aHVudGVyMg==
--- error_code: 401
=== TEST 62: Proxy-Authorization with an empty username falls back to Authorization (proxy: ":pass")
--- request
GET /hello
--- more_headers
Proxy-Authorization: ldap OnBhc3M=
Authorization: ldap dXNlcjAxOnBhc3N3b3JkMQ==
--- error_code: 200
--- response_body
hello world
--- error_log
find consumer ldapadvuser01
=== TEST 63: Proxy-Authorization with an empty password falls back to Authorization (proxy: "user:")
--- request
GET /hello
--- more_headers
Proxy-Authorization: ldap dXNlcjo=
Authorization: ldap dXNlcjAxOnBhc3N3b3JkMQ==
--- error_code: 200
--- response_body
hello world
--- error_log
find consumer ldapadvuser01
=== TEST 64: an empty-field Proxy-Authorization with no Authorization still 401s (proxy: ":")
--- request
GET /hello
--- more_headers
Proxy-Authorization: ldap Og==
--- error_code: 401
--- grep_error_log eval
qr/empty password/
--- grep_error_log_out
empty password
=== TEST 65: set up a route whose service-account password is wrong
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/routes/1',
ngx.HTTP_PUT,
[[{
"plugins": {
"ldap-auth-advanced": {
"ldap_uri": "127.0.0.1:1389",
"base_dn": "ou=users,dc=example,dc=org",
"attribute": "uid",
"bind_dn": "cn=admin,dc=example,dc=org",
"ldap_password": "rotated-away"
}
},
"upstream": {
"nodes": {
"127.0.0.1:1980": 1
},
"type": "roundrobin"
},
"uri": "/hello"
}]]
)
if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- response_body
passed
=== TEST 66: rejected service bind -> 500, never a client auth failure (creds: user01:password1)
--- request
GET /hello
--- more_headers
Authorization: ldap dXNlcjAxOnBhc3N3b3JkMQ==
--- error_code: 500
--- error_log
LDAP search bind failed
=== TEST 67: set up a route with a nonexistent base_dn
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/routes/1',
ngx.HTTP_PUT,
[[{
"plugins": {
"ldap-auth-advanced": {
"ldap_uri": "127.0.0.1:1389",
"base_dn": "ou=nowhere,dc=example,dc=org",
"attribute": "uid"
}
},
"upstream": {
"nodes": {
"127.0.0.1:1980": 1
},
"type": "roundrobin"
},
"uri": "/hello"
}]]
)
if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- response_body
passed
=== TEST 68: search against a nonexistent base_dn -> 500 (noSuchObject is a misconfiguration) (creds: user01:password1)
--- request
GET /hello
--- more_headers
Authorization: ldap dXNlcjAxOnBhc3N3b3JkMQ==
--- error_code: 500
--- error_log
LDAP user search failed
=== TEST 69: set up a route whose login attribute matches many entries (objectClass)
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/routes/1',
ngx.HTTP_PUT,
[[{
"plugins": {
"ldap-auth-advanced": {
"ldap_uri": "127.0.0.1:1389",
"base_dn": "ou=users,dc=example,dc=org",
"attribute": "objectClass"
}
},
"upstream": {
"nodes": {
"127.0.0.1:1980": 1
},
"type": "roundrobin"
},
"uri": "/hello"
}]]
)
if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- response_body
passed
=== TEST 70: sizeLimitExceeded stays a fail-closed 401, not a 500 (creds: inetOrgPerson:x)
--- request
GET /hello
--- more_headers
Authorization: ldap aW5ldE9yZ1BlcnNvbjp4
--- error_code: 401
--- grep_error_log eval
qr/ambiguous user match \(size limit exceeded\)/
--- grep_error_log_out
ambiguous user match (size limit exceeded)
=== TEST 71: empty ldap_uri is rejected (minLength)
--- config
location /t {
content_by_lua_block {
local plugin = require("apisix.plugins.ldap-auth-advanced")
local ok, err = plugin.check_schema({
ldap_uri = "",
base_dn = "ou=users,dc=example,dc=org",
})
ngx.say(ok and "passed" or err)
}
}
--- response_body_like eval
qr/property "ldap_uri" validation failed/
=== TEST 72: overlong base_dn is rejected (maxLength)
--- config
location /t {
content_by_lua_block {
local plugin = require("apisix.plugins.ldap-auth-advanced")
local ok, err = plugin.check_schema({
ldap_uri = "127.0.0.1:1389",
base_dn = "ou=" .. string.rep("x", 4094) .. ",dc=org",
})
ngx.say(ok and "passed" or err)
}
}
--- response_body_like eval
qr/property "base_dn" validation failed/
=== TEST 73: empty consumer user_dn is rejected (minLength)
--- config
location /t {
content_by_lua_block {
local core = require("apisix.core")
local plugin = require("apisix.plugins.ldap-auth-advanced")
local ok, err = plugin.check_schema({ user_dn = "" },
core.schema.TYPE_CONSUMER)
ngx.say(ok and "passed" or err)
}
}
--- response_body_like eval
qr/property "user_dn" validation failed/