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

feat: Add ldap-auth plugin #3894

Merged
merged 34 commits into from
Oct 13, 2021
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
a5d1ab9
add lualdap
jp-gouin Mar 16, 2021
2e2ffb0
add ldap plugin
jp-gouin Mar 16, 2021
6cfe4cd
add ldap plugin
jp-gouin Mar 16, 2021
7b01646
add ldap plugin
jp-gouin Mar 16, 2021
979e46b
add ldap-auth plugin , add test case , need to debug the test case
jp-gouin Mar 23, 2021
d46d980
correct typo
jp-gouin Mar 23, 2021
fad0827
update of readme and test case
jp-gouin Mar 23, 2021
d410f56
add test cases for ldap-auth plugin, update travis to deploy openldap…
jp-gouin Apr 6, 2021
8a43bc3
resolve merge conflict
jp-gouin Apr 6, 2021
6c29529
resolve merge conflict
jp-gouin Apr 6, 2021
3946036
remove trailing space, use libldap libldap-2.4-2
jp-gouin Apr 6, 2021
b67b69d
change libldap to libldap2-dev to avoid error upon lualdap installation
jp-gouin Apr 6, 2021
47d30e1
resolve merge conflicts
jp-gouin Aug 21, 2021
0bbc7e4
Merge branch 'apache-master' into master
jp-gouin Aug 21, 2021
c178e17
Add dependency openldap-devel or libldap2-dev for CI.
jp-gouin Aug 22, 2021
bb2ccfd
mispelling in ldap-auth
jp-gouin Aug 22, 2021
cb8cb6f
remove unused variable
jp-gouin Aug 22, 2021
48870cb
add libldap2-dev in Linux Get dependencies step
jp-gouin Aug 22, 2021
a2ceb94
update admin/plugins test cases 1 and 9 to include ldap-auth
jp-gouin Sep 14, 2021
7d10e1e
patch error when linking label to versionDB , the new api of harbor i…
jp-gouin Sep 14, 2021
558f8a8
update admin/plugins test cases 1 to include ldap-auth at the right p…
jp-gouin Sep 14, 2021
66b69a7
Merge branch 'master' of github.com:jp-gouin/apisix into master
jp-gouin Sep 14, 2021
078f628
change require style to match existing requirs , remove additionalPro…
jp-gouin Sep 18, 2021
23253f4
change require style to match existing requirs , remove additionalPro…
jp-gouin Sep 18, 2021
33fc909
remove additionalProperties from test case
jp-gouin Sep 19, 2021
89f8d6a
Add openldap reference in the install-dependencies.md, add sub foncti…
jp-gouin Oct 2, 2021
15cf6ee
resolve merge conflict
jp-gouin Oct 2, 2021
9c87c10
remove trailing space, reindex test case
jp-gouin Oct 2, 2021
9663476
add verification on extract_header, remove reference to apisix dashbo…
jp-gouin Oct 4, 2021
abe1090
revert wrong commit, add check on extract_header
jp-gouin Oct 4, 2021
10a4d87
remove unnecessary check
jp-gouin Oct 4, 2021
8ab4df8
commit change per review
jp-gouin Oct 10, 2021
79b01a6
add libldap2-dev dependency in .github/workflows/cli.yml
jp-gouin Oct 10, 2021
78032ba
remove unexisting err from ngx.decode_base64(m[1])
jp-gouin Oct 11, 2021
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
171 changes: 171 additions & 0 deletions apisix/plugins/ldap-auth.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
--
-- 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.
--
local core = require("apisix.core")
local ngx = ngx
local ngx_re = require("ngx.re")
local ipairs = ipairs
local consumer_mod = require("apisix.consumer")
local lualdap = require "lualdap"
Copy link
Member

Choose a reason for hiding this comment

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

Better to put all the require together and use require("lualdap") style

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agree, corrected


local lrucache = core.lrucache.new({
ttl = 300, count = 512
})
local consumers_lrucache = core.lrucache.new({
type = "plugin",
})

local schema = {
type = "object",
title = "work with route or service object",
properties = {
basedn = { type = "string" },
ldapuri = { type = "string" },
usetls = { type = "boolean" },
Copy link
Member

Choose a reason for hiding this comment

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

Would you use a snake case for the field name, like base_dn / ldap_uri?

Copy link
Contributor Author

@jp-gouin jp-gouin Apr 2, 2021

Choose a reason for hiding this comment

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

Ok changed

uid = { type = "string" },
auto_create_consumer = {type ="boolean"}
},
required = {"basedn","ldapuri"},
additionalProperties = true,
}

local consumer_schema = {
type = "object",
title = "work with consumer object",
properties = {
userdn = { type = "string" },
},
required = {"userdn"},
additionalProperties = false,
}

local plugin_name = "ldap-auth"

local _M = {
version = 0.1,
priority = 2520,
type = 'auth',
name = plugin_name,
schema = schema,
consumer_schema = consumer_schema
}

function _M.check_schema(conf, schema_type)
local ok, err
if schema_type == core.schema.TYPE_CONSUMER then
ok, err = core.schema.check(consumer_schema, conf)
else
ok, err = core.schema.check(schema, conf)
end

if not ok then
Copy link
Member

Choose a reason for hiding this comment

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

return ok, err is enough.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok added

return false, err
end

return true
end

local create_consume_cache
do
local consumer_names = {}

function create_consume_cache(consumers)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
function create_consume_cache(consumers)
function create_consumer_cache(consumers)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes thanks I’ll update the PR

core.table.clear(consumer_names)

for _, consumer in ipairs(consumers.nodes) do
core.log.info("consumer node: ", core.json.delay_encode(consumer))
consumer_names[consumer.auth_conf.userdn] = consumer
end

return consumer_names
end

end -- do

local function extract_auth_header(authorization)
local obj = { username = "", password = "" }

local m, err = ngx.re.match(authorization, "Basic\\s(.+)", "jo")
if err then
-- error authorization
return nil, err
end

local decoded = ngx.decode_base64(m[1])

local res
res, err = ngx_re.split(decoded, ":")
Copy link
Member

Choose a reason for hiding this comment

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

Better to check if decoded is not nil and the split result has two elements.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually it’s a copy paste from

local decoded = ngx.decode_base64(m[1])
, I’ll try to update the code

Copy link
Member

Choose a reason for hiding this comment

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

Feel free to submit another PR to update basic-auth.

if err then
return nil, "split authorization err:" .. err
end

obj.username = ngx.re.gsub(res[1], "\\s+", "", "jo")
obj.password = ngx.re.gsub(res[2], "\\s+", "", "jo")

return obj, nil
end

function _M.rewrite(conf, ctx)
core.log.info("plugin access phase, conf: ", core.json.delay_encode(conf))
Copy link
Contributor

Choose a reason for hiding this comment

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

Should it be "rewrite"?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For the name of the function?

Copy link
Contributor

Choose a reason for hiding this comment

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

For the log message. Although the rewrite function was called in the access phase, we may still log the rewrite phase.


-- 1. extract authorization from header
local auth_header = core.request.header(ctx, "Authorization")
if not auth_header then
core.response.set_header("WWW-Authenticate", "Basic realm='.'")
return 401, { message = "Missing authorization in request" }
end

local user, err = extract_auth_header(auth_header)
if err then
return 401, { message = err }
end

-- 2. try authenticate the user against the ldap server
local uid = "cn"
if conf.uid then
uid = conf.uid
end
local userdn = uid .. "=" .. user.username .. "," .. conf.basedn
local ld = lualdap.open_simple (conf.ldapuri, userdn, user.password, conf.usetls)
if not ld then
return 401, { message = "Invalid user authorization" }
end

-- 3. Retreive consumer for authorization plugin
if conf.auto_create_consumer then
Copy link
Member

Choose a reason for hiding this comment

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

Better to avoid creating temporary consumers. The temporary consumer may be broken somewhere else.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok remove this feature .
It was more to act like the oidc plugin , that does not require a consumer to be created , but i see your point

local tmpCustomer = {consumer_name = userdn, auth_conf = {username = userdn}}
local tmpCustomerConf = {conf_version = "1", consumer_name="ldapauth"}
consumer_mod.attach_consumer(ctx, tmpCustomer, tmpCustomerConf)
else
local consumer_conf = consumer_mod.plugin(plugin_name)
if not consumer_conf then
return 401, {message = "Missing related consumer"}
end
core.log.error(consumer_conf.conf_version)
local consumers = lrucache("consumers_key", consumer_conf.conf_version,
create_consume_cache, consumer_conf)

local consumer = consumers[userdn]
if not consumer then
return 401, {message = "Invalid API key in request"}
end
consumer_mod.attach_consumer(ctx, consumer, consumer_conf)
end

core.log.info("hit basic-auth access")
end

return _M
1 change: 1 addition & 0 deletions conf/config-default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ plugins: # plugin list (sorted in alphabetical order)
- jwt-auth
- kafka-logger
- key-auth
- ldap-auth
- limit-conn
- limit-count
- limit-req
Expand Down
152 changes: 152 additions & 0 deletions docs/en/latest/plugins/ldap-auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
---
title: ldap-auth
---

<!--
#
# 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.
#
-->

## Summary

- [**Name**](#name)
- [**Attributes**](#attributes)
- [**How To Enable**](#how-to-enable)
- [**Test Plugin**](#test-plugin)
- [**Disable Plugin**](#disable-plugin)

## Name

`ldap-auth` is an authentication plugin that can works with `consumer`. Add Ldap Authentication to a `service` or `route`.

The `consumer` then authenticate against the Ldap server using Basic authentication.
Copy link
Member

Choose a reason for hiding this comment

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

"authenticates"?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes corrected


For more information on Basic authentication, refer to [Wiki](https://en.wikipedia.org/wiki/Basic_access_authentication) for more information.

This authentication plugin use [lualdap](https://lualdap.github.io/lualdap/) plugin to connect against the ldap server

## Attributes

| Name | Type | Requirement | Default | Valid | Description |
| -------- | ------ | ----------- | ------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| basedn | string | required | | | the base dn of the `ldap` server (example : `ou=users,dc=example,dc=org`) |
| ldapuri | string | required | | | the uri of the ldap server |
| usetls | boolean | optional | `true` | | Boolean flag indicating if Transport Layer Security (TLS) should be used. |
| uid | string | optional | `cn` | | the user's password |
Copy link
Member

Choose a reason for hiding this comment

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

Is it the password?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No indeed it's the uid attribute that changes depending of the ldap server solution

| auto_create_consumer | boolean | optional | `false` | | enable this to automatically create consumer for each authentication , the consumer name will be the full `dn` of the user |

## How To Enable

### 1. set a consumer and config the value of the `ldap-auth` option

```shell
curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
"username": "foo",
"plugins": {
"ldap-auth": {
"userdn": "cn=user01,ou=users,dc=example,dc=org"
}
}
}'
```

you can visit Dashboard `http://127.0.0.1:9080/apisix/dashboard/` and add a Consumer through the web console.
Copy link
Member

Choose a reason for hiding this comment

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

We can remove this part now. The dashboard is not shipped by default.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok will remove , actually it’s a copy paste from

you also can add a Consumer through the web console:


then add ldap-auth plugin in the Consumer page

### 2. add a Route or add a Service, and enable the `ldap-auth` plugin

```shell
curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
"methods": ["GET"],
"uri": "/hello",
"plugins": {
"ldap-auth": {
"basedn": "ou=users,dc=example,dc=org",
"ldapuri": "172.19.0.1",
"uid": "cn"
},
},
"upstream": {
"type": "roundrobin",
"nodes": {
"127.0.0.1:1980": 1
}
}
}'
```

## Test Plugin

- missing Authorization header

```shell
$ curl -i http://127.0.0.1:9080/hello
HTTP/1.1 401 Unauthorized
...
{"message":"Missing authorization in request"}
```

- user is not exists:

```shell
$ curl -i -uuser:password1 http://127.0.0.1:9080/hello
HTTP/1.1 401 Unauthorized
...
{"message":"Invalid user key in authorization"}
```

- password is invalid:

```shell
$ curl -i -uuser01:passwordfalse http://127.0.0.1:9080/hello
HTTP/1.1 401 Unauthorized
...
{"message":"Password is error"}
```

- success:

```shell
$ curl -i -uuser01:password1 http://127.0.0.1:9080/hello
HTTP/1.1 200 OK
...
hello, world
```

## Disable Plugin

When you want to disable the `ldap-auth` plugin, it is very simple,
you can delete the corresponding json configuration in the plugin configuration,
no need to restart the service, it will take effect immediately:

```shell
$ curl http://127.0.0.1:2379/apisix/admin/routes/1 -X PUT -d value='
{
"methods": ["GET"],
"uri": "/hello",
"plugins": {},
"upstream": {
"type": "roundrobin",
"nodes": {
"127.0.0.1:1980": 1
}
}
}'
```
1 change: 1 addition & 0 deletions rockspec/apisix-master-0.rockspec
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ dependencies = {
"luasec = 0.9-1",
"lua-resty-consul = 0.3-2",
"penlight = 1.9.2-1",
"lualdap = 1.2.3-1",
Copy link
Member

Choose a reason for hiding this comment

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

Why not use the latest release: 1.2.6?

}

build = {
Expand Down
Loading