refactor(proj): Add apps
This commit is contained in:
parent
cf7c3b4f0c
commit
686c006d6e
|
@ -0,0 +1,26 @@
|
||||||
|
name: Run test cases
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run_test_cases:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- name: run test cases
|
||||||
|
run: |
|
||||||
|
docker network create --driver bridge --ipv6 --subnet fd15:555::/64 tests_emqx_bridge
|
||||||
|
docker run -i \
|
||||||
|
--network tests_emqx_bridge \
|
||||||
|
-v $(pwd):/emqx_auth_http \
|
||||||
|
erlang:22.3 \
|
||||||
|
bash -c "make -C /emqx_auth_http xref
|
||||||
|
make -C /emqx_auth_http eunit
|
||||||
|
make -C /emqx_auth_http ct
|
||||||
|
make -C /emqx_auth_http cover"
|
||||||
|
- uses: actions/upload-artifact@v1
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: _build/test/logs
|
|
@ -0,0 +1,25 @@
|
||||||
|
.eunit
|
||||||
|
deps
|
||||||
|
*.o
|
||||||
|
*.beam
|
||||||
|
*.plt
|
||||||
|
erl_crash.dump
|
||||||
|
ebin
|
||||||
|
rel/example_project
|
||||||
|
.concrete/DEV_MODE
|
||||||
|
.rebar
|
||||||
|
.erlang.mk/
|
||||||
|
emqx_auth_http.d
|
||||||
|
data
|
||||||
|
ct.cover.spec
|
||||||
|
cover/
|
||||||
|
ct.coverdata
|
||||||
|
eunit.coverdata
|
||||||
|
logs/
|
||||||
|
erlang.mk
|
||||||
|
_build/
|
||||||
|
rebar.lock
|
||||||
|
rebar3.crashdump
|
||||||
|
etc/emqx_auth_http.conf.rendered
|
||||||
|
.rebar3/
|
||||||
|
*.swp
|
|
@ -0,0 +1,201 @@
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright {yyyy} {name of copyright owner}
|
||||||
|
|
||||||
|
Licensed 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.
|
|
@ -0,0 +1,100 @@
|
||||||
|
emqx_auth_http
|
||||||
|
==============
|
||||||
|
|
||||||
|
EMQ X HTTP Auth/ACL Plugin
|
||||||
|
|
||||||
|
Build
|
||||||
|
-----
|
||||||
|
|
||||||
|
```
|
||||||
|
make && make tests
|
||||||
|
```
|
||||||
|
|
||||||
|
Configure the Plugin
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
File: etc/emqx_auth_http.conf
|
||||||
|
|
||||||
|
```
|
||||||
|
##--------------------------------------------------------------------
|
||||||
|
## Authentication request.
|
||||||
|
##
|
||||||
|
## Variables:
|
||||||
|
## - %u: username
|
||||||
|
## - %c: clientid
|
||||||
|
## - %a: ipaddress
|
||||||
|
## - %r: protocol
|
||||||
|
## - %P: password
|
||||||
|
## - %C: common name of client TLS cert
|
||||||
|
## - %d: subject of client TLS cert
|
||||||
|
##
|
||||||
|
## Value: URL
|
||||||
|
auth.http.auth_req = http://127.0.0.1:8080/mqtt/auth
|
||||||
|
## Value: post | get | put
|
||||||
|
auth.http.auth_req.method = post
|
||||||
|
## Value: Params
|
||||||
|
auth.http.auth_req.params = clientid=%c,username=%u,password=%P
|
||||||
|
|
||||||
|
##--------------------------------------------------------------------
|
||||||
|
## Superuser request.
|
||||||
|
##
|
||||||
|
## Variables:
|
||||||
|
## - %u: username
|
||||||
|
## - %c: clientid
|
||||||
|
## - %a: ipaddress
|
||||||
|
## - %r: protocol
|
||||||
|
## - %P: password
|
||||||
|
## - %C: common name of client TLS cert
|
||||||
|
## - %d: subject of client TLS cert
|
||||||
|
##
|
||||||
|
## Value: URL
|
||||||
|
auth.http.super_req = http://127.0.0.1:8080/mqtt/superuser
|
||||||
|
## Value: post | get | put
|
||||||
|
auth.http.super_req.method = post
|
||||||
|
## Value: Params
|
||||||
|
auth.http.super_req.params = clientid=%c,username=%u
|
||||||
|
|
||||||
|
##--------------------------------------------------------------------
|
||||||
|
## ACL request.
|
||||||
|
##
|
||||||
|
## Variables:
|
||||||
|
## - %A: 1 | 2, 1 = sub, 2 = pub
|
||||||
|
## - %u: username
|
||||||
|
## - %c: clientid
|
||||||
|
## - %a: ipaddress
|
||||||
|
## - %r: protocol
|
||||||
|
## - %m: mountpoint
|
||||||
|
## - %t: topic
|
||||||
|
##
|
||||||
|
## Value: URL
|
||||||
|
auth.http.acl_req = http://127.0.0.1:8080/mqtt/acl
|
||||||
|
## Value: post | get | put
|
||||||
|
auth.http.acl_req.method = get
|
||||||
|
## Value: Params
|
||||||
|
auth.http.acl_req.params = access=%A,username=%u,clientid=%c,ipaddr=%a,topic=%t
|
||||||
|
```
|
||||||
|
|
||||||
|
Load the Plugin
|
||||||
|
---------------
|
||||||
|
|
||||||
|
```
|
||||||
|
./bin/emqx_ctl plugins load emqx_auth_http
|
||||||
|
```
|
||||||
|
|
||||||
|
HTTP API
|
||||||
|
--------
|
||||||
|
|
||||||
|
200 if ok
|
||||||
|
|
||||||
|
4xx if unauthorized
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
|
Apache License Version 2.0
|
||||||
|
|
||||||
|
Author
|
||||||
|
------
|
||||||
|
|
||||||
|
EMQ X Team.
|
||||||
|
|
|
@ -0,0 +1,162 @@
|
||||||
|
##--------------------------------------------------------------------
|
||||||
|
## HTTP Auth/ACL Plugin
|
||||||
|
##--------------------------------------------------------------------
|
||||||
|
|
||||||
|
##--------------------------------------------------------------------
|
||||||
|
## Authentication request.
|
||||||
|
|
||||||
|
## HTTP URL API path for authentication request
|
||||||
|
##
|
||||||
|
## Value: URL
|
||||||
|
##
|
||||||
|
## Examples: http://127.0.0.1:8991/mqtt/auth, https://[::1]:8991/mqtt/auth
|
||||||
|
auth.http.auth_req = http://127.0.0.1:8991/mqtt/auth
|
||||||
|
|
||||||
|
## Value: post | get
|
||||||
|
auth.http.auth_req.method = post
|
||||||
|
|
||||||
|
## It only works when method=post
|
||||||
|
## Value: json | x-www-form-urlencoded
|
||||||
|
auth.http.auth_req.content_type = x-www-form-urlencoded
|
||||||
|
|
||||||
|
## Variables:
|
||||||
|
## - %u: username
|
||||||
|
## - %c: clientid
|
||||||
|
## - %a: ipaddress
|
||||||
|
## - %r: protocol
|
||||||
|
## - %P: password
|
||||||
|
## - %p: sockport of server accepted
|
||||||
|
## - %C: common name of client TLS cert
|
||||||
|
## - %d: subject of client TLS cert
|
||||||
|
##
|
||||||
|
## Value: Params
|
||||||
|
auth.http.auth_req.params = clientid=%c,username=%u,password=%P
|
||||||
|
|
||||||
|
##--------------------------------------------------------------------
|
||||||
|
## Superuser request.
|
||||||
|
|
||||||
|
## HTTP URL API path for Superuser request
|
||||||
|
##
|
||||||
|
## Value: URL
|
||||||
|
##
|
||||||
|
## Examples: http://127.0.0.1:8991/mqtt/superuser, https://[::1]:8991/mqtt/superuser
|
||||||
|
#auth.http.super_req = http://127.0.0.1:8991/mqtt/superuser
|
||||||
|
|
||||||
|
## Value: post | get
|
||||||
|
#auth.http.super_req.method = post
|
||||||
|
|
||||||
|
## It only works when method=pos
|
||||||
|
## Value: json | x-www-form-urlencoded
|
||||||
|
#auth.http.super_req.content_type = x-www-form-urlencoded
|
||||||
|
|
||||||
|
## Variables:
|
||||||
|
## - %u: username
|
||||||
|
## - %c: clientid
|
||||||
|
## - %a: ipaddress
|
||||||
|
## - %r: protocol
|
||||||
|
## - %P: password
|
||||||
|
## - %p: sockport of server accepted
|
||||||
|
## - %C: common name of client TLS cert
|
||||||
|
## - %d: subject of client TLS cert
|
||||||
|
##
|
||||||
|
## Value: Params
|
||||||
|
#auth.http.super_req.params = clientid=%c,username=%u
|
||||||
|
|
||||||
|
##--------------------------------------------------------------------
|
||||||
|
## ACL request.
|
||||||
|
|
||||||
|
## HTTP URL API path for ACL request
|
||||||
|
##
|
||||||
|
## Value: URL
|
||||||
|
##
|
||||||
|
## Examples: http://127.0.0.1:8991/mqtt/acl, https://[::1]:8991/mqtt/acl
|
||||||
|
auth.http.acl_req = http://127.0.0.1:8991/mqtt/acl
|
||||||
|
|
||||||
|
## Value: post | get
|
||||||
|
auth.http.acl_req.method = get
|
||||||
|
|
||||||
|
## It only works when method=post
|
||||||
|
## Value: json | x-www-form-urlencoded
|
||||||
|
auth.http.acl_req.content_type = x-www-form-urlencoded
|
||||||
|
|
||||||
|
## Variables:
|
||||||
|
## - %A: 1 | 2, 1 = sub, 2 = pub
|
||||||
|
## - %u: username
|
||||||
|
## - %c: clientid
|
||||||
|
## - %a: ipaddress
|
||||||
|
## - %r: protocol
|
||||||
|
## - %m: mountpoint
|
||||||
|
## - %t: topic
|
||||||
|
##
|
||||||
|
## Value: Params
|
||||||
|
auth.http.acl_req.params = access=%A,username=%u,clientid=%c,ipaddr=%a,topic=%t,mountpoint=%m
|
||||||
|
|
||||||
|
##------------------------------------------------------------------------------
|
||||||
|
## Http Reqeust options
|
||||||
|
|
||||||
|
## Time-out time for the http request, 0 is never timeout.
|
||||||
|
##
|
||||||
|
## Value: Duration
|
||||||
|
## -h: hour, e.g. '2h' for 2 hours
|
||||||
|
## -m: minute, e.g. '5m' for 5 minutes
|
||||||
|
## -s: second, e.g. '30s' for 30 seconds
|
||||||
|
##
|
||||||
|
## Default: 0
|
||||||
|
## auth.http.request.timeout = 0
|
||||||
|
|
||||||
|
## Connection time-out time, used during the initial request
|
||||||
|
## when the client is connecting to the server
|
||||||
|
##
|
||||||
|
## Value: Duration
|
||||||
|
##
|
||||||
|
## Default is same with the timeout option
|
||||||
|
## auth.http.request.connect_timeout = 0
|
||||||
|
|
||||||
|
## Re-send http reuqest times
|
||||||
|
##
|
||||||
|
## Value: integer
|
||||||
|
##
|
||||||
|
## Default: 3
|
||||||
|
auth.http.request.retry_times = 3
|
||||||
|
|
||||||
|
## The interval for re-sending the http request
|
||||||
|
##
|
||||||
|
## Value: Duration
|
||||||
|
##
|
||||||
|
## Default: 1s
|
||||||
|
auth.http.request.retry_interval = 1s
|
||||||
|
|
||||||
|
## The 'Exponential Backoff' mechanism for re-sending request. The actually
|
||||||
|
## re-send time interval is `interval * backoff ^ times`
|
||||||
|
##
|
||||||
|
## Value: float
|
||||||
|
##
|
||||||
|
## Default: 2.0
|
||||||
|
auth.http.request.retry_backoff = 2.0
|
||||||
|
|
||||||
|
##------------------------------------------------------------------------------
|
||||||
|
## SSL options
|
||||||
|
|
||||||
|
## Path to the file containing PEM-encoded CA certificates. The CA certificates
|
||||||
|
## are used during server authentication and when building the client certificate chain.
|
||||||
|
##
|
||||||
|
## Value: File
|
||||||
|
## auth.http.ssl.cacertfile = {{ platform_etc_dir }}/certs/ca.pem
|
||||||
|
|
||||||
|
## The path to a file containing the client's certificate.
|
||||||
|
##
|
||||||
|
## Value: File
|
||||||
|
## auth.http.ssl.certfile = {{ platform_etc_dir }}/certs/client-cert.pem
|
||||||
|
|
||||||
|
## Path to a file containing the client's private PEM-encoded key.
|
||||||
|
##
|
||||||
|
## Value: File
|
||||||
|
## auth.http.ssl.keyfile = {{ platform_etc_dir }}/certs/client-key.pem
|
||||||
|
|
||||||
|
##--------------------------------------------------------------------
|
||||||
|
## HTTP Request Headers
|
||||||
|
##
|
||||||
|
## Example: auth.http.header.Accept-Encoding = *
|
||||||
|
##
|
||||||
|
## Value: String
|
||||||
|
## auth.http.header.Accept = */*
|
|
@ -0,0 +1,25 @@
|
||||||
|
|
||||||
|
-define(APP, emqx_auth_http).
|
||||||
|
|
||||||
|
-record(http_request, {method = post, content_type, url, params, options = []}).
|
||||||
|
|
||||||
|
-record(auth_metrics, {
|
||||||
|
success = 'client.auth.success',
|
||||||
|
failure = 'client.auth.failure',
|
||||||
|
ignore = 'client.auth.ignore'
|
||||||
|
}).
|
||||||
|
|
||||||
|
-record(acl_metrics, {
|
||||||
|
allow = 'client.acl.allow',
|
||||||
|
deny = 'client.acl.deny',
|
||||||
|
ignore = 'client.acl.ignore'
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(METRICS(Type), tl(tuple_to_list(#Type{}))).
|
||||||
|
-define(METRICS(Type, K), #Type{}#Type.K).
|
||||||
|
|
||||||
|
-define(AUTH_METRICS, ?METRICS(auth_metrics)).
|
||||||
|
-define(AUTH_METRICS(K), ?METRICS(auth_metrics, K)).
|
||||||
|
|
||||||
|
-define(ACL_METRICS, ?METRICS(acl_metrics)).
|
||||||
|
-define(ACL_METRICS(K), ?METRICS(acl_metrics, K)).
|
|
@ -0,0 +1,165 @@
|
||||||
|
%%-*- mode: erlang -*-
|
||||||
|
%% emqx_auth_http config mapping
|
||||||
|
{mapping, "auth.http.auth_req", "emqx_auth_http.auth_req", [
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.http.auth_req.method", "emqx_auth_http.auth_req", [
|
||||||
|
{default, post},
|
||||||
|
{datatype, {enum, [post, get]}}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.http.auth_req.content_type", "emqx_auth_http.auth_req", [
|
||||||
|
{default, 'x-www-form-urlencoded'},
|
||||||
|
{datatype, {enum, [json, 'x-www-form-urlencoded']}}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.http.auth_req.params", "emqx_auth_http.auth_req", [
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{translation, "emqx_auth_http.auth_req", fun(Conf) ->
|
||||||
|
case cuttlefish:conf_get("auth.http.auth_req", Conf) of
|
||||||
|
undefined -> cuttlefish:unset();
|
||||||
|
Url ->
|
||||||
|
Params = cuttlefish:conf_get("auth.http.auth_req.params", Conf),
|
||||||
|
[{url, Url},
|
||||||
|
{method, cuttlefish:conf_get("auth.http.auth_req.method", Conf)},
|
||||||
|
{content_type, cuttlefish:conf_get("auth.http.auth_req.content_type", Conf)},
|
||||||
|
{params, [list_to_tuple(string:tokens(S, "=")) || S <- string:tokens(Params, ",")]}]
|
||||||
|
end
|
||||||
|
end}.
|
||||||
|
|
||||||
|
{mapping, "auth.http.super_req", "emqx_auth_http.super_req", [
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.http.super_req.method", "emqx_auth_http.super_req", [
|
||||||
|
{default, post},
|
||||||
|
{datatype, {enum, [post, get]}}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.http.super_req.content_type", "emqx_auth_http.super_req", [
|
||||||
|
{default, 'x-www-form-urlencoded'},
|
||||||
|
{datatype, {enum, [json, 'x-www-form-urlencoded']}}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.http.super_req.params", "emqx_auth_http.super_req", [
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{translation, "emqx_auth_http.super_req", fun(Conf) ->
|
||||||
|
case cuttlefish:conf_get("auth.http.super_req", Conf, undefined) of
|
||||||
|
undefined -> cuttlefish:unset();
|
||||||
|
Url -> Params = cuttlefish:conf_get("auth.http.super_req.params", Conf),
|
||||||
|
[{url, Url}, {method, cuttlefish:conf_get("auth.http.super_req.method", Conf)},
|
||||||
|
{content_type, cuttlefish:conf_get("auth.http.super_req.content_type", Conf)},
|
||||||
|
{params, [list_to_tuple(string:tokens(S, "=")) || S <- string:tokens(Params, ",")]}]
|
||||||
|
end
|
||||||
|
end}.
|
||||||
|
|
||||||
|
{mapping, "auth.http.acl_req", "emqx_auth_http.acl_req", [
|
||||||
|
{default, undefined},
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.http.acl_req.method", "emqx_auth_http.acl_req", [
|
||||||
|
{default, post},
|
||||||
|
{datatype, {enum, [post, get]}}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.http.acl_req.content_type", "emqx_auth_http.acl_req", [
|
||||||
|
{default, 'x-www-form-urlencoded'},
|
||||||
|
{datatype, {enum, [json, 'x-www-form-urlencoded']}}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.http.acl_req.params", "emqx_auth_http.acl_req", [
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{translation, "emqx_auth_http.acl_req", fun(Conf) ->
|
||||||
|
case cuttlefish:conf_get("auth.http.acl_req", Conf, undefined) of
|
||||||
|
undefined -> cuttlefish:unset();
|
||||||
|
Url -> Params = cuttlefish:conf_get("auth.http.acl_req.params", Conf),
|
||||||
|
[{url, Url}, {method, cuttlefish:conf_get("auth.http.acl_req.method", Conf)},
|
||||||
|
{content_type, cuttlefish:conf_get("auth.http.acl_req.content_type", Conf)},
|
||||||
|
{params, [list_to_tuple(string:tokens(S, "=")) || S <- string:tokens(Params, ",")]}]
|
||||||
|
end
|
||||||
|
end}.
|
||||||
|
|
||||||
|
{mapping, "auth.http.request.timeout", "emqx_auth_http.http_opts", [
|
||||||
|
{default, 0},
|
||||||
|
{datatype, [integer, {duration, ms}]}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.http.request.connect_timeout", "emqx_auth_http.http_opts", [
|
||||||
|
{datatype, [integer, {duration, ms}]}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.http.ssl.cacertfile", "emqx_auth_http.http_opts", [
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.http.ssl.certfile", "emqx_auth_http.http_opts", [
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.http.ssl.keyfile", "emqx_auth_http.http_opts", [
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{translation, "emqx_auth_http.http_opts", fun(Conf) ->
|
||||||
|
Filter = fun(L) -> [{K, V} || {K, V} <- L, V =/= undefined] end,
|
||||||
|
InfinityFun = fun(0) -> infinity;
|
||||||
|
(Duration) -> Duration
|
||||||
|
end,
|
||||||
|
SslOpts = Filter([{cacertfile, cuttlefish:conf_get("auth.http.ssl.cacertfile", Conf, undefined)},
|
||||||
|
{certfile, cuttlefish:conf_get("auth.http.ssl.certfile", Conf, undefined)},
|
||||||
|
{keyfile, cuttlefish:conf_get("auth.http.ssl.keyfile", Conf, undefined)}]),
|
||||||
|
Opts = [{timeout, InfinityFun(cuttlefish:conf_get("auth.http.request.timeout", Conf))},
|
||||||
|
{connect_timeout, InfinityFun(cuttlefish:conf_get("auth.http.request.connect_timeout", Conf, undefined))}],
|
||||||
|
case SslOpts of
|
||||||
|
[] -> Filter(Opts);
|
||||||
|
_ ->
|
||||||
|
TlsVers = ['tlsv1.2','tlsv1.1',tlsv1],
|
||||||
|
DefaultOpts = [{versions, TlsVers},
|
||||||
|
{ciphers, lists:foldl(
|
||||||
|
fun(TlsVer, Ciphers) ->
|
||||||
|
Ciphers ++ ssl:cipher_suites(all, TlsVer)
|
||||||
|
end, [], TlsVers)}],
|
||||||
|
Filter([{ssl, DefaultOpts ++ SslOpts} | Opts])
|
||||||
|
end
|
||||||
|
end}.
|
||||||
|
|
||||||
|
{mapping, "auth.http.request.retry_times", "emqx_auth_http.retry_opts", [
|
||||||
|
{default, 3},
|
||||||
|
{datatype, integer}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.http.request.retry_interval", "emqx_auth_http.retry_opts", [
|
||||||
|
{default, "1s"},
|
||||||
|
{datatype, {duration, ms}}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.http.request.retry_backoff", "emqx_auth_http.retry_opts", [
|
||||||
|
{default, 2.0},
|
||||||
|
{datatype, float}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{translation, "emqx_auth_http.retry_opts", fun(Conf) ->
|
||||||
|
[{times, cuttlefish:conf_get("auth.http.request.retry_times", Conf)},
|
||||||
|
{interval, cuttlefish:conf_get("auth.http.request.retry_interval", Conf)},
|
||||||
|
{backoff, cuttlefish:conf_get("auth.http.request.retry_backoff", Conf)}]
|
||||||
|
end}.
|
||||||
|
|
||||||
|
{mapping, "auth.http.header.$field", "emqx_auth_http.headers", [
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{translation, "emqx_auth_http.headers", fun(Conf) ->
|
||||||
|
lists:map(
|
||||||
|
fun({["auth", "http", "header", Field], Value}) ->
|
||||||
|
{Field, Value}
|
||||||
|
end,
|
||||||
|
cuttlefish_variable:filter_by_prefix("auth.http.header", Conf))
|
||||||
|
end}.
|
|
@ -0,0 +1,26 @@
|
||||||
|
{deps, []}.
|
||||||
|
|
||||||
|
{edoc_opts, [{preprocess, true}]}.
|
||||||
|
{erl_opts, [warn_unused_vars,
|
||||||
|
warn_shadow_vars,
|
||||||
|
warn_unused_import,
|
||||||
|
warn_obsolete_guard,
|
||||||
|
debug_info,
|
||||||
|
{parse_transform}]}.
|
||||||
|
|
||||||
|
{xref_checks, [undefined_function_calls, undefined_functions,
|
||||||
|
locals_not_used, deprecated_function_calls,
|
||||||
|
warnings_as_errors, deprecated_functions]}.
|
||||||
|
|
||||||
|
{cover_enabled, true}.
|
||||||
|
{cover_opts, [verbose]}.
|
||||||
|
{cover_export_enabled, true}.
|
||||||
|
|
||||||
|
{profiles,
|
||||||
|
[{test,
|
||||||
|
[{deps,
|
||||||
|
[{emqx_ct_helpers, {git, "https://github.com/emqx/emqx-ct-helpers", {tag, "1.2.2"}}},
|
||||||
|
{emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.2.0"}}}
|
||||||
|
]}
|
||||||
|
]}
|
||||||
|
]}.
|
|
@ -0,0 +1,92 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed 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.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_acl_http).
|
||||||
|
|
||||||
|
-include("emqx_auth_http.hrl").
|
||||||
|
|
||||||
|
-include_lib("emqx/include/emqx.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
|
-logger_header("[ACL http]").
|
||||||
|
|
||||||
|
-import(emqx_auth_http_cli,
|
||||||
|
[ request/8
|
||||||
|
, feedvar/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
%% ACL callbacks
|
||||||
|
-export([ register_metrics/0
|
||||||
|
, check_acl/5
|
||||||
|
, description/0
|
||||||
|
]).
|
||||||
|
|
||||||
|
-spec(register_metrics() -> ok).
|
||||||
|
register_metrics() ->
|
||||||
|
lists:foreach(fun emqx_metrics:ensure/1, ?ACL_METRICS).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% ACL callbacks
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
check_acl(ClientInfo, PubSub, Topic, AclResult, State) ->
|
||||||
|
return_with(fun inc_metrics/1,
|
||||||
|
do_check_acl(ClientInfo, PubSub, Topic, AclResult, State)).
|
||||||
|
|
||||||
|
do_check_acl(#{username := <<$$, _/binary>>}, _PubSub, _Topic, _AclResult, _Config) ->
|
||||||
|
ok;
|
||||||
|
do_check_acl(ClientInfo, PubSub, Topic, _AclResult, #{acl_req := AclReq,
|
||||||
|
http_opts := HttpOpts,
|
||||||
|
retry_opts := RetryOpts,
|
||||||
|
headers := Headers}) ->
|
||||||
|
ClientInfo1 = ClientInfo#{access => access(PubSub), topic => Topic},
|
||||||
|
case check_acl_request(AclReq, ClientInfo1, Headers, HttpOpts, RetryOpts) of
|
||||||
|
{ok, 200, "ignore"} -> ok;
|
||||||
|
{ok, 200, _Body} -> {stop, allow};
|
||||||
|
{ok, _Code, _Body} -> {stop, deny};
|
||||||
|
{error, Error} ->
|
||||||
|
?LOG(error, "Request ACL url ~s, error: ~p",
|
||||||
|
[AclReq#http_request.url, Error]),
|
||||||
|
ok
|
||||||
|
end.
|
||||||
|
|
||||||
|
description() -> "ACL with HTTP API".
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Internal functions
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
inc_metrics(ok) ->
|
||||||
|
emqx_metrics:inc(?ACL_METRICS(ignore));
|
||||||
|
inc_metrics({stop, allow}) ->
|
||||||
|
emqx_metrics:inc(?ACL_METRICS(allow));
|
||||||
|
inc_metrics({stop, deny}) ->
|
||||||
|
emqx_metrics:inc(?ACL_METRICS(deny)).
|
||||||
|
|
||||||
|
return_with(Fun, Result) ->
|
||||||
|
Fun(Result), Result.
|
||||||
|
|
||||||
|
check_acl_request(#http_request{url = Url,
|
||||||
|
method = Method,
|
||||||
|
content_type = ContentType,
|
||||||
|
params = Params,
|
||||||
|
options = Options},
|
||||||
|
ClientInfo, Headers, HttpOpts, RetryOpts) ->
|
||||||
|
request(Method, ContentType, Url, feedvar(Params, ClientInfo), Headers, HttpOpts, Options, RetryOpts).
|
||||||
|
|
||||||
|
access(subscribe) -> 1;
|
||||||
|
access(publish) -> 2.
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
{application, emqx_auth_http,
|
||||||
|
[{description, "EMQ X Authentication/ACL with HTTP API"},
|
||||||
|
{vsn, "git"},
|
||||||
|
{modules, []},
|
||||||
|
{registered, [emqx_auth_http_sup]},
|
||||||
|
{applications, [kernel,stdlib]},
|
||||||
|
{mod, {emqx_auth_http_app, []}},
|
||||||
|
{env, []},
|
||||||
|
{licenses, ["Apache-2.0"]},
|
||||||
|
{maintainers, ["EMQ X Team <contact@emqx.io>"]},
|
||||||
|
{links, [{"Homepage", "https://emqx.io/"},
|
||||||
|
{"Github", "https://github.com/emqx/emqx-auth-http"}
|
||||||
|
]}
|
||||||
|
]}.
|
|
@ -0,0 +1,24 @@
|
||||||
|
%%-*- mode: erlang -*-
|
||||||
|
%% .app.src.script
|
||||||
|
|
||||||
|
RemoveLeadingV =
|
||||||
|
fun(Tag) ->
|
||||||
|
case re:run(Tag, "^[v|e]?[0-9]\.[0-9]\.([0-9]|(rc|beta|alpha)\.[0-9])", [{capture, none}]) of
|
||||||
|
nomatch ->
|
||||||
|
re:replace(Tag, "/", "-", [{return ,list}]);
|
||||||
|
_ ->
|
||||||
|
%% if it is a version number prefixed by 'v' or 'e', then remove it
|
||||||
|
re:replace(Tag, "[v|e]", "", [{return ,list}])
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
case os:getenv("EMQX_DEPS_DEFAULT_VSN") of
|
||||||
|
false -> CONFIG; % env var not defined
|
||||||
|
[] -> CONFIG; % env var set to empty string
|
||||||
|
Tag ->
|
||||||
|
[begin
|
||||||
|
AppConf0 = lists:keystore(vsn, 1, AppConf, {vsn, RemoveLeadingV(Tag)}),
|
||||||
|
{application, App, AppConf0}
|
||||||
|
end || Conf = {application, App, AppConf} <- CONFIG]
|
||||||
|
end.
|
||||||
|
|
|
@ -0,0 +1,116 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed 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.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_auth_http).
|
||||||
|
|
||||||
|
-include("emqx_auth_http.hrl").
|
||||||
|
|
||||||
|
-include_lib("emqx/include/emqx.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
-include_lib("emqx/include/types.hrl").
|
||||||
|
|
||||||
|
-logger_header("[Auth http]").
|
||||||
|
|
||||||
|
-import(emqx_auth_http_cli,
|
||||||
|
[ request/8
|
||||||
|
, feedvar/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
%% Callbacks
|
||||||
|
-export([ register_metrics/0
|
||||||
|
, check/3
|
||||||
|
, description/0
|
||||||
|
]).
|
||||||
|
|
||||||
|
-spec(register_metrics() -> ok).
|
||||||
|
register_metrics() ->
|
||||||
|
lists:foreach(fun emqx_metrics:ensure/1, ?AUTH_METRICS).
|
||||||
|
|
||||||
|
check(ClientInfo, AuthResult, #{auth_req := AuthReq,
|
||||||
|
super_req := SuperReq,
|
||||||
|
http_opts := HttpOpts,
|
||||||
|
retry_opts := RetryOpts,
|
||||||
|
headers := Headers}) ->
|
||||||
|
case authenticate(AuthReq, ClientInfo, Headers, HttpOpts, RetryOpts) of
|
||||||
|
{ok, 200, "ignore"} ->
|
||||||
|
emqx_metrics:inc(?AUTH_METRICS(ignore)), ok;
|
||||||
|
{ok, 200, Body} ->
|
||||||
|
emqx_metrics:inc(?AUTH_METRICS(success)),
|
||||||
|
IsSuperuser = is_superuser(SuperReq, ClientInfo, Headers, HttpOpts, RetryOpts),
|
||||||
|
{stop, AuthResult#{is_superuser => IsSuperuser,
|
||||||
|
auth_result => success,
|
||||||
|
anonymous => false,
|
||||||
|
mountpoint => mountpoint(Body, ClientInfo)}};
|
||||||
|
{ok, Code, _Body} ->
|
||||||
|
?LOG(error, "Deny connection from url: ~s, response http code: ~p",
|
||||||
|
[AuthReq#http_request.url, Code]),
|
||||||
|
emqx_metrics:inc(?AUTH_METRICS(failure)),
|
||||||
|
{stop, AuthResult#{auth_result => http_to_connack_error(Code),
|
||||||
|
anonymous => false}};
|
||||||
|
{error, Error} ->
|
||||||
|
?LOG(error, "Request auth url: ~s, error: ~p",
|
||||||
|
[AuthReq#http_request.url, Error]),
|
||||||
|
emqx_metrics:inc(?AUTH_METRICS(failure)),
|
||||||
|
%%FIXME later: server_unavailable is not right.
|
||||||
|
{stop, AuthResult#{auth_result => server_unavailable,
|
||||||
|
anonymous => false}}
|
||||||
|
end.
|
||||||
|
|
||||||
|
description() -> "Authentication by HTTP API".
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Requests
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
authenticate(#http_request{url = Url,
|
||||||
|
method = Method,
|
||||||
|
content_type = ContentType,
|
||||||
|
params = Params,
|
||||||
|
options = Options},
|
||||||
|
ClientInfo, HttpHeaders, HttpOpts, RetryOpts) ->
|
||||||
|
request(Method, ContentType, Url, feedvar(Params, ClientInfo), HttpHeaders, HttpOpts, Options, RetryOpts).
|
||||||
|
|
||||||
|
-spec(is_superuser(maybe(#http_request{}), emqx_types:client(), list(), list(), list()) -> boolean()).
|
||||||
|
is_superuser(undefined, _ClientInfo, _HttpHeaders, _HttpOpts, _RetryOpts) ->
|
||||||
|
false;
|
||||||
|
is_superuser(#http_request{url = Url,
|
||||||
|
method = Method,
|
||||||
|
content_type = ContentType,
|
||||||
|
params = Params,
|
||||||
|
options = Options},
|
||||||
|
ClientInfo, HttpHeaders, HttpOpts, RetryOpts) ->
|
||||||
|
case request(Method, ContentType, Url, feedvar(Params, ClientInfo), HttpHeaders, HttpOpts, Options, RetryOpts) of
|
||||||
|
{ok, 200, _Body} -> true;
|
||||||
|
{ok, _Code, _Body} -> false;
|
||||||
|
{error, Error} -> ?LOG(error, "Request superuser url ~s, error: ~p", [Url, Error]),
|
||||||
|
false
|
||||||
|
end.
|
||||||
|
|
||||||
|
mountpoint(Body, #{mountpoint := Mountpoint}) ->
|
||||||
|
case emqx_json:safe_decode(iolist_to_binary(Body), [return_maps]) of
|
||||||
|
{error, _} -> Mountpoint;
|
||||||
|
{ok, Json} when is_map(Json) ->
|
||||||
|
maps:get(<<"mountpoint">>, Json, Mountpoint);
|
||||||
|
{ok, _NotMap} -> Mountpoint
|
||||||
|
end.
|
||||||
|
|
||||||
|
http_to_connack_error(400) -> bad_username_or_password;
|
||||||
|
http_to_connack_error(401) -> bad_username_or_password;
|
||||||
|
http_to_connack_error(403) -> not_authorized;
|
||||||
|
http_to_connack_error(429) -> banned;
|
||||||
|
http_to_connack_error(503) -> server_unavailable;
|
||||||
|
http_to_connack_error(504) -> server_busy;
|
||||||
|
http_to_connack_error(_) -> server_unavailable.
|
|
@ -0,0 +1,103 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed 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.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_auth_http_app).
|
||||||
|
|
||||||
|
-behaviour(application).
|
||||||
|
-behaviour(supervisor).
|
||||||
|
|
||||||
|
-emqx_plugin(auth).
|
||||||
|
|
||||||
|
-include("emqx_auth_http.hrl").
|
||||||
|
|
||||||
|
-export([ start/2
|
||||||
|
, stop/1
|
||||||
|
]).
|
||||||
|
-export([init/1]).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Application Callbacks
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
start(_StartType, _StartArgs) ->
|
||||||
|
with_env(auth_req, fun load_auth_hook/1),
|
||||||
|
with_env(acl_req, fun load_acl_hook/1),
|
||||||
|
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||||
|
|
||||||
|
load_auth_hook(AuthReq) ->
|
||||||
|
ok = emqx_auth_http:register_metrics(),
|
||||||
|
SuperReq = r(application:get_env(?APP, super_req, undefined)),
|
||||||
|
HttpOpts = application:get_env(?APP, http_opts, []),
|
||||||
|
RetryOpts = application:get_env(?APP, retry_opts, []),
|
||||||
|
Headers = application:get_env(?APP, headers, []),
|
||||||
|
Params = #{auth_req => AuthReq,
|
||||||
|
super_req => SuperReq,
|
||||||
|
http_opts => HttpOpts,
|
||||||
|
retry_opts => maps:from_list(RetryOpts),
|
||||||
|
headers => Headers},
|
||||||
|
emqx:hook('client.authenticate', {emqx_auth_http, check, [Params]}).
|
||||||
|
|
||||||
|
load_acl_hook(AclReq) ->
|
||||||
|
ok = emqx_acl_http:register_metrics(),
|
||||||
|
HttpOpts = application:get_env(?APP, http_opts, []),
|
||||||
|
RetryOpts = application:get_env(?APP, retry_opts, []),
|
||||||
|
Headers = application:get_env(?APP, headers, []),
|
||||||
|
Params = #{acl_req => AclReq,
|
||||||
|
http_opts => HttpOpts,
|
||||||
|
retry_opts => maps:from_list(RetryOpts),
|
||||||
|
headers => Headers},
|
||||||
|
emqx:hook('client.check_acl', {emqx_acl_http, check_acl, [Params]}).
|
||||||
|
|
||||||
|
stop(_State) ->
|
||||||
|
emqx:unhook('client.authenticate', {emqx_auth_http, check}),
|
||||||
|
emqx:unhook('client.check_acl', {emqx_acl_http, check_acl}).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Dummy supervisor
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
init([]) ->
|
||||||
|
{ok, { {one_for_all, 10, 100}, []} }.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Internel functions
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
with_env(Par, Fun) ->
|
||||||
|
case application:get_env(?APP, Par) of
|
||||||
|
undefined -> ok;
|
||||||
|
{ok, Req} -> Fun(r(Req))
|
||||||
|
end.
|
||||||
|
|
||||||
|
r(undefined) ->
|
||||||
|
undefined;
|
||||||
|
r(Config) ->
|
||||||
|
Method = proplists:get_value(method, Config, post),
|
||||||
|
ContentType = proplists:get_value(content_type, Config, 'x-www-form-urlencoded'),
|
||||||
|
Url = proplists:get_value(url, Config),
|
||||||
|
Params = proplists:get_value(params, Config),
|
||||||
|
#http_request{method = Method, content_type = ContentType, url = Url, params = Params, options = inet(Url)}.
|
||||||
|
|
||||||
|
inet(Url) ->
|
||||||
|
case uri_string:parse(Url) of
|
||||||
|
#{host := Host} ->
|
||||||
|
case inet:parse_address(Host) of
|
||||||
|
{ok, Ip} when tuple_size(Ip) =:= 8 ->
|
||||||
|
[{ipv6_host_with_brackets, true}, {socket_opts, [{ipfamily, inet6}]}];
|
||||||
|
_ -> []
|
||||||
|
end;
|
||||||
|
_ -> []
|
||||||
|
end.
|
|
@ -0,0 +1,101 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed 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.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_auth_http_cli).
|
||||||
|
|
||||||
|
-export([ request/8
|
||||||
|
, feedvar/2
|
||||||
|
, feedvar/3
|
||||||
|
]).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% HTTP Request
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
request(get, _ContentType, Url, Params, HttpHeaders, HttpOpts, Options, RetryOpts) ->
|
||||||
|
Req = {Url ++ "?" ++ cow_qs:qs(bin_kw(Params)), HttpHeaders},
|
||||||
|
reply(request_(get, Req, [{autoredirect, true} | HttpOpts], Options, RetryOpts));
|
||||||
|
|
||||||
|
request(post, 'x-www-form-urlencoded', Url, Params, HttpHeaders, HttpOpts, Options, RetryOpts) ->
|
||||||
|
Req = {Url, HttpHeaders, "application/x-www-form-urlencoded", cow_qs:qs(bin_kw(Params))},
|
||||||
|
reply(request_(post, Req, [{autoredirect, true} | HttpOpts], Options, RetryOpts));
|
||||||
|
|
||||||
|
request(post, json, Url, Params, HttpHeaders, HttpOpts, Options, RetryOpts) ->
|
||||||
|
Req = {Url, HttpHeaders, "application/json", emqx_json:encode(bin_kw(Params))},
|
||||||
|
reply(request_(post, Req, [{autoredirect, true} | HttpOpts], Options, RetryOpts)).
|
||||||
|
|
||||||
|
request_(Method, Req, HTTPOpts, Opts, RetryOpts = #{times := Times,
|
||||||
|
interval := Interval,
|
||||||
|
backoff := BackOff}) ->
|
||||||
|
case httpc:request(Method, Req, HTTPOpts, Opts) of
|
||||||
|
{error, _Reason} when Times > 0 ->
|
||||||
|
timer:sleep(trunc(Interval)),
|
||||||
|
RetryOpts1 = RetryOpts#{times := Times - 1,
|
||||||
|
interval := Interval * BackOff},
|
||||||
|
request_(Method, Req, HTTPOpts, Opts, RetryOpts1);
|
||||||
|
Other -> Other
|
||||||
|
end.
|
||||||
|
|
||||||
|
reply({ok, {{_, Code, _}, _Headers, Body}}) ->
|
||||||
|
{ok, Code, Body};
|
||||||
|
reply({ok, Code, Body}) ->
|
||||||
|
{ok, Code, Body};
|
||||||
|
reply({error, Error}) ->
|
||||||
|
{error, Error}.
|
||||||
|
|
||||||
|
%% TODO: move this conversion to cuttlefish config and schema
|
||||||
|
bin_kw(KeywordList) when is_list(KeywordList) ->
|
||||||
|
[{bin(K), bin(V)} || {K, V} <- KeywordList].
|
||||||
|
|
||||||
|
bin(Atom) when is_atom(Atom) ->
|
||||||
|
list_to_binary(atom_to_list(Atom));
|
||||||
|
bin(Int) when is_integer(Int) ->
|
||||||
|
integer_to_binary(Int);
|
||||||
|
bin(Float) when is_float(Float) ->
|
||||||
|
float_to_binary(Float, [{decimals, 12}, compact]);
|
||||||
|
bin(List) when is_list(List)->
|
||||||
|
list_to_binary(List);
|
||||||
|
bin(Binary) when is_binary(Binary) ->
|
||||||
|
Binary.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Feed Variables
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
feedvar(Params, ClientInfo = #{clientid := ClientId,
|
||||||
|
protocol := Protocol,
|
||||||
|
peerhost := Peerhost}) ->
|
||||||
|
lists:map(fun({Param, "%u"}) -> {Param, maps:get(username, ClientInfo, null)};
|
||||||
|
({Param, "%c"}) -> {Param, ClientId};
|
||||||
|
({Param, "%r"}) -> {Param, Protocol};
|
||||||
|
({Param, "%a"}) -> {Param, inet:ntoa(Peerhost)};
|
||||||
|
({Param, "%P"}) -> {Param, maps:get(password, ClientInfo, null)};
|
||||||
|
({Param, "%p"}) -> {Param, maps:get(sockport, ClientInfo, null)};
|
||||||
|
({Param, "%C"}) -> {Param, maps:get(cn, ClientInfo, null)};
|
||||||
|
({Param, "%d"}) -> {Param, maps:get(dn, ClientInfo, null)};
|
||||||
|
({Param, "%A"}) -> {Param, maps:get(access, ClientInfo, null)};
|
||||||
|
({Param, "%t"}) -> {Param, maps:get(topic, ClientInfo, null)};
|
||||||
|
({Param, "%m"}) -> {Param, maps:get(mountpoint, ClientInfo, null)};
|
||||||
|
({Param, Var}) -> {Param, Var}
|
||||||
|
end, Params).
|
||||||
|
|
||||||
|
feedvar(Params, Var, Val) ->
|
||||||
|
lists:map(fun({Param, Var0}) when Var0 == Var ->
|
||||||
|
{Param, Val};
|
||||||
|
({Param, Var0}) ->
|
||||||
|
{Param, Var0}
|
||||||
|
end, Params).
|
||||||
|
|
|
@ -0,0 +1,167 @@
|
||||||
|
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed 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.
|
||||||
|
|
||||||
|
-module(emqx_auth_http_SUITE).
|
||||||
|
|
||||||
|
-compile(export_all).
|
||||||
|
-compile(nowarn_export_all).
|
||||||
|
|
||||||
|
-include_lib("emqx/include/emqx.hrl").
|
||||||
|
-include_lib("common_test/include/ct.hrl").
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
|
-define(APP, emqx_auth_http).
|
||||||
|
|
||||||
|
-define(USER(ClientId, Username, Protocol, Peerhost, Zone),
|
||||||
|
#{clientid => ClientId, username => Username, protocol => Protocol,
|
||||||
|
peerhost => Peerhost, zone => Zone}).
|
||||||
|
|
||||||
|
-define(USER(ClientId, Username, Protocol, Peerhost, Zone, Mountpoint),
|
||||||
|
#{clientid => ClientId, username => Username, protocol => Protocol,
|
||||||
|
peerhost => Peerhost, zone => Zone, mountpoint => Mountpoint}).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Setups
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
all() ->
|
||||||
|
[{group, http_inet},
|
||||||
|
{group, http_inet6},
|
||||||
|
{group, https_inet},
|
||||||
|
{group, https_inet6}].
|
||||||
|
|
||||||
|
groups() ->
|
||||||
|
Cases = emqx_ct:all(?MODULE),
|
||||||
|
[{Name, Cases} || Name <- [http_inet, http_inet6, https_inet, https_inet6]].
|
||||||
|
|
||||||
|
init_per_group(GrpName, Cfg) ->
|
||||||
|
[Schema, Inet] = [list_to_atom(X) || X <- string:tokens(atom_to_list(GrpName), "_")],
|
||||||
|
http_auth_server:start(Schema, Inet),
|
||||||
|
Fun = fun(App) -> set_special_configs(App, Schema, Inet) end,
|
||||||
|
emqx_ct_helpers:start_apps([emqx_auth_http], Fun),
|
||||||
|
Cfg.
|
||||||
|
|
||||||
|
end_per_group(_GrpName, _Cfg) ->
|
||||||
|
http_auth_server:stop(),
|
||||||
|
emqx_ct_helpers:stop_apps([emqx_auth_http, emqx]).
|
||||||
|
|
||||||
|
set_special_configs(emqx, _Schmea, _Inet) ->
|
||||||
|
application:set_env(emqx, allow_anonymous, true),
|
||||||
|
application:set_env(emqx, enable_acl_cache, false),
|
||||||
|
LoadedPluginPath = filename:join(["test", "emqx_SUITE_data", "loaded_plugins"]),
|
||||||
|
application:set_env(emqx, plugins_loaded_file,
|
||||||
|
emqx_ct_helpers:deps_path(emqx, LoadedPluginPath));
|
||||||
|
|
||||||
|
set_special_configs(emqx_auth_http, Schema, Inet) ->
|
||||||
|
AuthReq = maps:from_list(application:get_env(emqx_auth_http, auth_req, [])),
|
||||||
|
SuprReq = maps:from_list(application:get_env(emqx_auth_http, super_req, [])),
|
||||||
|
AclReq = maps:from_list(application:get_env(emqx_auth_http, acl_req, [])),
|
||||||
|
SvrAddr = http_server_host(Schema, Inet),
|
||||||
|
|
||||||
|
AuthReq1 = AuthReq#{method := get, url := SvrAddr ++ "/mqtt/auth"},
|
||||||
|
SuprReq1 = SuprReq#{method := post, content_type := 'x-www-form-urlencoded', url := SvrAddr ++ "/mqtt/superuser"},
|
||||||
|
AclReq1 = AclReq #{method := post, content_type := json, url := SvrAddr ++ "/mqtt/acl"},
|
||||||
|
|
||||||
|
Schema =:= https andalso set_https_client_opts(),
|
||||||
|
|
||||||
|
application:set_env(emqx_auth_http, auth_req, maps:to_list(AuthReq1)),
|
||||||
|
application:set_env(emqx_auth_http, super_req, maps:to_list(SuprReq1)),
|
||||||
|
application:set_env(emqx_auth_http, acl_req, maps:to_list(AclReq1)).
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
set_https_client_opts() ->
|
||||||
|
HttpOpts = maps:from_list(application:get_env(emqx_auth_http, http_opts, [])),
|
||||||
|
HttpOpts1 = HttpOpts#{ssl => emqx_ct_helpers:client_ssl_twoway()},
|
||||||
|
application:set_env(emqx_auth_http, http_opts, maps:to_list(HttpOpts1)).
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
http_server_host(http, inet) -> "http://127.0.0.1:8991";
|
||||||
|
http_server_host(http, inet6) -> "http://[::1]:8991";
|
||||||
|
http_server_host(https, inet) -> "https://127.0.0.1:8991";
|
||||||
|
http_server_host(https, inet6) -> "https://[::1]:8991".
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% Testcases
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
t_check_acl(_) ->
|
||||||
|
SuperUser = ?USER(<<"superclient">>, <<"superuser">>, mqtt, {127,0,0,1}, external),
|
||||||
|
deny = emqx_access_control:check_acl(SuperUser, subscribe, <<"users/testuser/1">>),
|
||||||
|
deny = emqx_access_control:check_acl(SuperUser, publish, <<"anytopic">>),
|
||||||
|
|
||||||
|
User1 = ?USER(<<"client1">>, <<"testuser">>, mqtt, {127,0,0,1}, external),
|
||||||
|
UnIpUser1 = ?USER(<<"client1">>, <<"testuser">>, mqtt, {192,168,0,4}, external),
|
||||||
|
UnClientIdUser1 = ?USER(<<"unkonwc">>, <<"testuser">>, mqtt, {127,0,0,1}, external),
|
||||||
|
UnnameUser1= ?USER(<<"client1">>, <<"unuser">>, mqtt, {127,0,0,1}, external),
|
||||||
|
allow = emqx_access_control:check_acl(User1, subscribe, <<"users/testuser/1">>),
|
||||||
|
deny = emqx_access_control:check_acl(User1, publish, <<"users/testuser/1">>),
|
||||||
|
deny = emqx_access_control:check_acl(UnIpUser1, subscribe, <<"users/testuser/1">>),
|
||||||
|
deny = emqx_access_control:check_acl(UnClientIdUser1, subscribe, <<"users/testuser/1">>),
|
||||||
|
deny = emqx_access_control:check_acl(UnnameUser1, subscribe, <<"$SYS/testuser/1">>),
|
||||||
|
|
||||||
|
User2 = ?USER(<<"client2">>, <<"xyz">>, mqtt, {127,0,0,1}, external),
|
||||||
|
UserC = ?USER(<<"client2">>, <<"xyz">>, mqtt, {192,168,1,3}, external),
|
||||||
|
allow = emqx_access_control:check_acl(UserC, publish, <<"a/b/c">>),
|
||||||
|
deny = emqx_access_control:check_acl(User2, publish, <<"a/b/c">>),
|
||||||
|
deny = emqx_access_control:check_acl(User2, subscribe, <<"$SYS/testuser/1">>).
|
||||||
|
|
||||||
|
t_check_auth(_) ->
|
||||||
|
User1 = ?USER(<<"client1">>, <<"testuser1">>, mqtt, {127,0,0,1}, external, undefined),
|
||||||
|
User2 = ?USER(<<"client2">>, <<"testuser2">>, mqtt, {127,0,0,1}, exteneral, undefined),
|
||||||
|
User3 = ?USER(<<"client3">>, undefined, mqtt, {127,0,0,1}, exteneral, undefined),
|
||||||
|
|
||||||
|
{ok, #{auth_result := success,
|
||||||
|
anonymous := false,
|
||||||
|
is_superuser := false}} = emqx_access_control:authenticate(User1#{password => <<"pass1">>}),
|
||||||
|
{error, bad_username_or_password} = emqx_access_control:authenticate(User1#{password => <<"pass">>}),
|
||||||
|
{error, bad_username_or_password} = emqx_access_control:authenticate(User1#{password => <<>>}),
|
||||||
|
|
||||||
|
{ok, #{is_superuser := false}} = emqx_access_control:authenticate(User2#{password => <<"pass2">>}),
|
||||||
|
{error, bad_username_or_password} = emqx_access_control:authenticate(User2#{password => <<>>}),
|
||||||
|
{error, bad_username_or_password} = emqx_access_control:authenticate(User2#{password => <<"errorpwd">>}),
|
||||||
|
|
||||||
|
{error, bad_username_or_password} = emqx_access_control:authenticate(User3#{password => <<"pwd">>}).
|
||||||
|
|
||||||
|
t_sub_pub(_) ->
|
||||||
|
ct:pal("start client"),
|
||||||
|
{ok, T1} = emqtt:start_link([{host, "localhost"},
|
||||||
|
{clientid, <<"client1">>},
|
||||||
|
{username, <<"testuser1">>},
|
||||||
|
{password, <<"pass1">>}]),
|
||||||
|
{ok, _} = emqtt:connect(T1),
|
||||||
|
emqtt:publish(T1, <<"topic">>, <<"body">>, [{qos, 0}, {retain, true}]),
|
||||||
|
timer:sleep(1000),
|
||||||
|
{ok, T2} = emqtt:start_link([{host, "localhost"},
|
||||||
|
{clientid, <<"client2">>},
|
||||||
|
{username, <<"testuser2">>},
|
||||||
|
{password, <<"pass2">>}]),
|
||||||
|
{ok, _} = emqtt:connect(T2),
|
||||||
|
emqtt:subscribe(T2, <<"topic">>),
|
||||||
|
receive
|
||||||
|
{publish, _Topic, Payload} ->
|
||||||
|
?assertEqual(<<"body">>, Payload)
|
||||||
|
after 1000 -> false end,
|
||||||
|
emqtt:disconnect(T1),
|
||||||
|
emqtt:disconnect(T2).
|
||||||
|
|
||||||
|
t_comment_config(_) ->
|
||||||
|
AuthCount = length(emqx_hooks:lookup('client.authenticate')),
|
||||||
|
AclCount = length(emqx_hooks:lookup('client.check_acl')),
|
||||||
|
application:stop(?APP),
|
||||||
|
[application:unset_env(?APP, Par) || Par <- [acl_req, auth_req]],
|
||||||
|
application:start(?APP),
|
||||||
|
?assertEqual([], emqx_hooks:lookup('client.authenticate')),
|
||||||
|
?assertEqual(AuthCount - 1, length(emqx_hooks:lookup('client.authenticate'))),
|
||||||
|
?assertEqual(AclCount - 1, length(emqx_hooks:lookup('client.check_acl'))).
|
||||||
|
|
|
@ -0,0 +1,152 @@
|
||||||
|
-module(http_auth_server).
|
||||||
|
|
||||||
|
-export([ start/2
|
||||||
|
, stop/0
|
||||||
|
]).
|
||||||
|
|
||||||
|
-define(SUPERUSER, [[{"username", "superuser"}, {"clientid", "superclient"}]]).
|
||||||
|
|
||||||
|
-define(ACL, [[{<<"username">>, <<"testuser">>},
|
||||||
|
{<<"clientid">>, <<"client1">>},
|
||||||
|
{<<"access">>, <<"1">>},
|
||||||
|
{<<"topic">>, <<"users/testuser/1">>},
|
||||||
|
{<<"ipaddr">>, <<"127.0.0.1">>},
|
||||||
|
{<<"mountpoint">>, <<"null">>}],
|
||||||
|
[{<<"username">>, <<"xyz">>},
|
||||||
|
{<<"clientid">>, <<"client2">>},
|
||||||
|
{<<"access">>, <<"2">>},
|
||||||
|
{<<"topic">>, <<"a/b/c">>},
|
||||||
|
{<<"ipaddr">>, <<"192.168.1.3">>},
|
||||||
|
{<<"mountpoint">>, <<"null">>}],
|
||||||
|
[{<<"username">>, <<"testuser1">>},
|
||||||
|
{<<"clientid">>, <<"client1">>},
|
||||||
|
{<<"access">>, <<"2">>},
|
||||||
|
{<<"topic">>, <<"topic">>},
|
||||||
|
{<<"ipaddr">>, <<"127.0.0.1">>},
|
||||||
|
{<<"mountpoint">>, <<"null">>}],
|
||||||
|
[{<<"username">>, <<"testuser2">>},
|
||||||
|
{<<"clientid">>, <<"client2">>},
|
||||||
|
{<<"access">>, <<"1">>},
|
||||||
|
{<<"topic">>, <<"topic">>},
|
||||||
|
{<<"ipaddr">>, <<"127.0.0.1">>},
|
||||||
|
{<<"mountpoint">>, <<"null">>}]]).
|
||||||
|
|
||||||
|
-define(AUTH, [[{<<"clientid">>, <<"client1">>},
|
||||||
|
{<<"username">>, <<"testuser1">>},
|
||||||
|
{<<"password">>, <<"pass1">>}],
|
||||||
|
[{<<"clientid">>, <<"client2">>},
|
||||||
|
{<<"username">>, <<"testuser2">>},
|
||||||
|
{<<"password">>, <<"pass2">>}]]).
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% REST Interface
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-rest_api(#{ name => auth
|
||||||
|
, method => 'GET'
|
||||||
|
, path => "/mqtt/auth"
|
||||||
|
, func => authenticate
|
||||||
|
, descr => "Authenticate user access permission"
|
||||||
|
}).
|
||||||
|
|
||||||
|
-rest_api(#{ name => is_superuser
|
||||||
|
, method => 'GET'
|
||||||
|
, path => "/mqtt/superuser"
|
||||||
|
, func => is_superuser
|
||||||
|
, descr => "Is super user"
|
||||||
|
}).
|
||||||
|
|
||||||
|
-rest_api(#{ name => acl
|
||||||
|
, method => 'GET'
|
||||||
|
, path => "/mqtt/acl"
|
||||||
|
, func => check_acl
|
||||||
|
, descr => "Check acl"
|
||||||
|
}).
|
||||||
|
|
||||||
|
-rest_api(#{ name => auth
|
||||||
|
, method => 'POST'
|
||||||
|
, path => "/mqtt/auth"
|
||||||
|
, func => authenticate
|
||||||
|
, descr => "Authenticate user access permission"
|
||||||
|
}).
|
||||||
|
|
||||||
|
-rest_api(#{ name => is_superuser
|
||||||
|
, method => 'POST'
|
||||||
|
, path => "/mqtt/superuser"
|
||||||
|
, func => is_superuser
|
||||||
|
, descr => "Is super user"
|
||||||
|
}).
|
||||||
|
|
||||||
|
-rest_api(#{ name => acl
|
||||||
|
, method => 'POST'
|
||||||
|
, path => "/mqtt/acl"
|
||||||
|
, func => check_acl
|
||||||
|
, descr => "Check acl"
|
||||||
|
}).
|
||||||
|
|
||||||
|
-export([ authenticate/2
|
||||||
|
, is_superuser/2
|
||||||
|
, check_acl/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
authenticate(_Binding, Params) ->
|
||||||
|
return(check(Params, ?AUTH)).
|
||||||
|
|
||||||
|
is_superuser(_Binding, Params) ->
|
||||||
|
return(check(Params, ?SUPERUSER)).
|
||||||
|
|
||||||
|
check_acl(_Binding, Params) ->
|
||||||
|
return(check(Params, ?ACL)).
|
||||||
|
|
||||||
|
return(allow) -> {200, <<"allow">>};
|
||||||
|
return(deny) -> {400, <<"deny">>}.
|
||||||
|
|
||||||
|
start(http, Inet) ->
|
||||||
|
application:ensure_all_started(minirest),
|
||||||
|
Handlers = [{"/", minirest:handler(#{modules => [?MODULE]})}],
|
||||||
|
Dispatch = [{"/[...]", minirest, Handlers}],
|
||||||
|
minirest:start_http(http_auth_server, #{socket_opts => [Inet, {port, 8991}]}, Dispatch);
|
||||||
|
|
||||||
|
start(https, Inet) ->
|
||||||
|
application:ensure_all_started(minirest),
|
||||||
|
Handlers = [{"/", minirest:handler(#{modules => [?MODULE]})}],
|
||||||
|
Dispatch = [{"/[...]", minirest, Handlers}],
|
||||||
|
minirest:start_https(http_auth_server, #{socket_opts => [Inet, {port, 8991} | certopts()]}, Dispatch).
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
certopts() ->
|
||||||
|
Certfile = filename:join(["etc", "certs", "cert.pem"]),
|
||||||
|
Keyfile = filename:join(["etc", "certs", "key.pem"]),
|
||||||
|
CaCert = filename:join(["etc", "certs", "cacert.pem"]),
|
||||||
|
[{verify, verify_peer},
|
||||||
|
{certfile, emqx_ct_helpers:deps_path(emqx, Certfile)},
|
||||||
|
{keyfile, emqx_ct_helpers:deps_path(emqx, Keyfile)},
|
||||||
|
{cacertfile, emqx_ct_helpers:deps_path(emqx, CaCert)}] ++ emqx_ct_helpers:client_ssl().
|
||||||
|
|
||||||
|
stop() ->
|
||||||
|
minirest:stop_http(http_auth_server).
|
||||||
|
|
||||||
|
-spec check(HttpReqParams :: list(), DefinedConf :: list()) -> allow | deny.
|
||||||
|
check(_Params, []) ->
|
||||||
|
%ct:pal("check auth_result: deny~n"),
|
||||||
|
deny;
|
||||||
|
check(Params, [ConfRecord|T]) ->
|
||||||
|
% ct:pal("Params: ~p, ConfRecord:~p ~n", [Params, ConfRecord]),
|
||||||
|
case match_config(Params, ConfRecord) of
|
||||||
|
not_match ->
|
||||||
|
check(Params, T);
|
||||||
|
matched -> allow
|
||||||
|
end.
|
||||||
|
|
||||||
|
match_config([], _ConfigColumn) ->
|
||||||
|
%ct:pal("match_config auth_result: matched~n"),
|
||||||
|
matched;
|
||||||
|
|
||||||
|
match_config([Param|T], ConfigColumn) ->
|
||||||
|
%ct:pal("Param: ~p, ConfigColumn:~p ~n", [Param, ConfigColumn]),
|
||||||
|
case lists:member(Param, ConfigColumn) of
|
||||||
|
true ->
|
||||||
|
match_config(T, ConfigColumn);
|
||||||
|
false ->
|
||||||
|
not_match
|
||||||
|
end.
|
|
@ -0,0 +1,29 @@
|
||||||
|
name: Run test cases
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run_test_cases:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
container:
|
||||||
|
image: erlang:22.1
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- name: run test cases
|
||||||
|
run: |
|
||||||
|
make xref
|
||||||
|
make eunit
|
||||||
|
make ct
|
||||||
|
make cover
|
||||||
|
- uses: actions/upload-artifact@v1
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: _build/test/logs
|
||||||
|
- uses: actions/upload-artifact@v1
|
||||||
|
with:
|
||||||
|
name: cover
|
||||||
|
path: _build/test/cover
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
.eunit
|
||||||
|
deps
|
||||||
|
*.o
|
||||||
|
*.beam
|
||||||
|
*.plt
|
||||||
|
erl_crash.dump
|
||||||
|
ebin
|
||||||
|
rel/example_project
|
||||||
|
.concrete/DEV_MODE
|
||||||
|
.rebar
|
||||||
|
.erlang.mk/
|
||||||
|
emqx_auth_jwt.d
|
||||||
|
data/
|
||||||
|
.DS_Store
|
||||||
|
cover/
|
||||||
|
ct.coverdata
|
||||||
|
eunit.coverdata
|
||||||
|
logs/
|
||||||
|
test/ct.cover.spec
|
||||||
|
emq_auth_jwt.d
|
||||||
|
erlang.mk
|
||||||
|
_build/
|
||||||
|
rebar.lock
|
||||||
|
rebar3.crashdump
|
||||||
|
etc/emqx_auth_jwt.conf.rendered
|
||||||
|
.rebar3/
|
||||||
|
*.swp
|
|
@ -0,0 +1,201 @@
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright {yyyy} {name of copyright owner}
|
||||||
|
|
||||||
|
Licensed 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.
|
|
@ -0,0 +1,90 @@
|
||||||
|
|
||||||
|
# emqx-auth-jwt
|
||||||
|
|
||||||
|
EMQ X JWT Authentication Plugin
|
||||||
|
|
||||||
|
Build
|
||||||
|
-----
|
||||||
|
|
||||||
|
```
|
||||||
|
make && make tests
|
||||||
|
```
|
||||||
|
|
||||||
|
Configure the Plugin
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
File: etc/plugins/emqx_auth_jwt.conf
|
||||||
|
|
||||||
|
```
|
||||||
|
## HMAC Hash Secret.
|
||||||
|
##
|
||||||
|
## Value: String
|
||||||
|
auth.jwt.secret = emqxsecret
|
||||||
|
|
||||||
|
## From where the JWT string can be got
|
||||||
|
##
|
||||||
|
## Value: username | password
|
||||||
|
## Default: password
|
||||||
|
auth.jwt.from = password
|
||||||
|
|
||||||
|
## RSA or ECDSA public key file.
|
||||||
|
##
|
||||||
|
## Value: File
|
||||||
|
## auth.jwt.pubkey = etc/certs/jwt_public_key.pem
|
||||||
|
|
||||||
|
## Enable to verify claims fields
|
||||||
|
##
|
||||||
|
## Value: on | off
|
||||||
|
auth.jwt.verify_claims = off
|
||||||
|
|
||||||
|
## The checklist of claims to validate
|
||||||
|
##
|
||||||
|
## Value: String
|
||||||
|
## auth.jwt.verify_claims.$name = expected
|
||||||
|
##
|
||||||
|
## Variables:
|
||||||
|
## - %u: username
|
||||||
|
## - %c: clientid
|
||||||
|
# auth.jwt.verify_claims.username = %u
|
||||||
|
```
|
||||||
|
|
||||||
|
Load the Plugin
|
||||||
|
---------------
|
||||||
|
|
||||||
|
```
|
||||||
|
./bin/emqx_ctl plugins load emqx_auth_jwt
|
||||||
|
```
|
||||||
|
|
||||||
|
Example
|
||||||
|
-------
|
||||||
|
|
||||||
|
```
|
||||||
|
mosquitto_pub -t 'pub' -m 'hello' -i test -u test -P eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiYm9iIiwiYWdlIjoyOX0.bIV_ZQ8D5nQi0LT8AVkpM4Pd6wmlbpR9S8nOLJAsA8o
|
||||||
|
```
|
||||||
|
|
||||||
|
Algorithms
|
||||||
|
----------
|
||||||
|
|
||||||
|
The JWT spec supports several algorithms for cryptographic signing. This plugin currently supports:
|
||||||
|
|
||||||
|
* HS256 - HMAC using SHA-256 hash algorithm
|
||||||
|
* HS384 - HMAC using SHA-384 hash algorithm
|
||||||
|
* HS512 - HMAC using SHA-512 hash algorithm
|
||||||
|
|
||||||
|
* RS256 - RSA with the SHA-256 hash algorithm
|
||||||
|
* RS384 - RSA with the SHA-384 hash algorithm
|
||||||
|
* RS512 - RSA with the SHA-512 hash algorithm
|
||||||
|
|
||||||
|
* ES256 - ECDSA using the P-256 curve
|
||||||
|
* ES384 - ECDSA using the P-384 curve
|
||||||
|
* ES512 - ECDSA using the P-512 curve
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
|
Apache License Version 2.0
|
||||||
|
|
||||||
|
Author
|
||||||
|
------
|
||||||
|
|
||||||
|
EMQ X Team.
|
|
@ -0,0 +1,2 @@
|
||||||
|
1. Notice for the [Critical vulnerabilities in JSON Web Token](https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/)
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
|
||||||
|
https://crypto.stackexchange.com/questions/30657/hmac-vs-ecdsa-for-jwt
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
##--------------------------------------------------------------------
|
||||||
|
## JWT Auth Plugin
|
||||||
|
##--------------------------------------------------------------------
|
||||||
|
|
||||||
|
## HMAC Hash Secret.
|
||||||
|
##
|
||||||
|
## Value: String
|
||||||
|
auth.jwt.secret = emqxsecret
|
||||||
|
|
||||||
|
## From where the JWT string can be got
|
||||||
|
##
|
||||||
|
## Value: username | password
|
||||||
|
## Default: password
|
||||||
|
auth.jwt.from = password
|
||||||
|
|
||||||
|
## RSA or ECDSA public key file.
|
||||||
|
##
|
||||||
|
## Value: File
|
||||||
|
## auth.jwt.pubkey = etc/certs/jwt_public_key.pem
|
||||||
|
|
||||||
|
## Enable to verify claims fields
|
||||||
|
##
|
||||||
|
## Value: on | off
|
||||||
|
auth.jwt.verify_claims = off
|
||||||
|
|
||||||
|
## The checklist of claims to validate
|
||||||
|
##
|
||||||
|
## Value: String
|
||||||
|
## auth.jwt.verify_claims.$name = expected
|
||||||
|
##
|
||||||
|
## Variables:
|
||||||
|
## - %u: username
|
||||||
|
## - %c: clientid
|
||||||
|
# auth.jwt.verify_claims.username = %u
|
||||||
|
|
||||||
|
## The Signature format
|
||||||
|
## - `der`: The erlang default format
|
||||||
|
## - `raw`: Compatible with others platform maybe
|
||||||
|
#auth.jwt.signature_format = der
|
|
@ -0,0 +1,48 @@
|
||||||
|
%%-*- mode: erlang -*-
|
||||||
|
|
||||||
|
{mapping, "auth.jwt.secret", "emqx_auth_jwt.secret", [
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.jwt.from", "emqx_auth_jwt.from", [
|
||||||
|
{default, password},
|
||||||
|
{datatype, atom}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.jwt.pubkey", "emqx_auth_jwt.pubkey", [
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.jwt.verify_claims", "emqx_auth_jwt.verify_claims", [
|
||||||
|
{default, off},
|
||||||
|
{datatype, flag}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.jwt.verify_claims.$name", "emqx_auth_jwt.verify_claims", [
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{translation, "emqx_auth_jwt.verify_claims", fun(Conf) ->
|
||||||
|
case cuttlefish:conf_get("auth.jwt.verify_claims", Conf) of
|
||||||
|
false -> cuttlefish:unset();
|
||||||
|
true ->
|
||||||
|
lists:foldr(
|
||||||
|
fun({["auth","jwt","verify_claims", Name], Value}, Acc) ->
|
||||||
|
[{list_to_atom(Name), list_to_binary(Value)} | Acc];
|
||||||
|
({["auth","jwt","verify_claims"], _Value}, Acc) ->
|
||||||
|
Acc
|
||||||
|
end, [], cuttlefish_variable:filter_by_prefix("auth.jwt.verify_claims", Conf))
|
||||||
|
end
|
||||||
|
end}.
|
||||||
|
|
||||||
|
{mapping, "auth.jwt.signature_format", "emqx_auth_jwt.jwerl_opts", [
|
||||||
|
{default, "der"},
|
||||||
|
{datatype, {enum, [raw, der]}}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{translation, "emqx_auth_jwt.jwerl_opts", fun(Conf) ->
|
||||||
|
Filter = fun(L) -> [I || I <- L, I /= undefined] end,
|
||||||
|
maps:from_list(Filter(
|
||||||
|
[{raw, cuttlefish:conf_get("auth.jwt.signature_format", Conf) == raw}]
|
||||||
|
))
|
||||||
|
end}.
|
|
@ -0,0 +1,24 @@
|
||||||
|
{deps,
|
||||||
|
[{jwerl, {git, "https://github.com/emqx/jwerl.git", {branch, "1.1.1"}}}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{edoc_opts, [{preprocess, true}]}.
|
||||||
|
{erl_opts, [warn_unused_vars,
|
||||||
|
warn_shadow_vars,
|
||||||
|
warn_unused_import,
|
||||||
|
warn_obsolete_guard,
|
||||||
|
debug_info,
|
||||||
|
{parse_transform}]}.
|
||||||
|
|
||||||
|
{xref_checks, [undefined_function_calls, undefined_functions,
|
||||||
|
locals_not_used, deprecated_function_calls,
|
||||||
|
warnings_as_errors, deprecated_functions]}.
|
||||||
|
{cover_enabled, true}.
|
||||||
|
{cover_opts, [verbose]}.
|
||||||
|
{cover_export_enabled, true}.
|
||||||
|
|
||||||
|
{profiles,
|
||||||
|
[{test,
|
||||||
|
[{deps, [{emqx_ct_helpers, {git, "http://github.com/emqx/emqx-ct-helpers", {tag, "1.2.2"}}}]}
|
||||||
|
]}
|
||||||
|
]}.
|
|
@ -0,0 +1,14 @@
|
||||||
|
{application, emqx_auth_jwt,
|
||||||
|
[{description, "EMQ X Authentication with JWT"},
|
||||||
|
{vsn, "git"},
|
||||||
|
{modules, []},
|
||||||
|
{registered, [emqx_auth_jwt_sup]},
|
||||||
|
{applications, [kernel,stdlib,jwerl]},
|
||||||
|
{mod, {emqx_auth_jwt_app, []}},
|
||||||
|
{env, []},
|
||||||
|
{licenses, ["Apache-2.0"]},
|
||||||
|
{maintainers, ["EMQ X Team <contact@emqx.io>"]},
|
||||||
|
{links, [{"Homepage", "https://emqx.io/"},
|
||||||
|
{"Github", "https://github.com/emqx/emqx-auth-jwt"}
|
||||||
|
]}
|
||||||
|
]}.
|
|
@ -0,0 +1,24 @@
|
||||||
|
%%-*- mode: erlang -*-
|
||||||
|
%% .app.src.script
|
||||||
|
|
||||||
|
RemoveLeadingV =
|
||||||
|
fun(Tag) ->
|
||||||
|
case re:run(Tag, "^[v|e]?[0-9]\.[0-9]\.([0-9]|(rc|beta|alpha)\.[0-9])", [{capture, none}]) of
|
||||||
|
nomatch ->
|
||||||
|
re:replace(Tag, "/", "-", [{return ,list}]);
|
||||||
|
_ ->
|
||||||
|
%% if it is a version number prefixed by 'v' or 'e', then remove it
|
||||||
|
re:replace(Tag, "[v|e]", "", [{return ,list}])
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
case os:getenv("EMQX_DEPS_DEFAULT_VSN") of
|
||||||
|
false -> CONFIG; % env var not defined
|
||||||
|
[] -> CONFIG; % env var set to empty string
|
||||||
|
Tag ->
|
||||||
|
[begin
|
||||||
|
AppConf0 = lists:keystore(vsn, 1, AppConf, {vsn, RemoveLeadingV(Tag)}),
|
||||||
|
{application, App, AppConf0}
|
||||||
|
end || Conf = {application, App, AppConf} <- CONFIG]
|
||||||
|
end.
|
||||||
|
|
|
@ -0,0 +1,146 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed 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.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_auth_jwt).
|
||||||
|
|
||||||
|
-include_lib("emqx/include/emqx.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
|
-logger_header("[JWT]").
|
||||||
|
|
||||||
|
-export([ register_metrics/0
|
||||||
|
, check/3
|
||||||
|
, description/0
|
||||||
|
]).
|
||||||
|
|
||||||
|
-record(auth_metrics, {
|
||||||
|
success = 'client.auth.success',
|
||||||
|
failure = 'client.auth.failure',
|
||||||
|
ignore = 'client.auth.ignore'
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(METRICS(Type), tl(tuple_to_list(#Type{}))).
|
||||||
|
-define(METRICS(Type, K), #Type{}#Type.K).
|
||||||
|
|
||||||
|
-define(AUTH_METRICS, ?METRICS(auth_metrics)).
|
||||||
|
-define(AUTH_METRICS(K), ?METRICS(auth_metrics, K)).
|
||||||
|
|
||||||
|
-spec(register_metrics() -> ok).
|
||||||
|
register_metrics() ->
|
||||||
|
lists:foreach(fun emqx_metrics:ensure/1, ?AUTH_METRICS).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Authentication callbacks
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
check(ClientInfo, AuthResult, Env = #{from := From, checklists := Checklists}) ->
|
||||||
|
case maps:find(From, ClientInfo) of
|
||||||
|
error ->
|
||||||
|
ok = emqx_metrics:inc(?AUTH_METRICS(ignore)),
|
||||||
|
{ok, AuthResult#{auth_result => token_undefined, anonymous => false}};
|
||||||
|
{ok, Token} ->
|
||||||
|
try jwerl:header(Token) of
|
||||||
|
Headers ->
|
||||||
|
case verify_token(Headers, Token, Env) of
|
||||||
|
{ok, Claims} ->
|
||||||
|
{stop, maps:merge(AuthResult, verify_claims(Checklists, Claims, ClientInfo))};
|
||||||
|
{error, Reason} ->
|
||||||
|
ok = emqx_metrics:inc(?AUTH_METRICS(failure)),
|
||||||
|
{stop, AuthResult#{auth_result => Reason, anonymous => false}}
|
||||||
|
end
|
||||||
|
catch
|
||||||
|
_Error:Reason ->
|
||||||
|
?LOG(error, "Check token error: ~p", [Reason]),
|
||||||
|
emqx_metrics:inc(?AUTH_METRICS(ignore))
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
description() -> "Authentication with JWT".
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Verify Token
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
verify_token(#{alg := <<"HS", _/binary>>}, _Token, #{secret := undefined}) ->
|
||||||
|
{error, hmac_secret_undefined};
|
||||||
|
verify_token(#{alg := Alg = <<"HS", _/binary>>}, Token, #{secret := Secret, opts := Opts}) ->
|
||||||
|
verify_token2(Alg, Token, Secret, Opts);
|
||||||
|
|
||||||
|
verify_token(#{alg := <<"RS", _/binary>>}, _Token, #{pubkey := undefined}) ->
|
||||||
|
{error, rsa_pubkey_undefined};
|
||||||
|
verify_token(#{alg := Alg = <<"RS", _/binary>>}, Token, #{pubkey := PubKey, opts := Opts}) ->
|
||||||
|
verify_token2(Alg, Token, PubKey, Opts);
|
||||||
|
|
||||||
|
verify_token(#{alg := <<"ES", _/binary>>}, _Token, #{pubkey := undefined}) ->
|
||||||
|
{error, ecdsa_pubkey_undefined};
|
||||||
|
verify_token(#{alg := Alg = <<"ES", _/binary>>}, Token, #{pubkey := PubKey, opts := Opts}) ->
|
||||||
|
verify_token2(Alg, Token, PubKey, Opts);
|
||||||
|
|
||||||
|
verify_token(Header, _Token, _Env) ->
|
||||||
|
?LOG(error, "Unsupported token algorithm: ~p", [Header]),
|
||||||
|
{error, token_unsupported}.
|
||||||
|
|
||||||
|
verify_token2(Alg, Token, SecretOrKey, Opts) ->
|
||||||
|
try jwerl:verify(Token, decode_algo(Alg), SecretOrKey, #{}, Opts) of
|
||||||
|
{ok, Claims} ->
|
||||||
|
{ok, Claims};
|
||||||
|
{error, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
catch
|
||||||
|
_Error:Reason ->
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
decode_algo(<<"HS256">>) -> hs256;
|
||||||
|
decode_algo(<<"HS384">>) -> hs384;
|
||||||
|
decode_algo(<<"HS512">>) -> hs512;
|
||||||
|
decode_algo(<<"RS256">>) -> rs256;
|
||||||
|
decode_algo(<<"RS384">>) -> rs384;
|
||||||
|
decode_algo(<<"RS512">>) -> rs512;
|
||||||
|
decode_algo(<<"ES256">>) -> es256;
|
||||||
|
decode_algo(<<"ES384">>) -> es384;
|
||||||
|
decode_algo(<<"ES512">>) -> es512;
|
||||||
|
decode_algo(<<"none">>) -> none;
|
||||||
|
decode_algo(Alg) -> throw({error, {unsupported_algorithm, Alg}}).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Verify Claims
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
verify_claims(Checklists, Claims, ClientInfo) ->
|
||||||
|
case do_verify_claims(feedvar(Checklists, ClientInfo), Claims) of
|
||||||
|
{error, Reason} ->
|
||||||
|
ok = emqx_metrics:inc(?AUTH_METRICS(failure)),
|
||||||
|
#{auth_result => Reason, anonymous => false};
|
||||||
|
ok ->
|
||||||
|
ok = emqx_metrics:inc(?AUTH_METRICS(success)),
|
||||||
|
#{auth_result => success, anonymous => false, jwt_claims => Claims}
|
||||||
|
end.
|
||||||
|
|
||||||
|
do_verify_claims([], _Claims) ->
|
||||||
|
ok;
|
||||||
|
do_verify_claims([{Key, Expected} | L], Claims) ->
|
||||||
|
case maps:get(Key, Claims, undefined) =:= Expected of
|
||||||
|
true -> do_verify_claims(L, Claims);
|
||||||
|
false -> {error, {verify_claim_failed, Key}}
|
||||||
|
end.
|
||||||
|
|
||||||
|
feedvar(Checklists, #{username := Username, clientid := ClientId}) ->
|
||||||
|
lists:map(fun({K, <<"%u">>}) -> {K, Username};
|
||||||
|
({K, <<"%c">>}) -> {K, ClientId};
|
||||||
|
({K, Expected}) -> {K, Expected}
|
||||||
|
end, Checklists).
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed 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.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_auth_jwt_app).
|
||||||
|
|
||||||
|
-behaviour(application).
|
||||||
|
|
||||||
|
-behaviour(supervisor).
|
||||||
|
|
||||||
|
-emqx_plugin(auth).
|
||||||
|
|
||||||
|
-export([start/2, stop/1]).
|
||||||
|
|
||||||
|
-export([init/1]).
|
||||||
|
|
||||||
|
-define(APP, emqx_auth_jwt).
|
||||||
|
|
||||||
|
-define(JWT_ACTION, {emqx_auth_jwt, check, [auth_env()]}).
|
||||||
|
|
||||||
|
start(_Type, _Args) ->
|
||||||
|
ok = emqx_auth_jwt:register_metrics(),
|
||||||
|
emqx:hook('client.authenticate', ?JWT_ACTION),
|
||||||
|
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||||
|
|
||||||
|
stop(_State) ->
|
||||||
|
emqx:unhook('client.authenticate', ?JWT_ACTION).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Dummy supervisor
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
init([]) ->
|
||||||
|
{ok, { {one_for_all, 1, 10}, []} }.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Internal functions
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
auth_env() ->
|
||||||
|
#{secret => env(secret, undefined),
|
||||||
|
from => env(from, password),
|
||||||
|
pubkey => read_pubkey(),
|
||||||
|
checklists => env(verify_claims, []),
|
||||||
|
opts => env(jwerl_opts, #{})
|
||||||
|
}.
|
||||||
|
|
||||||
|
read_pubkey() ->
|
||||||
|
case env(pubkey, undefined) of
|
||||||
|
undefined -> undefined;
|
||||||
|
Path ->
|
||||||
|
{ok, PubKey} = file:read_file(Path), PubKey
|
||||||
|
end.
|
||||||
|
|
||||||
|
env(Key, Default) ->
|
||||||
|
application:get_env(?APP, Key, Default).
|
||||||
|
|
|
@ -0,0 +1,137 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed 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.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_auth_jwt_SUITE).
|
||||||
|
|
||||||
|
-compile(nowarn_export_all).
|
||||||
|
-compile(export_all).
|
||||||
|
|
||||||
|
-include_lib("emqx/include/emqx.hrl").
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
-include_lib("common_test/include/ct.hrl").
|
||||||
|
|
||||||
|
-define(APP, emqx_auth_jwt).
|
||||||
|
|
||||||
|
all() ->
|
||||||
|
[{group, emqx_auth_jwt}].
|
||||||
|
|
||||||
|
groups() ->
|
||||||
|
[{emqx_auth_jwt, [sequence], [ t_check_auth
|
||||||
|
, t_check_claims
|
||||||
|
, t_check_claims_clientid
|
||||||
|
, t_check_claims_username
|
||||||
|
]}
|
||||||
|
].
|
||||||
|
|
||||||
|
init_per_suite(Config) ->
|
||||||
|
emqx_ct_helpers:start_apps([emqx, emqx_auth_jwt], fun set_special_configs/1),
|
||||||
|
Config.
|
||||||
|
|
||||||
|
end_per_suite(_Config) ->
|
||||||
|
emqx_ct_helpers:stop_apps([emqx_auth_jwt, emqx]).
|
||||||
|
|
||||||
|
set_special_configs(emqx) ->
|
||||||
|
application:set_env(emqx, allow_anonymous, false),
|
||||||
|
application:set_env(emqx, acl_nomatch, deny),
|
||||||
|
application:set_env(emqx, enable_acl_cache, false),
|
||||||
|
LoadedPluginPath = filename:join(["test", "emqx_SUITE_data", "loaded_plugins"]),
|
||||||
|
AclFilePath = filename:join(["test", "emqx_SUITE_data", "acl.conf"]),
|
||||||
|
application:set_env(emqx, plugins_loaded_file,
|
||||||
|
emqx_ct_helpers:deps_path(emqx, LoadedPluginPath)),
|
||||||
|
application:set_env(emqx, acl_file,
|
||||||
|
emqx_ct_helpers:deps_path(emqx, AclFilePath));
|
||||||
|
|
||||||
|
set_special_configs(emqx_auth_jwt) ->
|
||||||
|
application:set_env(emqx_auth_jwt, secret, "emqxsecret"),
|
||||||
|
application:set_env(emqx_auth_jwt, from, password);
|
||||||
|
|
||||||
|
set_special_configs(_) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% Testcases
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
t_check_auth(_) ->
|
||||||
|
Plain = #{clientid => <<"client1">>, username => <<"plain">>, zone => external},
|
||||||
|
Jwt = jwerl:sign([{clientid, <<"client1">>},
|
||||||
|
{username, <<"plain">>},
|
||||||
|
{exp, os:system_time(seconds) + 3}], hs256, <<"emqxsecret">>),
|
||||||
|
ct:pal("Jwt: ~p~n", [Jwt]),
|
||||||
|
|
||||||
|
Result0 = emqx_access_control:authenticate(Plain#{password => Jwt}),
|
||||||
|
ct:pal("Auth result: ~p~n", [Result0]),
|
||||||
|
?assertMatch({ok, #{auth_result := success, jwt_claims := #{clientid := <<"client1">>}}}, Result0),
|
||||||
|
|
||||||
|
ct:sleep(3100),
|
||||||
|
Result1 = emqx_access_control:authenticate(Plain#{password => Jwt}),
|
||||||
|
ct:pal("Auth result after 1000ms: ~p~n", [Result1]),
|
||||||
|
?assertMatch({error, _}, Result1),
|
||||||
|
|
||||||
|
Jwt_Error = jwerl:sign([{clientid, <<"client1">>},
|
||||||
|
{username, <<"plain">>}], hs256, <<"secret">>),
|
||||||
|
ct:pal("invalid jwt: ~p~n", [Jwt_Error]),
|
||||||
|
Result2 = emqx_access_control:authenticate(Plain#{password => Jwt_Error}),
|
||||||
|
ct:pal("Auth result for the invalid jwt: ~p~n", [Result2]),
|
||||||
|
?assertEqual({error, invalid_signature}, Result2),
|
||||||
|
?assertMatch({error, _}, emqx_access_control:authenticate(Plain#{password => <<"asd">>})).
|
||||||
|
|
||||||
|
t_check_claims(_) ->
|
||||||
|
application:set_env(emqx_auth_jwt, verify_claims, [{sub, <<"value">>}]),
|
||||||
|
Plain = #{clientid => <<"client1">>, username => <<"plain">>, zone => external},
|
||||||
|
Jwt = jwerl:sign([{clientid, <<"client1">>},
|
||||||
|
{username, <<"plain">>},
|
||||||
|
{sub, value},
|
||||||
|
{exp, os:system_time(seconds) + 3}], hs256, <<"emqxsecret">>),
|
||||||
|
Result0 = emqx_access_control:authenticate(Plain#{password => Jwt}),
|
||||||
|
ct:pal("Auth result: ~p~n", [Result0]),
|
||||||
|
?assertMatch({ok, #{auth_result := success, jwt_claims := _}}, Result0),
|
||||||
|
Jwt_Error = jwerl:sign([{clientid, <<"client1">>},
|
||||||
|
{username, <<"plain">>}], hs256, <<"secret">>),
|
||||||
|
Result2 = emqx_access_control:authenticate(Plain#{password => Jwt_Error}),
|
||||||
|
ct:pal("Auth result for the invalid jwt: ~p~n", [Result2]),
|
||||||
|
?assertEqual({error, invalid_signature}, Result2).
|
||||||
|
|
||||||
|
t_check_claims_clientid(_) ->
|
||||||
|
application:set_env(emqx_auth_jwt, verify_claims, [{clientid, <<"%c">>}]),
|
||||||
|
Plain = #{clientid => <<"client23">>, username => <<"plain">>, zone => external},
|
||||||
|
Jwt = jwerl:sign([{clientid, <<"client23">>},
|
||||||
|
{username, <<"plain">>},
|
||||||
|
{exp, os:system_time(seconds) + 3}], hs256, <<"emqxsecret">>),
|
||||||
|
Result0 = emqx_access_control:authenticate(Plain#{password => Jwt}),
|
||||||
|
ct:pal("Auth result: ~p~n", [Result0]),
|
||||||
|
?assertMatch({ok, #{auth_result := success, jwt_claims := _}}, Result0),
|
||||||
|
Jwt_Error = jwerl:sign([{clientid, <<"client1">>},
|
||||||
|
{username, <<"plain">>}], hs256, <<"secret">>),
|
||||||
|
Result2 = emqx_access_control:authenticate(Plain#{password => Jwt_Error}),
|
||||||
|
ct:pal("Auth result for the invalid jwt: ~p~n", [Result2]),
|
||||||
|
?assertEqual({error, invalid_signature}, Result2).
|
||||||
|
|
||||||
|
t_check_claims_username(_) ->
|
||||||
|
application:set_env(emqx_auth_jwt, verify_claims, [{username, <<"%u">>}]),
|
||||||
|
Plain = #{clientid => <<"client23">>, username => <<"plain">>, zone => external},
|
||||||
|
Jwt = jwerl:sign([{clientid, <<"client23">>},
|
||||||
|
{username, <<"plain">>},
|
||||||
|
{exp, os:system_time(seconds) + 3}], hs256, <<"emqxsecret">>),
|
||||||
|
Result0 = emqx_access_control:authenticate(Plain#{password => Jwt}),
|
||||||
|
ct:pal("Auth result: ~p~n", [Result0]),
|
||||||
|
?assertMatch({ok, #{auth_result := success, jwt_claims := _}}, Result0),
|
||||||
|
Jwt_Error = jwerl:sign([{clientid, <<"client1">>},
|
||||||
|
{username, <<"plain">>}], hs256, <<"secret">>),
|
||||||
|
Result3 = emqx_access_control:authenticate(Plain#{password => Jwt_Error}),
|
||||||
|
ct:pal("Auth result for the invalid jwt: ~p~n", [Result3]),
|
||||||
|
?assertEqual({error, invalid_signature}, Result3).
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
erlang:
|
||||||
|
image: erlang:22.1
|
||||||
|
volumes:
|
||||||
|
- ../:/emqx_auth_ldap
|
||||||
|
networks:
|
||||||
|
- emqx_bridge
|
||||||
|
depends_on:
|
||||||
|
- ldap_server
|
||||||
|
tty: true
|
||||||
|
|
||||||
|
ldap_server:
|
||||||
|
build: ./emqx-ldap
|
||||||
|
image: emqx-ldap:1.0
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- 389:389
|
||||||
|
- 636:636
|
||||||
|
networks:
|
||||||
|
- emqx_bridge
|
||||||
|
|
||||||
|
networks:
|
||||||
|
emqx_bridge:
|
||||||
|
driver: bridge
|
|
@ -0,0 +1,26 @@
|
||||||
|
FROM buildpack-deps:stretch
|
||||||
|
|
||||||
|
ENV VERSION=2.4.50
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y groff groff-base
|
||||||
|
RUN wget ftp://ftp.openldap.org/pub/OpenLDAP/openldap-release/openldap-${VERSION}.tgz \
|
||||||
|
&& gunzip -c openldap-${VERSION}.tgz | tar xvfB - \
|
||||||
|
&& cd openldap-${VERSION} \
|
||||||
|
&& ./configure && make depend && make && make install \
|
||||||
|
&& cd .. && rm -rf openldap-${VERSION}
|
||||||
|
|
||||||
|
COPY ./slapd.conf /usr/local/etc/openldap/slapd.conf
|
||||||
|
COPY ./emqx.io.ldif /usr/local/etc/openldap/schema/emqx.io.ldif
|
||||||
|
COPY ./emqx.schema /usr/local/etc/openldap/schema/emqx.schema
|
||||||
|
COPY ./*.pem /usr/local/etc/openldap/
|
||||||
|
|
||||||
|
RUN mkdir -p /usr/local/etc/openldap/data \
|
||||||
|
&& slapadd -l /usr/local/etc/openldap/schema/emqx.io.ldif -f /usr/local/etc/openldap/slapd.conf
|
||||||
|
|
||||||
|
WORKDIR /usr/local/etc/openldap
|
||||||
|
|
||||||
|
EXPOSE 389 636
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/local/libexec/slapd", "-h", "ldap:/// ldaps:///", "-d", "3", "-f", "/usr/local/etc/openldap/slapd.conf"]
|
||||||
|
|
||||||
|
CMD []
|
|
@ -0,0 +1,16 @@
|
||||||
|
include /usr/local/etc/openldap/schema/core.schema
|
||||||
|
include /usr/local/etc/openldap/schema/cosine.schema
|
||||||
|
include /usr/local/etc/openldap/schema/inetorgperson.schema
|
||||||
|
include /usr/local/etc/openldap/schema/ppolicy.schema
|
||||||
|
include /usr/local/etc/openldap/schema/emqx.schema
|
||||||
|
|
||||||
|
TLSCACertificateFile /usr/local/etc/openldap/cacert.pem
|
||||||
|
TLSCertificateFile /usr/local/etc/openldap/cert.pem
|
||||||
|
TLSCertificateKeyFile /usr/local/etc/openldap/key.pem
|
||||||
|
|
||||||
|
database bdb
|
||||||
|
suffix "dc=emqx,dc=io"
|
||||||
|
rootdn "cn=root,dc=emqx,dc=io"
|
||||||
|
rootpw {SSHA}eoF7NhNrejVYYyGHqnt+MdKNBh4r1w3W
|
||||||
|
|
||||||
|
directory /usr/local/etc/openldap/data
|
|
@ -0,0 +1,49 @@
|
||||||
|
name: Run test cases
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run_test_cases:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
network_type:
|
||||||
|
- ipv4
|
||||||
|
- ipv6
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: install docker-compose
|
||||||
|
run: |
|
||||||
|
sudo curl -L "https://github.com/docker/compose/releases/download/1.25.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||||
|
sudo chmod +x /usr/local/bin/docker-compose
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- name: prepare
|
||||||
|
env:
|
||||||
|
NETWORK_TYPE: ${{ matrix.network_type }}
|
||||||
|
run: |
|
||||||
|
set -e -x -u
|
||||||
|
cp ./emqx.io.ldif ./emqx.schema ./.ci/emqx-ldap
|
||||||
|
cp ./test/certs/* ./.ci/emqx-ldap
|
||||||
|
docker-compose -f ./.ci/docker-compose.yml -p tests build
|
||||||
|
if [ "$NETWORK_TYPE" = "ipv6" ];then docker network create --driver bridge --ipv6 --subnet fd15:555::/64 tests_emqx_bridge --attachable; fi
|
||||||
|
docker-compose -f ./.ci/docker-compose.yml -p tests up -d
|
||||||
|
if [ "$NETWORK_TYPE" != "ipv6" ];then
|
||||||
|
docker exec -i tests_erlang_1 sh -c "sed -i '/auth.ldap.servers/c auth.ldap.servers = ldap_server' ./emqx_auth_ldap/etc/emqx_auth_ldap.conf"
|
||||||
|
else
|
||||||
|
ipv6_address=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.GlobalIPv6Address}}{{end}}' $(docker ps -a -f name=tests_ldap_server_1 -q))
|
||||||
|
docker exec -i $(docker ps -a -f name=tests_erlang_1 -q) sh -c "sed -i '/auth.ldap.servers/c auth.ldap.servers = $ipv6_address' /emqx_auth_ldap/etc/emqx_auth_ldap.conf"
|
||||||
|
fi
|
||||||
|
- name: run test cases
|
||||||
|
run: |
|
||||||
|
set -e -x -u
|
||||||
|
docker exec -i tests_erlang_1 sh -c "make -C /emqx_auth_ldap xref"
|
||||||
|
docker exec -i tests_erlang_1 sh -c "make -C /emqx_auth_ldap eunit"
|
||||||
|
docker exec -i tests_erlang_1 sh -c "make -C /emqx_auth_ldap ct"
|
||||||
|
docker exec -i tests_erlang_1 sh -c "make -C /emqx_auth_ldap cover"
|
||||||
|
- uses: actions/upload-artifact@v1
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
name: logs_${{ matrix.network_type }}
|
||||||
|
path: _build/test/logs
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
.eunit
|
||||||
|
deps
|
||||||
|
*.o
|
||||||
|
*.beam
|
||||||
|
*.plt
|
||||||
|
erl_crash.dump
|
||||||
|
ebin
|
||||||
|
rel/example_project
|
||||||
|
.concrete/DEV_MODE
|
||||||
|
.rebar
|
||||||
|
.erlang.mk/
|
||||||
|
emqx_auth_ldap.d
|
||||||
|
data/
|
||||||
|
cover/
|
||||||
|
ct.coverdata
|
||||||
|
eunit.coverdata
|
||||||
|
logs/
|
||||||
|
test/ct.cover.spec
|
||||||
|
.DS_Store
|
||||||
|
_build/
|
||||||
|
rebar.lock
|
||||||
|
erlang.mk
|
||||||
|
rebar3.crashdump
|
||||||
|
.rebar3/
|
||||||
|
etc/emqx_auth_ldap.conf.rendered
|
|
@ -0,0 +1,201 @@
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright {yyyy} {name of copyright owner}
|
||||||
|
|
||||||
|
Licensed 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.
|
|
@ -0,0 +1,96 @@
|
||||||
|
emqx_auth_ldap
|
||||||
|
==============
|
||||||
|
|
||||||
|
EMQ X LDAP Authentication Plugin
|
||||||
|
|
||||||
|
Build
|
||||||
|
-----
|
||||||
|
|
||||||
|
```
|
||||||
|
make
|
||||||
|
```
|
||||||
|
|
||||||
|
Load the Plugin
|
||||||
|
---------------
|
||||||
|
|
||||||
|
```
|
||||||
|
# ./bin/emqx_ctl plugins load emqx_auth_ldap
|
||||||
|
```
|
||||||
|
|
||||||
|
Generate Password
|
||||||
|
---------------
|
||||||
|
|
||||||
|
```
|
||||||
|
slappasswd -h '{ssha}' -s public
|
||||||
|
```
|
||||||
|
|
||||||
|
Configuration Open LDAP
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
vim /etc/openldap/slapd.conf
|
||||||
|
|
||||||
|
```
|
||||||
|
include /etc/openldap/schema/core.schema
|
||||||
|
include /etc/openldap/schema/cosine.schema
|
||||||
|
include /etc/openldap/schema/inetorgperson.schema
|
||||||
|
include /etc/openldap/schema/ppolicy.schema
|
||||||
|
include /etc/openldap/schema/emqx.schema
|
||||||
|
|
||||||
|
database bdb
|
||||||
|
suffix "dc=emqx,dc=io"
|
||||||
|
rootdn "cn=root,dc=emqx,dc=io"
|
||||||
|
rootpw {SSHA}eoF7NhNrejVYYyGHqnt+MdKNBh4r1w3W
|
||||||
|
|
||||||
|
directory /etc/openldap/data
|
||||||
|
```
|
||||||
|
|
||||||
|
If the ldap launched with error below:
|
||||||
|
```
|
||||||
|
Unrecognized database type (bdb)
|
||||||
|
5c4a72b9 slapd.conf: line 7: <database> failed init (bdb)
|
||||||
|
slapadd: bad configuration file!
|
||||||
|
```
|
||||||
|
|
||||||
|
Insert lines to the slapd.conf
|
||||||
|
```
|
||||||
|
modulepath /usr/lib/ldap
|
||||||
|
moduleload back_bdb.la
|
||||||
|
```
|
||||||
|
|
||||||
|
Import EMQX User Data
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
Use ldapadd
|
||||||
|
```
|
||||||
|
# ldapadd -x -D "cn=root,dc=emqx,dc=io" -w public -f emqx.com.ldif
|
||||||
|
```
|
||||||
|
|
||||||
|
Use slapadd
|
||||||
|
```
|
||||||
|
# sudo slapadd -l schema/emqx.io.ldif -f slapd.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
Launch slapd
|
||||||
|
```
|
||||||
|
# sudo slapd -d 3
|
||||||
|
```
|
||||||
|
|
||||||
|
Test
|
||||||
|
-----
|
||||||
|
After configure slapd correctly and launch slapd successfully.
|
||||||
|
You could execute
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
# make tests
|
||||||
|
```
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
|
Apache License Version 2.0
|
||||||
|
|
||||||
|
Author
|
||||||
|
------
|
||||||
|
|
||||||
|
EMQ X Team.
|
||||||
|
|
|
@ -0,0 +1,135 @@
|
||||||
|
## create emqx.io
|
||||||
|
|
||||||
|
dn:dc=emqx,dc=io
|
||||||
|
objectclass: top
|
||||||
|
objectclass: dcobject
|
||||||
|
objectclass: organization
|
||||||
|
dc:emqx
|
||||||
|
o:emqx,Inc.
|
||||||
|
|
||||||
|
# create testdevice.emqx.io
|
||||||
|
dn:ou=testdevice,dc=emqx,dc=io
|
||||||
|
objectClass: top
|
||||||
|
objectclass:organizationalUnit
|
||||||
|
ou:testdevice
|
||||||
|
|
||||||
|
# create user admin
|
||||||
|
dn:uid=admin,ou=testdevice,dc=emqx,dc=io
|
||||||
|
objectClass: top
|
||||||
|
objectClass: simpleSecurityObject
|
||||||
|
objectClass: account
|
||||||
|
userPassword:: e1NIQX1XNnBoNU1tNVB6OEdnaVVMYlBnekczN21qOWc9
|
||||||
|
uid: admin
|
||||||
|
|
||||||
|
## create user=mqttuser0001,
|
||||||
|
# password=mqttuser0001,
|
||||||
|
# passhash={SHA}mlb3fat40MKBTXUVZwCKmL73R/0=
|
||||||
|
# base64passhash=e1NIQX1tbGIzZmF0NDBNS0JUWFVWWndDS21MNzNSLzA9
|
||||||
|
dn:uid=mqttuser0001,ou=testdevice,dc=emqx,dc=io
|
||||||
|
objectClass: top
|
||||||
|
objectClass: mqttUser
|
||||||
|
objectClass: mqttDevice
|
||||||
|
objectClass: mqttSecurity
|
||||||
|
uid: mqttuser0001
|
||||||
|
isEnabled: TRUE
|
||||||
|
mqttAccountName: user1
|
||||||
|
mqttPublishTopic: mqttuser0001/pub/1
|
||||||
|
mqttPublishTopic: mqttuser0001/pub/+
|
||||||
|
mqttPublishTopic: mqttuser0001/pub/#
|
||||||
|
mqttSubscriptionTopic: mqttuser0001/sub/1
|
||||||
|
mqttSubscriptionTopic: mqttuser0001/sub/+
|
||||||
|
mqttSubscriptionTopic: mqttuser0001/sub/#
|
||||||
|
mqttPubSubTopic: mqttuser0001/pubsub/1
|
||||||
|
mqttPubSubTopic: mqttuser0001/pubsub/+
|
||||||
|
mqttPubSubTopic: mqttuser0001/pubsub/#
|
||||||
|
userPassword:: e1NIQX1tbGIzZmF0NDBNS0JUWFVWWndDS21MNzNSLzA9
|
||||||
|
|
||||||
|
## create user=mqttuser0002
|
||||||
|
# password=mqttuser0002,
|
||||||
|
# passhash={SSHA}n9XdtoG4Q/TQ3TQF4Y+khJbMBH4qXj4M
|
||||||
|
# base64passhash=e1NTSEF9bjlYZHRvRzRRL1RRM1RRRjRZK2toSmJNQkg0cVhqNE0=
|
||||||
|
dn:uid=mqttuser0002,ou=testdevice,dc=emqx,dc=io
|
||||||
|
objectClass: top
|
||||||
|
objectClass: mqttUser
|
||||||
|
objectClass: mqttDevice
|
||||||
|
objectClass: mqttSecurity
|
||||||
|
uid: mqttuser0002
|
||||||
|
isEnabled: TRUE
|
||||||
|
mqttAccountName: user2
|
||||||
|
mqttPublishTopic: mqttuser0002/pub/1
|
||||||
|
mqttPublishTopic: mqttuser0002/pub/+
|
||||||
|
mqttPublishTopic: mqttuser0002/pub/#
|
||||||
|
mqttSubscriptionTopic: mqttuser0002/sub/1
|
||||||
|
mqttSubscriptionTopic: mqttuser0002/sub/+
|
||||||
|
mqttSubscriptionTopic: mqttuser0002/sub/#
|
||||||
|
mqttPubSubTopic: mqttuser0002/pubsub/1
|
||||||
|
mqttPubSubTopic: mqttuser0002/pubsub/+
|
||||||
|
mqttPubSubTopic: mqttuser0002/pubsub/#
|
||||||
|
userPassword:: e1NTSEF9bjlYZHRvRzRRL1RRM1RRRjRZK2toSmJNQkg0cVhqNE0=
|
||||||
|
|
||||||
|
## create user mqttuser0003
|
||||||
|
# password=mqttuser0003,
|
||||||
|
# passhash={MD5}ybsPGoaK3nDyiQvveiCOIw==
|
||||||
|
# base64passhash=e01ENX15YnNQR29hSzNuRHlpUXZ2ZWlDT0l3PT0=
|
||||||
|
dn:uid=mqttuser0003,ou=testdevice,dc=emqx,dc=io
|
||||||
|
objectClass: top
|
||||||
|
objectClass: mqttUser
|
||||||
|
objectClass: mqttDevice
|
||||||
|
objectClass: mqttSecurity
|
||||||
|
uid: mqttuser0003
|
||||||
|
isEnabled: TRUE
|
||||||
|
mqttPublishTopic: mqttuser0003/pub/1
|
||||||
|
mqttPublishTopic: mqttuser0003/pub/+
|
||||||
|
mqttPublishTopic: mqttuser0003/pub/#
|
||||||
|
mqttSubscriptionTopic: mqttuser0003/sub/1
|
||||||
|
mqttSubscriptionTopic: mqttuser0003/sub/+
|
||||||
|
mqttSubscriptionTopic: mqttuser0003/sub/#
|
||||||
|
mqttPubSubTopic: mqttuser0003/pubsub/1
|
||||||
|
mqttPubSubTopic: mqttuser0003/pubsub/+
|
||||||
|
mqttPubSubTopic: mqttuser0003/pubsub/#
|
||||||
|
userPassword:: e01ENX15YnNQR29hSzNuRHlpUXZ2ZWlDT0l3PT0=
|
||||||
|
|
||||||
|
## create user mqttuser0004
|
||||||
|
# password=mqttuser0004,
|
||||||
|
# passhash={MD5}2Br6pPDSEDIEvUlu9+s+MA==
|
||||||
|
# base64passhash=e01ENX0yQnI2cFBEU0VESUV2VWx1OStzK01BPT0=
|
||||||
|
dn:uid=mqttuser0004,ou=testdevice,dc=emqx,dc=io
|
||||||
|
objectClass: top
|
||||||
|
objectClass: mqttUser
|
||||||
|
objectClass: mqttDevice
|
||||||
|
objectClass: mqttSecurity
|
||||||
|
uid: mqttuser0004
|
||||||
|
isEnabled: TRUE
|
||||||
|
mqttPublishTopic: mqttuser0004/pub/1
|
||||||
|
mqttPublishTopic: mqttuser0004/pub/+
|
||||||
|
mqttPublishTopic: mqttuser0004/pub/#
|
||||||
|
mqttSubscriptionTopic: mqttuser0004/sub/1
|
||||||
|
mqttSubscriptionTopic: mqttuser0004/sub/+
|
||||||
|
mqttSubscriptionTopic: mqttuser0004/sub/#
|
||||||
|
mqttPubSubTopic: mqttuser0004/pubsub/1
|
||||||
|
mqttPubSubTopic: mqttuser0004/pubsub/+
|
||||||
|
mqttPubSubTopic: mqttuser0004/pubsub/#
|
||||||
|
userPassword: {MD5}2Br6pPDSEDIEvUlu9+s+MA==
|
||||||
|
|
||||||
|
## create user mqttuser0005
|
||||||
|
# password=mqttuser0005,
|
||||||
|
# passhash={SHA}jKnxeEDGR14kE8AR7yuVFOelhz4=
|
||||||
|
# base64passhash=e1NIQX1qS254ZUVER1IxNGtFOEFSN3l1VkZPZWxoejQ9
|
||||||
|
objectClass: top
|
||||||
|
dn:uid=mqttuser0005,ou=testdevice,dc=emqx,dc=io
|
||||||
|
objectClass: mqttUser
|
||||||
|
objectClass: mqttDevice
|
||||||
|
objectClass: mqttSecurity
|
||||||
|
uid: mqttuser0005
|
||||||
|
isEnabled: TRUE
|
||||||
|
mqttPublishTopic: mqttuser0005/pub/1
|
||||||
|
mqttPublishTopic: mqttuser0005/pub/+
|
||||||
|
mqttPublishTopic: mqttuser0005/pub/#
|
||||||
|
mqttSubscriptionTopic: mqttuser0005/sub/1
|
||||||
|
mqttSubscriptionTopic: mqttuser0005/sub/+
|
||||||
|
mqttSubscriptionTopic: mqttuser0005/sub/#
|
||||||
|
mqttPubSubTopic: mqttuser0005/pubsub/1
|
||||||
|
mqttPubSubTopic: mqttuser0005/pubsub/+
|
||||||
|
mqttPubSubTopic: mqttuser0005/pubsub/#
|
||||||
|
userPassword: {SHA}jKnxeEDGR14kE8AR7yuVFOelhz4=
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
#
|
||||||
|
# Preliminary Apple OS X Native LDAP Schema
|
||||||
|
# This file is subject to change.
|
||||||
|
#
|
||||||
|
attributetype ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.1.3 NAME 'isEnabled'
|
||||||
|
EQUALITY booleanMatch
|
||||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.7
|
||||||
|
SINGLE-VALUE
|
||||||
|
USAGE userApplications )
|
||||||
|
|
||||||
|
attributetype ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.4.1 NAME ( 'mqttPublishTopic' 'mpt' )
|
||||||
|
EQUALITY caseIgnoreMatch
|
||||||
|
SUBSTR caseIgnoreSubstringsMatch
|
||||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
|
||||||
|
USAGE userApplications )
|
||||||
|
attributetype ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.4.2 NAME ( 'mqttSubscriptionTopic' 'mst' )
|
||||||
|
EQUALITY caseIgnoreMatch
|
||||||
|
SUBSTR caseIgnoreSubstringsMatch
|
||||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
|
||||||
|
USAGE userApplications )
|
||||||
|
attributetype ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.4.3 NAME ( 'mqttPubSubTopic' 'mpst' )
|
||||||
|
EQUALITY caseIgnoreMatch
|
||||||
|
SUBSTR caseIgnoreSubstringsMatch
|
||||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
|
||||||
|
USAGE userApplications )
|
||||||
|
attributetype ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.4.4 NAME ( 'mqttAccountName' 'man' )
|
||||||
|
EQUALITY caseIgnoreMatch
|
||||||
|
SUBSTR caseIgnoreSubstringsMatch
|
||||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
|
||||||
|
USAGE userApplications )
|
||||||
|
|
||||||
|
|
||||||
|
objectclass ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.4 NAME 'mqttUser'
|
||||||
|
AUXILIARY
|
||||||
|
MAY ( mqttPublishTopic $ mqttSubscriptionTopic $ mqttPubSubTopic $ mqttAccountName) )
|
||||||
|
|
||||||
|
objectclass ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.2 NAME 'mqttDevice'
|
||||||
|
SUP top
|
||||||
|
STRUCTURAL
|
||||||
|
MUST ( uid )
|
||||||
|
MAY ( isEnabled ) )
|
||||||
|
|
||||||
|
objectclass ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.3 NAME 'mqttSecurity'
|
||||||
|
SUP top
|
||||||
|
AUXILIARY
|
||||||
|
MAY ( userPassword $ userPKCS12 $ pwdAttribute $ pwdLockout ) )
|
|
@ -0,0 +1,78 @@
|
||||||
|
##--------------------------------------------------------------------
|
||||||
|
## LDAP Auth Plugin
|
||||||
|
##--------------------------------------------------------------------
|
||||||
|
|
||||||
|
## LDAP server list, seperated by ','.
|
||||||
|
##
|
||||||
|
## Value: String
|
||||||
|
auth.ldap.servers = 127.0.0.1
|
||||||
|
|
||||||
|
## LDAP server port.
|
||||||
|
##
|
||||||
|
## Value: Port
|
||||||
|
auth.ldap.port = 389
|
||||||
|
|
||||||
|
## LDAP pool size
|
||||||
|
##
|
||||||
|
## Value: String
|
||||||
|
auth.ldap.pool = 8
|
||||||
|
|
||||||
|
## LDAP Bind DN.
|
||||||
|
##
|
||||||
|
## Value: DN
|
||||||
|
auth.ldap.bind_dn = cn=root,dc=emqx,dc=io
|
||||||
|
|
||||||
|
## LDAP Bind Password.
|
||||||
|
##
|
||||||
|
## Value: String
|
||||||
|
auth.ldap.bind_password = public
|
||||||
|
|
||||||
|
## LDAP query timeout.
|
||||||
|
##
|
||||||
|
## Value: Number
|
||||||
|
auth.ldap.timeout = 30s
|
||||||
|
|
||||||
|
## Device DN.
|
||||||
|
##
|
||||||
|
## Variables:
|
||||||
|
##
|
||||||
|
## Value: DN
|
||||||
|
auth.ldap.device_dn = ou=device,dc=emqx,dc=io
|
||||||
|
|
||||||
|
## Specified ObjectClass
|
||||||
|
##
|
||||||
|
## Variables:
|
||||||
|
##
|
||||||
|
## Value: string
|
||||||
|
auth.ldap.match_objectclass = mqttUser
|
||||||
|
|
||||||
|
## attributetype for username
|
||||||
|
##
|
||||||
|
## Variables:
|
||||||
|
##
|
||||||
|
## Value: string
|
||||||
|
auth.ldap.username.attributetype = uid
|
||||||
|
|
||||||
|
## attributetype for password
|
||||||
|
##
|
||||||
|
## Variables:
|
||||||
|
##
|
||||||
|
## Value: string
|
||||||
|
auth.ldap.password.attributetype = userPassword
|
||||||
|
|
||||||
|
## Whether to enable SSL.
|
||||||
|
##
|
||||||
|
## Value: true | false
|
||||||
|
auth.ldap.ssl = false
|
||||||
|
|
||||||
|
#auth.ldap.ssl.certfile = etc/certs/cert.pem
|
||||||
|
|
||||||
|
#auth.ldap.ssl.keyfile = etc/certs/key.pem
|
||||||
|
|
||||||
|
#auth.ldap.ssl.cacertfile = etc/certs/cacert.pem
|
||||||
|
|
||||||
|
#auth.ldap.ssl.verify = verify_peer
|
||||||
|
|
||||||
|
#auth.ldap.ssl.fail_if_no_peer_cert = true
|
||||||
|
|
||||||
|
#auth.ldap.ssl.server_name_indication = your_server_name
|
|
@ -0,0 +1,23 @@
|
||||||
|
|
||||||
|
-define(APP, emqx_auth_ldap).
|
||||||
|
|
||||||
|
-record(auth_metrics, {
|
||||||
|
success = 'client.auth.success',
|
||||||
|
failure = 'client.auth.failure',
|
||||||
|
ignore = 'client.auth.ignore'
|
||||||
|
}).
|
||||||
|
|
||||||
|
-record(acl_metrics, {
|
||||||
|
allow = 'client.acl.allow',
|
||||||
|
deny = 'client.acl.deny',
|
||||||
|
ignore = 'client.acl.ignore'
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(METRICS(Type), tl(tuple_to_list(#Type{}))).
|
||||||
|
-define(METRICS(Type, K), #Type{}#Type.K).
|
||||||
|
|
||||||
|
-define(AUTH_METRICS, ?METRICS(auth_metrics)).
|
||||||
|
-define(AUTH_METRICS(K), ?METRICS(auth_metrics, K)).
|
||||||
|
|
||||||
|
-define(ACL_METRICS, ?METRICS(acl_metrics)).
|
||||||
|
-define(ACL_METRICS(K), ?METRICS(acl_metrics, K)).
|
|
@ -0,0 +1,176 @@
|
||||||
|
%%-*- mode: erlang -*-
|
||||||
|
%% emqx_auth_ldap config mapping
|
||||||
|
|
||||||
|
{mapping, "auth.ldap.servers", "emqx_auth_ldap.ldap", [
|
||||||
|
{default, "127.0.0.1"},
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.ldap.port", "emqx_auth_ldap.ldap", [
|
||||||
|
{default, 389},
|
||||||
|
{datatype, integer}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.ldap.pool", "emqx_auth_ldap.ldap", [
|
||||||
|
{default, 8},
|
||||||
|
{datatype, integer}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.ldap.bind_dn", "emqx_auth_ldap.ldap", [
|
||||||
|
{datatype, string},
|
||||||
|
{default, "cn=root,dc=emqx,dc=io"}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.ldap.bind_password", "emqx_auth_ldap.ldap", [
|
||||||
|
{datatype, string},
|
||||||
|
{default, "public"}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.ldap.timeout", "emqx_auth_ldap.ldap", [
|
||||||
|
{default, "30s"},
|
||||||
|
{datatype, {duration, ms}}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.ldap.ssl", "emqx_auth_ldap.ldap", [
|
||||||
|
{default, false},
|
||||||
|
{datatype, {enum, [true, false]}}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.ldap.ssl.certfile", "emqx_auth_ldap.ldap", [
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.ldap.ssl.keyfile", "emqx_auth_ldap.ldap", [
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.ldap.ssl.cacertfile", "emqx_auth_ldap.ldap", [
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.ldap.ssl.verify", "emqx_auth_ldap.ldap", [
|
||||||
|
{default, verify_none},
|
||||||
|
{datatype, {enum, [verify_none, verify_peer]}}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.ldap.ssl.fail_if_no_peer_cert", "emqx_auth_ldap.ldap", [
|
||||||
|
{datatype, {enum, [true, false]}}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.ldap.ssl.server_name_indication", "emqx_auth_ldap.ldap", [
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{translation, "emqx_auth_ldap.ldap", fun(Conf) ->
|
||||||
|
A2N = fun(A) -> case inet:parse_address(A) of {ok, N} -> N; _ -> A end end,
|
||||||
|
Servers = [A2N(A) || A <- string:tokens(cuttlefish:conf_get("auth.ldap.servers", Conf), ",")],
|
||||||
|
Port = cuttlefish:conf_get("auth.ldap.port", Conf),
|
||||||
|
Pool = cuttlefish:conf_get("auth.ldap.pool", Conf),
|
||||||
|
BindDN = cuttlefish:conf_get("auth.ldap.bind_dn", Conf),
|
||||||
|
BindPassword = cuttlefish:conf_get("auth.ldap.bind_password", Conf),
|
||||||
|
Timeout = cuttlefish:conf_get("auth.ldap.timeout", Conf),
|
||||||
|
Filter = fun(Ls) -> [E || E = {_, V} <- Ls, V /= undefined]end,
|
||||||
|
SslOpts = fun() ->
|
||||||
|
[{certfile, cuttlefish:conf_get("auth.ldap.ssl.certfile", Conf)},
|
||||||
|
{keyfile, cuttlefish:conf_get("auth.ldap.ssl.keyfile", Conf)},
|
||||||
|
{cacertfile, cuttlefish:conf_get("auth.ldap.ssl.cacertfile", Conf, undefined)},
|
||||||
|
{verify, cuttlefish:conf_get("auth.ldap.ssl.verify", Conf, undefined)},
|
||||||
|
{server_name_indication, cuttlefish:conf_get("auth.ldap.ssl.server_name_indication", Conf, disable)},
|
||||||
|
{fail_if_no_peer_cert, cuttlefish:conf_get("auth.ldap.ssl.fail_if_no_peer_cert", Conf, undefined)}]
|
||||||
|
end,
|
||||||
|
Opts = [{servers, Servers},
|
||||||
|
{port, Port},
|
||||||
|
{timeout, Timeout},
|
||||||
|
{bind_dn, BindDN},
|
||||||
|
{bind_password, BindPassword},
|
||||||
|
{pool, Pool},
|
||||||
|
{auto_reconnect, 2}],
|
||||||
|
case cuttlefish:conf_get("auth.ldap.ssl", Conf) of
|
||||||
|
true -> [{ssl, true}, {sslopts, Filter(SslOpts())}|Opts];
|
||||||
|
false -> [{ssl, false}|Opts]
|
||||||
|
end
|
||||||
|
end}.
|
||||||
|
|
||||||
|
{mapping, "auth.ldap.device_dn", "emqx_auth_ldap.device_dn", [
|
||||||
|
{default, "ou=device,dc=emqx,dc=io"},
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.ldap.match_objectclass", "emqx_auth_ldap.match_objectclass", [
|
||||||
|
{default, "mqttUser"},
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.ldap.custom_base_dn", "emqx_auth_ldap.custom_base_dn", [
|
||||||
|
{default, "${username_attr}=${user},${device_dn}"},
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
%% auth.ldap.filters.1.key = "objectClass"
|
||||||
|
%% auth.ldap.filters.1.value = "mqttUser"
|
||||||
|
%% auth.ldap.filters.1.op = "and"
|
||||||
|
%% auth.ldap.filters.2.key = "uiAttr"
|
||||||
|
%% auth.ldap.filters.2.value "someAttr"
|
||||||
|
%% auth.ldap.filters.2.op = "or"
|
||||||
|
%% auth.ldap.filters.3.key = "someKey"
|
||||||
|
%% auth.ldap.filters.3.value = "someValue"
|
||||||
|
%% The configuratation structure sent to the application:
|
||||||
|
%% [{"objectClass","mqttUser"},"and",{"uiAttr","someAttr"},"or",{"someKey","someAttr"}]
|
||||||
|
%% The resulting LDAP filter would look like this:
|
||||||
|
%% ==> "|(&(objectClass=Class)(uiAttr=someAttr)(someKey=someValue))"
|
||||||
|
{translation, "emqx_auth_ldap.filters",
|
||||||
|
fun(Conf) ->
|
||||||
|
Settings = cuttlefish_variable:filter_by_prefix("auth.ldap.filters", Conf),
|
||||||
|
Keys = [{Num, {key, V}} || {["auth","ldap","filters", Num, "key"], V} <- Settings],
|
||||||
|
Values = [{Num, {value, V}} || {["auth","ldap","filters", Num, "value"], V} <- Settings],
|
||||||
|
Ops = [{Num, {op, V}} || {["auth","ldap","filters", Num, "op"], V} <- Settings],
|
||||||
|
RawFilters = Keys ++ Values ++ Ops,
|
||||||
|
Filters =
|
||||||
|
lists:foldl(
|
||||||
|
fun({Num,{T,V}}, Acc)->
|
||||||
|
maps:update_with(Num,
|
||||||
|
fun(F)->
|
||||||
|
maps:put(T,V,F)
|
||||||
|
end,
|
||||||
|
#{T=>V}, Acc)
|
||||||
|
end, #{}, RawFilters),
|
||||||
|
Order=lists:usort(maps:keys(Filters)),
|
||||||
|
lists:reverse(
|
||||||
|
lists:foldl(
|
||||||
|
fun(F,Acc)->
|
||||||
|
case F of
|
||||||
|
#{key:=K, op:=Op, value:=V} -> [Op,{K,V}|Acc];
|
||||||
|
#{key:=K, value:=V} -> [{K,V}|Acc]
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
[],
|
||||||
|
lists:map(fun(K) -> maps:get(K, Filters) end, Order)))
|
||||||
|
end}.
|
||||||
|
|
||||||
|
{mapping, "auth.ldap.filters.$num.key", "emqx_auth_ldap.filters", [
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.ldap.filters.$num.value", "emqx_auth_ldap.filters", [
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.ldap.filters.$num.op", "emqx_auth_ldap.filters", [
|
||||||
|
{datatype, {enum, [ "or", "and" ] } }
|
||||||
|
]}.
|
||||||
|
|
||||||
|
|
||||||
|
{mapping, "auth.ldap.bind_as_user", "emqx_auth_ldap.bind_as_user", [
|
||||||
|
{default, false},
|
||||||
|
{datatype, {enum, [true, false]}}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.ldap.username.attributetype", "emqx_auth_ldap.username_attr", [
|
||||||
|
{default, "uid"},
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.ldap.password.attributetype", "emqx_auth_ldap.password_attr", [
|
||||||
|
{default, "userPassword"},
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
|
@ -0,0 +1,27 @@
|
||||||
|
{deps,
|
||||||
|
[{eldap2, {git, "https://github.com/emqx/eldap2", {tag, "v0.2.2"}}},
|
||||||
|
{ecpool, {git, "https://github.com/emqx/ecpool", {tag, "v0.4.2"}}},
|
||||||
|
{emqx_passwd, {git, "https://github.com/emqx/emqx-passwd", {tag, "v1.1.1"}}}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{profiles,
|
||||||
|
[{test,
|
||||||
|
[{deps, [{emqx_ct_helpers, {git, "https://github.com/emqx/emqx-ct-helpers", {tag, "1.2.2"}}}]}
|
||||||
|
]}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{edoc_opts, [{preprocess, true}]}.
|
||||||
|
{erl_opts, [warn_unused_vars,
|
||||||
|
warn_shadow_vars,
|
||||||
|
warn_unused_import,
|
||||||
|
warn_obsolete_guard,
|
||||||
|
debug_info,
|
||||||
|
{parse_transform}]}.
|
||||||
|
|
||||||
|
{xref_checks, [undefined_function_calls, undefined_functions,
|
||||||
|
locals_not_used, deprecated_function_calls,
|
||||||
|
warnings_as_errors, deprecated_functions]}.
|
||||||
|
{cover_enabled, true}.
|
||||||
|
{cover_opts, [verbose]}.
|
||||||
|
{cover_export_enabled, true}.
|
||||||
|
|
|
@ -0,0 +1,98 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed 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.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_acl_ldap).
|
||||||
|
|
||||||
|
-include("emqx_auth_ldap.hrl").
|
||||||
|
|
||||||
|
-include_lib("emqx/include/emqx.hrl").
|
||||||
|
-include_lib("eldap/include/eldap.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
|
-export([ register_metrics/0
|
||||||
|
, check_acl/5
|
||||||
|
, description/0
|
||||||
|
]).
|
||||||
|
|
||||||
|
-import(proplists, [get_value/2]).
|
||||||
|
|
||||||
|
-import(emqx_auth_ldap_cli, [search/4]).
|
||||||
|
|
||||||
|
-spec(register_metrics() -> ok).
|
||||||
|
register_metrics() ->
|
||||||
|
lists:foreach(fun emqx_metrics:ensure/1, ?ACL_METRICS).
|
||||||
|
|
||||||
|
check_acl(ClientInfo, PubSub, Topic, NoMatchAction, State) ->
|
||||||
|
case do_check_acl(ClientInfo, PubSub, Topic, NoMatchAction, State) of
|
||||||
|
ok -> emqx_metrics:inc(?ACL_METRICS(ignore)), ok;
|
||||||
|
{stop, allow} -> emqx_metrics:inc(?ACL_METRICS(allow)), {stop, allow};
|
||||||
|
{stop, deny} -> emqx_metrics:inc(?ACL_METRICS(deny)), {stop, deny}
|
||||||
|
end.
|
||||||
|
|
||||||
|
do_check_acl(#{username := <<$$, _/binary>>}, _PubSub, _Topic, _NoMatchAction, _State) ->
|
||||||
|
ok;
|
||||||
|
|
||||||
|
do_check_acl(#{username := Username}, PubSub, Topic, _NoMatchAction,
|
||||||
|
#{device_dn := DeviceDn,
|
||||||
|
match_objectclass := ObjectClass,
|
||||||
|
username_attr := UidAttr,
|
||||||
|
custom_base_dn := CustomBaseDN,
|
||||||
|
pool := Pool} = Config) ->
|
||||||
|
|
||||||
|
Filters = maps:get(filters, Config, []),
|
||||||
|
|
||||||
|
ReplaceRules = [{"${username_attr}", UidAttr},
|
||||||
|
{"${user}", binary_to_list(Username)},
|
||||||
|
{"${device_dn}", DeviceDn}],
|
||||||
|
|
||||||
|
Filter = emqx_auth_ldap:prepare_filter(Filters, UidAttr, ObjectClass, ReplaceRules),
|
||||||
|
|
||||||
|
Attribute = case PubSub of
|
||||||
|
publish -> "mqttPublishTopic";
|
||||||
|
subscribe -> "mqttSubscriptionTopic"
|
||||||
|
end,
|
||||||
|
Attribute1 = "mqttPubSubTopic",
|
||||||
|
?LOG(debug, "[LDAP] search dn:~p filter:~p, attribute:~p",
|
||||||
|
[DeviceDn, Filter, Attribute]),
|
||||||
|
|
||||||
|
BaseDN = emqx_auth_ldap:replace_vars(CustomBaseDN, ReplaceRules),
|
||||||
|
|
||||||
|
case search(Pool, BaseDN, Filter, [Attribute, Attribute1]) of
|
||||||
|
{error, noSuchObject} ->
|
||||||
|
ok;
|
||||||
|
{ok, #eldap_search_result{entries = []}} ->
|
||||||
|
ok;
|
||||||
|
{ok, #eldap_search_result{entries = [Entry]}} ->
|
||||||
|
Topics = get_value(Attribute, Entry#eldap_entry.attributes)
|
||||||
|
++ get_value(Attribute1, Entry#eldap_entry.attributes),
|
||||||
|
match(Topic, Topics);
|
||||||
|
Error ->
|
||||||
|
?LOG(error, "[LDAP] search error:~p", [Error]),
|
||||||
|
{stop, deny}
|
||||||
|
end.
|
||||||
|
|
||||||
|
match(_Topic, []) ->
|
||||||
|
ok;
|
||||||
|
|
||||||
|
match(Topic, [Filter | Topics]) ->
|
||||||
|
case emqx_topic:match(Topic, list_to_binary(Filter)) of
|
||||||
|
true -> {stop, allow};
|
||||||
|
false -> match(Topic, Topics)
|
||||||
|
end.
|
||||||
|
|
||||||
|
description() ->
|
||||||
|
"ACL with LDAP".
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
{application, emqx_auth_ldap,
|
||||||
|
[{description, "EMQ X Authentication/ACL with LDAP"},
|
||||||
|
{vsn, "git"},
|
||||||
|
{modules, []},
|
||||||
|
{registered, [emqx_auth_ldap_sup]},
|
||||||
|
{applications, [kernel,stdlib,eldap2,ecpool,emqx_passwd]},
|
||||||
|
{mod, {emqx_auth_ldap_app,[]}},
|
||||||
|
{env, []},
|
||||||
|
{licenses, ["Apache-2.0"]},
|
||||||
|
{maintainers, ["EMQ X Team <contact@emqx.io>"]},
|
||||||
|
{links, [{"Homepage", "https://emqx.io/"},
|
||||||
|
{"Github", "https://github.com/emqx/emqx-auth-ldap"}
|
||||||
|
]}
|
||||||
|
]}.
|
|
@ -0,0 +1,24 @@
|
||||||
|
%%-*- mode: erlang -*-
|
||||||
|
%% .app.src.script
|
||||||
|
|
||||||
|
RemoveLeadingV =
|
||||||
|
fun(Tag) ->
|
||||||
|
case re:run(Tag, "^[v|e]?[0-9]\.[0-9]\.([0-9]|(rc|beta|alpha)\.[0-9])", [{capture, none}]) of
|
||||||
|
nomatch ->
|
||||||
|
re:replace(Tag, "/", "-", [{return ,list}]);
|
||||||
|
_ ->
|
||||||
|
%% if it is a version number prefixed by 'v' or 'e', then remove it
|
||||||
|
re:replace(Tag, "[v|e]", "", [{return ,list}])
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
case os:getenv("EMQX_DEPS_DEFAULT_VSN") of
|
||||||
|
false -> CONFIG; % env var not defined
|
||||||
|
[] -> CONFIG; % env var set to empty string
|
||||||
|
Tag ->
|
||||||
|
[begin
|
||||||
|
AppConf0 = lists:keystore(vsn, 1, AppConf, {vsn, RemoveLeadingV(Tag)}),
|
||||||
|
{application, App, AppConf0}
|
||||||
|
end || Conf = {application, App, AppConf} <- CONFIG]
|
||||||
|
end.
|
||||||
|
|
|
@ -0,0 +1,210 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed 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.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_auth_ldap).
|
||||||
|
|
||||||
|
-include("emqx_auth_ldap.hrl").
|
||||||
|
|
||||||
|
-include_lib("emqx/include/emqx.hrl").
|
||||||
|
-include_lib("eldap/include/eldap.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
|
-import(proplists, [get_value/2]).
|
||||||
|
|
||||||
|
-import(emqx_auth_ldap_cli, [search/3]).
|
||||||
|
|
||||||
|
-export([ register_metrics/0
|
||||||
|
, check/3
|
||||||
|
, description/0
|
||||||
|
, prepare_filter/4
|
||||||
|
, replace_vars/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
-spec(register_metrics() -> ok).
|
||||||
|
register_metrics() ->
|
||||||
|
lists:foreach(fun emqx_metrics:ensure/1, ?AUTH_METRICS).
|
||||||
|
|
||||||
|
check(ClientInfo = #{username := Username, password := Password}, AuthResult,
|
||||||
|
State = #{password_attr := PasswdAttr, bind_as_user := BindAsUserRequired, pool := Pool}) ->
|
||||||
|
CheckResult =
|
||||||
|
case lookup_user(Username, State) of
|
||||||
|
undefined -> {error, not_found};
|
||||||
|
{error, Error} -> {error, Error};
|
||||||
|
Entry ->
|
||||||
|
PasswordString = binary_to_list(Password),
|
||||||
|
ObjectName = Entry#eldap_entry.object_name,
|
||||||
|
Attributes = Entry#eldap_entry.attributes,
|
||||||
|
case BindAsUserRequired of
|
||||||
|
true ->
|
||||||
|
emqx_auth_ldap_cli:post_bind(Pool, ObjectName, PasswordString);
|
||||||
|
false ->
|
||||||
|
case get_value(PasswdAttr, Attributes) of
|
||||||
|
undefined ->
|
||||||
|
logger:error("LDAP Search State: ~p, uid: ~p, result:~p",
|
||||||
|
[State, Username, Attributes]),
|
||||||
|
{error, not_found};
|
||||||
|
[Passhash1] ->
|
||||||
|
format_password(Passhash1, Password, ClientInfo)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
case CheckResult of
|
||||||
|
ok ->
|
||||||
|
ok = emqx_metrics:inc(?AUTH_METRICS(success)),
|
||||||
|
{stop, AuthResult#{auth_result => success, anonymous => false}};
|
||||||
|
{error, not_found} ->
|
||||||
|
emqx_metrics:inc(?AUTH_METRICS(ignore));
|
||||||
|
{error, ResultCode} ->
|
||||||
|
ok = emqx_metrics:inc(?AUTH_METRICS(failure)),
|
||||||
|
?LOG(error, "[LDAP] Auth from ldap failed: ~p", [ResultCode]),
|
||||||
|
{stop, AuthResult#{auth_result => ResultCode, anonymous => false}}
|
||||||
|
end.
|
||||||
|
|
||||||
|
lookup_user(Username, #{username_attr := UidAttr,
|
||||||
|
match_objectclass := ObjectClass,
|
||||||
|
device_dn := DeviceDn,
|
||||||
|
custom_base_dn := CustomBaseDN, pool := Pool} = Config) ->
|
||||||
|
|
||||||
|
Filters = maps:get(filters, Config, []),
|
||||||
|
|
||||||
|
ReplaceRules = [{"${username_attr}", UidAttr},
|
||||||
|
{"${user}", binary_to_list(Username)},
|
||||||
|
{"${device_dn}", DeviceDn}],
|
||||||
|
|
||||||
|
Filter = prepare_filter(Filters, UidAttr, ObjectClass, ReplaceRules),
|
||||||
|
|
||||||
|
%% auth.ldap.custom_base_dn = "${username_attr}=${user},${device_dn}"
|
||||||
|
BaseDN = replace_vars(CustomBaseDN, ReplaceRules),
|
||||||
|
|
||||||
|
case search(Pool, BaseDN, Filter) of
|
||||||
|
%% This clause seems to be impossible to match. `eldap2:search/2` does
|
||||||
|
%% not validates the result, so if it returns "successfully" from the
|
||||||
|
%% LDAP server, it always returns `{ok, #eldap_search_result{}}`.
|
||||||
|
{error, noSuchObject} ->
|
||||||
|
undefined;
|
||||||
|
%% In case no user was found by the search, but the search was completed
|
||||||
|
%% without error we get an empty `entries` list.
|
||||||
|
{ok, #eldap_search_result{entries = []}} ->
|
||||||
|
undefined;
|
||||||
|
{ok, #eldap_search_result{entries = [Entry]}} ->
|
||||||
|
Attributes = Entry#eldap_entry.attributes,
|
||||||
|
case get_value("isEnabled", Attributes) of
|
||||||
|
undefined ->
|
||||||
|
Entry;
|
||||||
|
[Val] ->
|
||||||
|
case list_to_atom(string:to_lower(Val)) of
|
||||||
|
true -> Entry;
|
||||||
|
false -> {error, username_disabled}
|
||||||
|
end
|
||||||
|
end;
|
||||||
|
{error, Error} ->
|
||||||
|
?LOG(error, "[LDAP] Search dn: ~p, filter: ~p, fail:~p", [DeviceDn, Filter, Error]),
|
||||||
|
{error, username_or_password_error}
|
||||||
|
end.
|
||||||
|
|
||||||
|
check_pass(Password, Password, _ClientInfo) -> ok;
|
||||||
|
check_pass(_, _, _) -> {error, bad_username_or_password}.
|
||||||
|
|
||||||
|
format_password(Passhash, Password, ClientInfo) ->
|
||||||
|
case do_format_password(Passhash, Password) of
|
||||||
|
{error, Error2} ->
|
||||||
|
{error, Error2};
|
||||||
|
{Passhash1, Password1} ->
|
||||||
|
check_pass(Passhash1, Password1, ClientInfo)
|
||||||
|
end.
|
||||||
|
|
||||||
|
do_format_password(Passhash, Password) ->
|
||||||
|
Base64PasshashHandler =
|
||||||
|
handle_passhash(fun(HashType, Passhash1, Password1) ->
|
||||||
|
Passhash2 = binary_to_list(base64:decode(Passhash1)),
|
||||||
|
resolve_passhash(HashType, Passhash2, Password1)
|
||||||
|
end,
|
||||||
|
fun(_Passhash, _Password) ->
|
||||||
|
{error, password_error}
|
||||||
|
end),
|
||||||
|
PasshashHandler = handle_passhash(fun resolve_passhash/3, Base64PasshashHandler),
|
||||||
|
PasshashHandler(Passhash, Password).
|
||||||
|
|
||||||
|
resolve_passhash(HashType, Passhash, Password) ->
|
||||||
|
[_, Passhash1] = string:tokens(Passhash, "}"),
|
||||||
|
do_resolve(HashType, Passhash1, Password).
|
||||||
|
|
||||||
|
handle_passhash(HandleMatch, HandleNoMatch) ->
|
||||||
|
fun(Passhash, Password) ->
|
||||||
|
case re:run(Passhash, "(?<={)[^{}]+(?=})", [{capture, all, list}, global]) of
|
||||||
|
{match, [[HashType]]} ->
|
||||||
|
HandleMatch(list_to_atom(string:to_lower(HashType)), Passhash, Password);
|
||||||
|
_ ->
|
||||||
|
HandleNoMatch(Passhash, Password)
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
do_resolve(ssha, Passhash, Password) ->
|
||||||
|
D64 = base64:decode(Passhash),
|
||||||
|
{HashedData, Salt} = lists:split(20, binary_to_list(D64)),
|
||||||
|
NewHash = crypto:hash(sha, <<Password/binary, (list_to_binary(Salt))/binary>>),
|
||||||
|
{list_to_binary(HashedData), NewHash};
|
||||||
|
do_resolve(HashType, Passhash, Password) ->
|
||||||
|
Password1 = base64:encode(crypto:hash(HashType, Password)),
|
||||||
|
{list_to_binary(Passhash), Password1}.
|
||||||
|
|
||||||
|
description() -> "LDAP Authentication Plugin".
|
||||||
|
|
||||||
|
prepare_filter(Filters, _UidAttr, ObjectClass, ReplaceRules) ->
|
||||||
|
SubFilters =
|
||||||
|
lists:map(fun({K, V}) ->
|
||||||
|
{replace_vars(K, ReplaceRules), replace_vars(V, ReplaceRules)};
|
||||||
|
(Op) ->
|
||||||
|
Op
|
||||||
|
end, Filters),
|
||||||
|
case SubFilters of
|
||||||
|
[] -> eldap2:equalityMatch("objectClass", ObjectClass);
|
||||||
|
_List -> compile_filters(SubFilters, [])
|
||||||
|
end.
|
||||||
|
|
||||||
|
|
||||||
|
compile_filters([{Key, Value}], []) ->
|
||||||
|
compile_equal(Key, Value);
|
||||||
|
compile_filters([{K1, V1}, "and", {K2, V2} | Rest], []) ->
|
||||||
|
compile_filters(
|
||||||
|
Rest,
|
||||||
|
eldap2:'and'([compile_equal(K1, V1),
|
||||||
|
compile_equal(K2, V2)]));
|
||||||
|
compile_filters([{K1, V1}, "or", {K2, V2} | Rest], []) ->
|
||||||
|
compile_filters(
|
||||||
|
Rest,
|
||||||
|
eldap2:'or'([compile_equal(K1, V1),
|
||||||
|
compile_equal(K2, V2)]));
|
||||||
|
compile_filters(["and", {K, V} | Rest], PartialFilter) ->
|
||||||
|
compile_filters(
|
||||||
|
Rest,
|
||||||
|
eldap2:'and'([PartialFilter,
|
||||||
|
compile_equal(K, V)]));
|
||||||
|
compile_filters(["or", {K, V} | Rest], PartialFilter) ->
|
||||||
|
compile_filters(
|
||||||
|
Rest,
|
||||||
|
eldap2:'or'([PartialFilter,
|
||||||
|
compile_equal(K, V)]));
|
||||||
|
compile_filters([], Filter) ->
|
||||||
|
Filter.
|
||||||
|
|
||||||
|
compile_equal(Key, Value) ->
|
||||||
|
eldap2:equalityMatch(Key, Value).
|
||||||
|
|
||||||
|
replace_vars(CustomBaseDN, ReplaceRules) ->
|
||||||
|
lists:foldl(fun({Pattern, Substitute}, DN) ->
|
||||||
|
lists:flatten(string:replace(DN, Pattern, Substitute))
|
||||||
|
end, CustomBaseDN, ReplaceRules).
|
|
@ -0,0 +1,78 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed 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.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_auth_ldap_app).
|
||||||
|
|
||||||
|
-behaviour(application).
|
||||||
|
|
||||||
|
-emqx_plugin(auth).
|
||||||
|
|
||||||
|
-include("emqx_auth_ldap.hrl").
|
||||||
|
|
||||||
|
%% Application callbacks
|
||||||
|
-export([ start/2
|
||||||
|
, prep_stop/1
|
||||||
|
, stop/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
start(_StartType, _StartArgs) ->
|
||||||
|
{ok, Sup} = emqx_auth_ldap_sup:start_link(),
|
||||||
|
if_enabled([device_dn, match_objectclass,
|
||||||
|
username_attr, password_attr,
|
||||||
|
filters, custom_base_dn, bind_as_user],
|
||||||
|
fun load_auth_hook/1),
|
||||||
|
if_enabled([device_dn, match_objectclass,
|
||||||
|
username_attr, password_attr,
|
||||||
|
filters, custom_base_dn, bind_as_user],
|
||||||
|
fun load_acl_hook/1),
|
||||||
|
{ok, Sup}.
|
||||||
|
|
||||||
|
prep_stop(State) ->
|
||||||
|
emqx:unhook('client.authenticate', fun emqx_auth_ldap:check/3),
|
||||||
|
emqx:unhook('client.check_acl', fun emqx_acl_ldap:check_acl/5),
|
||||||
|
State.
|
||||||
|
|
||||||
|
stop(_State) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
load_auth_hook(DeviceDn) ->
|
||||||
|
ok = emqx_auth_ldap:register_metrics(),
|
||||||
|
Params = maps:from_list(DeviceDn),
|
||||||
|
emqx:hook('client.authenticate', fun emqx_auth_ldap:check/3, [Params#{pool => ?APP}]).
|
||||||
|
|
||||||
|
load_acl_hook(DeviceDn) ->
|
||||||
|
ok = emqx_acl_ldap:register_metrics(),
|
||||||
|
Params = maps:from_list(DeviceDn),
|
||||||
|
emqx:hook('client.check_acl', fun emqx_acl_ldap:check_acl/5 , [Params#{pool => ?APP}]).
|
||||||
|
|
||||||
|
if_enabled(Cfgs, Fun) ->
|
||||||
|
case get_env(Cfgs) of
|
||||||
|
{ok, InitArgs} -> Fun(InitArgs);
|
||||||
|
[] -> ok
|
||||||
|
end.
|
||||||
|
|
||||||
|
get_env(Cfgs) ->
|
||||||
|
get_env(Cfgs, []).
|
||||||
|
|
||||||
|
get_env([Cfg | LeftCfgs], ENVS) ->
|
||||||
|
case application:get_env(?APP, Cfg) of
|
||||||
|
{ok, ENV} ->
|
||||||
|
get_env(LeftCfgs, [{Cfg, ENV} | ENVS]);
|
||||||
|
undefined ->
|
||||||
|
get_env(LeftCfgs, ENVS)
|
||||||
|
end;
|
||||||
|
get_env([], ENVS) ->
|
||||||
|
{ok, ENVS}.
|
|
@ -0,0 +1,150 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed 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.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_auth_ldap_cli).
|
||||||
|
|
||||||
|
-behaviour(ecpool_worker).
|
||||||
|
|
||||||
|
-include("emqx_auth_ldap.hrl").
|
||||||
|
|
||||||
|
-include_lib("emqx/include/emqx.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
|
%% ecpool callback
|
||||||
|
-export([connect/1]).
|
||||||
|
|
||||||
|
-export([ search/3
|
||||||
|
, search/4
|
||||||
|
, post_bind/3
|
||||||
|
, init_args/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
-import(proplists,
|
||||||
|
[ get_value/2
|
||||||
|
, get_value/3
|
||||||
|
]).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% LDAP Connect/Search
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
connect(Opts) ->
|
||||||
|
Servers = get_value(servers, Opts, ["localhost"]),
|
||||||
|
Port = get_value(port, Opts, 389),
|
||||||
|
Timeout = get_value(timeout, Opts, 30),
|
||||||
|
BindDn = get_value(bind_dn, Opts),
|
||||||
|
BindPassword = get_value(bind_password, Opts),
|
||||||
|
LdapOpts = case get_value(ssl, Opts, false)of
|
||||||
|
true ->
|
||||||
|
SslOpts = get_value(sslopts, Opts),
|
||||||
|
[{port, Port}, {timeout, Timeout}, {sslopts, SslOpts}];
|
||||||
|
false ->
|
||||||
|
[{port, Port}, {timeout, Timeout}]
|
||||||
|
end,
|
||||||
|
?LOG(debug, "[LDAP] Connecting to OpenLDAP server: ~p, Opts:~p ...", [Servers, LdapOpts]),
|
||||||
|
|
||||||
|
case eldap2:open(Servers, LdapOpts) of
|
||||||
|
{ok, LDAP} ->
|
||||||
|
try eldap2:simple_bind(LDAP, BindDn, BindPassword) of
|
||||||
|
ok -> {ok, LDAP};
|
||||||
|
{error, Error} ->
|
||||||
|
?LOG(error, "[LDAP] Can't authenticated to OpenLDAP server: ~p", [Error]),
|
||||||
|
{error, Error}
|
||||||
|
catch
|
||||||
|
error:Reason ->
|
||||||
|
?LOG(error, "[LDAP] Can't authenticated to OpenLDAP server: ~p", [Reason]),
|
||||||
|
{error, Reason}
|
||||||
|
end;
|
||||||
|
{error, Reason} ->
|
||||||
|
?LOG(error, "[LDAP] Can't connect to OpenLDAP server: ~p", [Reason]),
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
search(Pool, Base, Filter) ->
|
||||||
|
ecpool:with_client(Pool,
|
||||||
|
fun(C) ->
|
||||||
|
case application:get_env(?APP, bind_as_user) of
|
||||||
|
{ok, true} ->
|
||||||
|
{ok, Opts} = application:get_env(?APP, ldap),
|
||||||
|
BindDn = get_value(bind_dn, Opts),
|
||||||
|
BindPassword = get_value(bind_password, Opts),
|
||||||
|
try eldap2:simple_bind(C, BindDn, BindPassword) of
|
||||||
|
ok ->
|
||||||
|
eldap2:search(C, [{base, Base},
|
||||||
|
{filter, Filter},
|
||||||
|
{deref, eldap2:derefFindingBaseObj()}]);
|
||||||
|
{error, Error} ->
|
||||||
|
{error, Error}
|
||||||
|
catch
|
||||||
|
error:Reason -> {error, Reason}
|
||||||
|
end;
|
||||||
|
{ok, false} ->
|
||||||
|
eldap2:search(C, [{base, Base},
|
||||||
|
{filter, Filter},
|
||||||
|
{deref, eldap2:derefFindingBaseObj()}])
|
||||||
|
end
|
||||||
|
end).
|
||||||
|
|
||||||
|
search(Pool, Base, Filter, Attributes) ->
|
||||||
|
ecpool:with_client(Pool,
|
||||||
|
fun(C) ->
|
||||||
|
case application:get_env(?APP, bind_as_user) of
|
||||||
|
{ok, true} ->
|
||||||
|
{ok, Opts} = application:get_env(?APP, ldap),
|
||||||
|
BindDn = get_value(bind_dn, Opts),
|
||||||
|
BindPassword = get_value(bind_password, Opts),
|
||||||
|
try eldap2:simple_bind(C, BindDn, BindPassword) of
|
||||||
|
ok ->
|
||||||
|
eldap2:search(C, [{base, Base},
|
||||||
|
{filter, Filter},
|
||||||
|
{attributes, Attributes},
|
||||||
|
{deref, eldap2:derefFindingBaseObj()}]);
|
||||||
|
{error, Error} ->
|
||||||
|
{error, Error}
|
||||||
|
catch
|
||||||
|
error:Reason -> {error, Reason}
|
||||||
|
end;
|
||||||
|
{ok, false} ->
|
||||||
|
eldap2:search(C, [{base, Base},
|
||||||
|
{filter, Filter},
|
||||||
|
{attributes, Attributes},
|
||||||
|
{deref, eldap2:derefFindingBaseObj()}])
|
||||||
|
end
|
||||||
|
end).
|
||||||
|
|
||||||
|
post_bind(Pool, BindDn, BindPassword) ->
|
||||||
|
ecpool:with_client(Pool,
|
||||||
|
fun(C) ->
|
||||||
|
try eldap2:simple_bind(C, BindDn, BindPassword) of
|
||||||
|
ok -> ok;
|
||||||
|
{error, Error} ->
|
||||||
|
{error, Error}
|
||||||
|
catch
|
||||||
|
error:Reason -> {error, Reason}
|
||||||
|
end
|
||||||
|
end).
|
||||||
|
|
||||||
|
|
||||||
|
init_args(ENVS) ->
|
||||||
|
DeviceDn = get_value(device_dn, ENVS),
|
||||||
|
ObjectClass = get_value(match_objectclass, ENVS),
|
||||||
|
UidAttr = get_value(username_attr, ENVS),
|
||||||
|
PasswdAttr = get_value(password_attr, ENVS),
|
||||||
|
{ok, #{device_dn => DeviceDn,
|
||||||
|
match_objectclass => ObjectClass,
|
||||||
|
username_attr => UidAttr,
|
||||||
|
password_attr => PasswdAttr}}.
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed 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.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_auth_ldap_sup).
|
||||||
|
|
||||||
|
-behaviour(supervisor).
|
||||||
|
|
||||||
|
-include("emqx_auth_ldap.hrl").
|
||||||
|
|
||||||
|
-export([start_link/0]).
|
||||||
|
|
||||||
|
-export([init/1]).
|
||||||
|
|
||||||
|
start_link() ->
|
||||||
|
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||||
|
|
||||||
|
init([]) ->
|
||||||
|
%% LDAP Connection Pool.
|
||||||
|
{ok, Server} = application:get_env(?APP, ldap),
|
||||||
|
PoolSpec = ecpool:pool_spec(?APP, ?APP, emqx_auth_ldap_cli, Server),
|
||||||
|
{ok, {{one_for_one, 10, 100}, [PoolSpec]}}.
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDUTCCAjmgAwIBAgIJAPPYCjTmxdt/MA0GCSqGSIb3DQEBCwUAMD8xCzAJBgNV
|
||||||
|
BAYTAkNOMREwDwYDVQQIDAhoYW5nemhvdTEMMAoGA1UECgwDRU1RMQ8wDQYDVQQD
|
||||||
|
DAZSb290Q0EwHhcNMjAwNTA4MDgwNjUyWhcNMzAwNTA2MDgwNjUyWjA/MQswCQYD
|
||||||
|
VQQGEwJDTjERMA8GA1UECAwIaGFuZ3pob3UxDDAKBgNVBAoMA0VNUTEPMA0GA1UE
|
||||||
|
AwwGUm9vdENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzcgVLex1
|
||||||
|
EZ9ON64EX8v+wcSjzOZpiEOsAOuSXOEN3wb8FKUxCdsGrsJYB7a5VM/Jot25Mod2
|
||||||
|
juS3OBMg6r85k2TWjdxUoUs+HiUB/pP/ARaaW6VntpAEokpij/przWMPgJnBF3Ur
|
||||||
|
MjtbLayH9hGmpQrI5c2vmHQ2reRZnSFbY+2b8SXZ+3lZZgz9+BaQYWdQWfaUWEHZ
|
||||||
|
uDaNiViVO0OT8DRjCuiDp3yYDj3iLWbTA/gDL6Tf5XuHuEwcOQUrd+h0hyIphO8D
|
||||||
|
tsrsHZ14j4AWYLk1CPA6pq1HIUvEl2rANx2lVUNv+nt64K/Mr3RnVQd9s8bK+TXQ
|
||||||
|
KGHd2Lv/PALYuwIDAQABo1AwTjAdBgNVHQ4EFgQUGBmW+iDzxctWAWxmhgdlE8Pj
|
||||||
|
EbQwHwYDVR0jBBgwFoAUGBmW+iDzxctWAWxmhgdlE8PjEbQwDAYDVR0TBAUwAwEB
|
||||||
|
/zANBgkqhkiG9w0BAQsFAAOCAQEAGbhRUjpIred4cFAFJ7bbYD9hKu/yzWPWkMRa
|
||||||
|
ErlCKHmuYsYk+5d16JQhJaFy6MGXfLgo3KV2itl0d+OWNH0U9ULXcglTxy6+njo5
|
||||||
|
CFqdUBPwN1jxhzo9yteDMKF4+AHIxbvCAJa17qcwUKR5MKNvv09C6pvQDJLzid7y
|
||||||
|
E2dkgSuggik3oa0427KvctFf8uhOV94RvEDyqvT5+pgNYZ2Yfga9pD/jjpoHEUlo
|
||||||
|
88IGU8/wJCx3Ds2yc8+oBg/ynxG8f/HmCC1ET6EHHoe2jlo8FpU/SgGtghS1YL30
|
||||||
|
IWxNsPrUP+XsZpBJy/mvOhE5QXo6Y35zDqqj8tI7AGmAWu22jg==
|
||||||
|
-----END CERTIFICATE-----
|
|
@ -0,0 +1,19 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDEzCCAfugAwIBAgIBAjANBgkqhkiG9w0BAQsFADA/MQswCQYDVQQGEwJDTjER
|
||||||
|
MA8GA1UECAwIaGFuZ3pob3UxDDAKBgNVBAoMA0VNUTEPMA0GA1UEAwwGUm9vdENB
|
||||||
|
MB4XDTIwMDUwODA4MDcwNVoXDTMwMDUwNjA4MDcwNVowPzELMAkGA1UEBhMCQ04x
|
||||||
|
ETAPBgNVBAgMCGhhbmd6aG91MQwwCgYDVQQKDANFTVExDzANBgNVBAMMBlNlcnZl
|
||||||
|
cjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALNeWT3pE+QFfiRJzKmn
|
||||||
|
AMUrWo3K2j/Tm3+Xnl6WLz67/0rcYrJbbKvS3uyRP/stXyXEKw9CepyQ1ViBVFkW
|
||||||
|
Aoy8qQEOWFDsZc/5UzhXUnb6LXr3qTkFEjNmhj+7uzv/lbBxlUG1NlYzSeOB6/RT
|
||||||
|
8zH/lhOeKhLnWYPXdXKsa1FL6ij4X8DeDO1kY7fvAGmBn/THh1uTpDizM4YmeI+7
|
||||||
|
4dmayA5xXvARte5h4Vu5SIze7iC057N+vymToMk2Jgk+ZZFpyXrnq+yo6RaD3ANc
|
||||||
|
lrc4FbeUQZ5a5s5Sxgs9a0Y3WMG+7c5VnVXcbjBRz/aq2NtOnQQjikKKQA8GF080
|
||||||
|
BQkCAwEAAaMaMBgwCQYDVR0TBAIwADALBgNVHQ8EBAMCBeAwDQYJKoZIhvcNAQEL
|
||||||
|
BQADggEBAJefnMZpaRDHQSNUIEL3iwGXE9c6PmIsQVE2ustr+CakBp3TZ4l0enLt
|
||||||
|
iGMfEVFju69cO4oyokWv+hl5eCMkHBf14Kv51vj448jowYnF1zmzn7SEzm5Uzlsa
|
||||||
|
sqjtAprnLyof69WtLU1j5rYWBuFX86yOTwRAFNjm9fvhAcrEONBsQtqipBWkMROp
|
||||||
|
iUYMkRqbKcQMdwxov+lHBYKq9zbWRoqLROAn54SRqgQk6c15JdEfgOOjShbsOkIH
|
||||||
|
UhqcwRkQic7n1zwHVGVDgNIZVgmJ2IdIWBlPEC7oLrRrBD/X1iEEXtKab6p5o22n
|
||||||
|
KB5mN+iQaE+Oe2cpGKZJiJRdM+IqDDQ=
|
||||||
|
-----END CERTIFICATE-----
|
|
@ -0,0 +1,19 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDEzCCAfugAwIBAgIBATANBgkqhkiG9w0BAQsFADA/MQswCQYDVQQGEwJDTjER
|
||||||
|
MA8GA1UECAwIaGFuZ3pob3UxDDAKBgNVBAoMA0VNUTEPMA0GA1UEAwwGUm9vdENB
|
||||||
|
MB4XDTIwMDUwODA4MDY1N1oXDTMwMDUwNjA4MDY1N1owPzELMAkGA1UEBhMCQ04x
|
||||||
|
ETAPBgNVBAgMCGhhbmd6aG91MQwwCgYDVQQKDANFTVExDzANBgNVBAMMBkNsaWVu
|
||||||
|
dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMy4hoksKcZBDbY680u6
|
||||||
|
TS25U51nuB1FBcGMlF9B/t057wPOlxF/OcmbxY5MwepS41JDGPgulE1V7fpsXkiW
|
||||||
|
1LUimYV/tsqBfymIe0mlY7oORahKji7zKQ2UBIVFhdlvQxunlIDnw6F9popUgyHt
|
||||||
|
dMhtlgZK8oqRwHxO5dbfoukYd6J/r+etS5q26sgVkf3C6dt0Td7B25H9qW+f7oLV
|
||||||
|
PbcHYCa+i73u9670nrpXsC+Qc7Mygwa2Kq/jwU+ftyLQnOeW07DuzOwsziC/fQZa
|
||||||
|
nbxR+8U9FNftgRcC3uP/JMKYUqsiRAuaDokARZxVTV5hUElfpO6z6/NItSDvvh3i
|
||||||
|
eikCAwEAAaMaMBgwCQYDVR0TBAIwADALBgNVHQ8EBAMCBeAwDQYJKoZIhvcNAQEL
|
||||||
|
BQADggEBABchYxKo0YMma7g1qDswJXsR5s56Czx/I+B41YcpMBMTrRqpUC0nHtLk
|
||||||
|
M7/tZp592u/tT8gzEnQjZLKBAhFeZaR3aaKyknLqwiPqJIgg0pgsBGITrAK3Pv4z
|
||||||
|
5/YvAJJKgTe5UdeTz6U4lvNEux/4juZ4pmqH4qSFJTOzQS7LmgSmNIdd072rwXBd
|
||||||
|
UzcSHzsJgEMb88u/LDLjj1pQ7AtZ4Tta8JZTvcgBFmjB0QUi6fgkHY6oGat/W4kR
|
||||||
|
jSRUBlMUbM/drr2PVzRc2dwbFIl3X+ZE6n5Sl3ZwRAC/s92JU6CPMRW02muVu6xl
|
||||||
|
goraNgPISnrbpR6KjxLZkVembXzjNNc=
|
||||||
|
-----END CERTIFICATE-----
|
|
@ -0,0 +1,27 @@
|
||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEpAIBAAKCAQEAzLiGiSwpxkENtjrzS7pNLblTnWe4HUUFwYyUX0H+3TnvA86X
|
||||||
|
EX85yZvFjkzB6lLjUkMY+C6UTVXt+mxeSJbUtSKZhX+2yoF/KYh7SaVjug5FqEqO
|
||||||
|
LvMpDZQEhUWF2W9DG6eUgOfDoX2milSDIe10yG2WBkryipHAfE7l1t+i6Rh3on+v
|
||||||
|
561LmrbqyBWR/cLp23RN3sHbkf2pb5/ugtU9twdgJr6Lve73rvSeulewL5BzszKD
|
||||||
|
BrYqr+PBT5+3ItCc55bTsO7M7CzOIL99BlqdvFH7xT0U1+2BFwLe4/8kwphSqyJE
|
||||||
|
C5oOiQBFnFVNXmFQSV+k7rPr80i1IO++HeJ6KQIDAQABAoIBAGWgvPjfuaU3qizq
|
||||||
|
uti/FY07USz0zkuJdkANH6LiSjlchzDmn8wJ0pApCjuIE0PV/g9aS8z4opp5q/gD
|
||||||
|
UBLM/a8mC/xf2EhTXOMrY7i9p/I3H5FZ4ZehEqIw9sWKK9YzC6dw26HabB2BGOnW
|
||||||
|
5nozPSQ6cp2RGzJ7BIkxSZwPzPnVTgy3OAuPOiJytvK+hGLhsNaT+Y9bNDvplVT2
|
||||||
|
ZwYTV8GlHZC+4b2wNROILm0O86v96O+Qd8nn3fXjGHbMsAnONBq10bZS16L4fvkH
|
||||||
|
5G+W/1PeSXmtZFppdRRDxIW+DWcXK0D48WRliuxcV4eOOxI+a9N2ZJZZiNLQZGwg
|
||||||
|
w3A8+mECgYEA8HuJFrlRvdoBe2U/EwUtG74dcyy30L4yEBnN5QscXmEEikhaQCfX
|
||||||
|
Wm6EieMcIB/5I5TQmSw0cmBMeZjSXYoFdoI16/X6yMMuATdxpvhOZGdUGXxhAH+x
|
||||||
|
xoTUavWZnEqW3fkUU71kT5E2f2i+0zoatFESXHeslJyz85aAYpP92H0CgYEA2e5A
|
||||||
|
Yozt5eaA1Gyhd8SeptkEU4xPirNUnVQHStpMWUb1kzTNXrPmNWccQ7JpfpG6DcYl
|
||||||
|
zUF6p6mlzY+zkMiyPQjwEJlhiHM2NlL1QS7td0R8ewgsFoyn8WsBI4RejWrEG9td
|
||||||
|
EDniuIw+pBFkcWthnTLHwECHdzgquToyTMjrBB0CgYEA28tdGbrZXhcyAZEhHAZA
|
||||||
|
Gzog+pKlkpEzeonLKIuGKzCrEKRecIK5jrqyQsCjhS0T7ZRnL4g6i0s+umiV5M5w
|
||||||
|
fcc292pEA1h45L3DD6OlKplSQVTv55/OYS4oY3YEJtf5mfm8vWi9lQeY8sxOlQpn
|
||||||
|
O+VZTdBHmTC8PGeTAgZXHZUCgYA6Tyv88lYowB7SN2qQgBQu8jvdGtqhcs/99GCr
|
||||||
|
H3N0I69LPsKAR0QeH8OJPXBKhDUywESXAaEOwS5yrLNP1tMRz5Vj65YUCzeDG3kx
|
||||||
|
gpvY4IMp7ArX0bSRvJ6mYSFnVxy3k174G3TVCfksrtagHioVBGQ7xUg5ltafjrms
|
||||||
|
n8l55QKBgQDVzU8tQvBVqY8/1lnw11Vj4fkE/drZHJ5UkdC1eenOfSWhlSLfUJ8j
|
||||||
|
ds7vEWpRPPoVuPZYeR1y78cyxKe1GBx6Wa2lF5c7xjmiu0xbRnrxYeLolce9/ntp
|
||||||
|
asClqpnHT8/VJYTD7Kqj0fouTTZf0zkig/y+2XERppd8k+pSKjUCPQ==
|
||||||
|
-----END RSA PRIVATE KEY-----
|
|
@ -0,0 +1,27 @@
|
||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEowIBAAKCAQEAs15ZPekT5AV+JEnMqacAxStajcraP9Obf5eeXpYvPrv/Stxi
|
||||||
|
sltsq9Le7JE/+y1fJcQrD0J6nJDVWIFUWRYCjLypAQ5YUOxlz/lTOFdSdvotevep
|
||||||
|
OQUSM2aGP7u7O/+VsHGVQbU2VjNJ44Hr9FPzMf+WE54qEudZg9d1cqxrUUvqKPhf
|
||||||
|
wN4M7WRjt+8AaYGf9MeHW5OkOLMzhiZ4j7vh2ZrIDnFe8BG17mHhW7lIjN7uILTn
|
||||||
|
s36/KZOgyTYmCT5lkWnJeuer7KjpFoPcA1yWtzgVt5RBnlrmzlLGCz1rRjdYwb7t
|
||||||
|
zlWdVdxuMFHP9qrY206dBCOKQopADwYXTzQFCQIDAQABAoIBAQCuvCbr7Pd3lvI/
|
||||||
|
n7VFQG+7pHRe1VKwAxDkx2t8cYos7y/QWcm8Ptwqtw58HzPZGWYrgGMCRpzzkRSF
|
||||||
|
V9g3wP1S5Scu5C6dBu5YIGc157tqNGXB+SpdZddJQ4Nc6yGHXYERllT04ffBGc3N
|
||||||
|
WG/oYS/1cSteiSIrsDy/91FvGRCi7FPxH3wIgHssY/tw69s1Cfvaq5lr2NTFzxIG
|
||||||
|
xCvpJKEdSfVfS9I7LYiymVjst3IOR/w76/ZFY9cRa8ZtmQSWWsm0TUpRC1jdcbkm
|
||||||
|
ZoJptYWlP+gSwx/fpMYftrkJFGOJhHJHQhwxT5X/ajAISeqjjwkWSEJLwnHQd11C
|
||||||
|
Zy2+29lBAoGBANlEAIK4VxCqyPXNKfoOOi5dS64NfvyH4A1v2+KaHWc7lqaqPN49
|
||||||
|
ezfN2n3X+KWx4cviDD914Yc2JQ1vVJjSaHci7yivocDo2OfZDmjBqzaMp/y+rX1R
|
||||||
|
/f3MmiTqMa468rjaxI9RRZu7vDgpTR+za1+OBCgMzjvAng8dJuN/5gjlAoGBANNY
|
||||||
|
uYPKtearBmkqdrSV7eTUe49Nhr0XotLaVBH37TCW0Xv9wjO2xmbm5Ga/DCtPIsBb
|
||||||
|
yPeYwX9FjoasuadUD7hRvbFu6dBa0HGLmkXRJZTcD7MEX2Lhu4BuC72yDLLFd0r+
|
||||||
|
Ep9WP7F5iJyagYqIZtz+4uf7gBvUDdmvXz3sGr1VAoGAdXTD6eeKeiI6PlhKBztF
|
||||||
|
zOb3EQOO0SsLv3fnodu7ZaHbUgLaoTMPuB17r2jgrYM7FKQCBxTNdfGZmmfDjlLB
|
||||||
|
0xZ5wL8ibU30ZXL8zTlWPElST9sto4B+FYVVF/vcG9sWeUUb2ncPcJ/Po3UAktDG
|
||||||
|
jYQTTyuNGtSJHpad/YOZctkCgYBtWRaC7bq3of0rJGFOhdQT9SwItN/lrfj8hyHA
|
||||||
|
OjpqTV4NfPmhsAtu6j96OZaeQc+FHvgXwt06cE6Rt4RG4uNPRluTFgO7XYFDfitP
|
||||||
|
vCppnoIw6S5BBvHwPP+uIhUX2bsi/dm8vu8tb+gSvo4PkwtFhEr6I9HglBKmcmog
|
||||||
|
q6waEQKBgHyecFBeM6Ls11Cd64vborwJPAuxIW7HBAFj/BS99oeG4TjBx4Sz2dFd
|
||||||
|
rzUibJt4ndnHIvCN8JQkjNG14i9hJln+H3mRss8fbZ9vQdqG+2vOWADYSzzsNI55
|
||||||
|
RFY7JjluKcVkp/zCDeUxTU3O6sS+v6/3VE11Cob6OYQx3lN5wrZ3
|
||||||
|
-----END RSA PRIVATE KEY-----
|
|
@ -0,0 +1,153 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed 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.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_auth_ldap_SUITE).
|
||||||
|
|
||||||
|
-compile(export_all).
|
||||||
|
-compile(nowarn_export_all).
|
||||||
|
|
||||||
|
-include_lib("emqx/include/emqx.hrl").
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
-include_lib("common_test/include/ct.hrl").
|
||||||
|
|
||||||
|
-define(PID, emqx_auth_ldap).
|
||||||
|
|
||||||
|
-define(APP, emqx_auth_ldap).
|
||||||
|
|
||||||
|
-define(DeviceDN, "ou=test_device,dc=emqx,dc=io").
|
||||||
|
|
||||||
|
-define(AuthDN, "ou=test_auth,dc=emqx,dc=io").
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Setups
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
all() ->
|
||||||
|
[{group, nossl}, {group, ssl}].
|
||||||
|
|
||||||
|
groups() ->
|
||||||
|
Cases = emqx_ct:all(?MODULE),
|
||||||
|
[{nossl, Cases}, {ssl, Cases}].
|
||||||
|
|
||||||
|
init_per_group(GrpName, Cfg) ->
|
||||||
|
Fun = fun(App) -> set_special_configs(GrpName, App) end,
|
||||||
|
emqx_ct_helpers:start_apps([emqx_auth_ldap], Fun),
|
||||||
|
emqx_mod_acl_internal:unload([]),
|
||||||
|
Cfg.
|
||||||
|
|
||||||
|
end_per_group(_GrpName, _Cfg) ->
|
||||||
|
emqx_ct_helpers:stop_apps([emqx_auth_ldap]).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Cases
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
t_check_auth(_) ->
|
||||||
|
MqttUser1 = #{clientid => <<"mqttuser1">>,
|
||||||
|
username => <<"mqttuser0001">>,
|
||||||
|
password => <<"mqttuser0001">>,
|
||||||
|
zone => external},
|
||||||
|
MqttUser2 = #{clientid => <<"mqttuser2">>,
|
||||||
|
username => <<"mqttuser0002">>,
|
||||||
|
password => <<"mqttuser0002">>,
|
||||||
|
zone => external},
|
||||||
|
MqttUser3 = #{clientid => <<"mqttuser3">>,
|
||||||
|
username => <<"mqttuser0003">>,
|
||||||
|
password => <<"mqttuser0003">>,
|
||||||
|
zone => external},
|
||||||
|
MqttUser4 = #{clientid => <<"mqttuser4">>,
|
||||||
|
username => <<"mqttuser0004">>,
|
||||||
|
password => <<"mqttuser0004">>,
|
||||||
|
zone => external},
|
||||||
|
MqttUser5 = #{clientid => <<"mqttuser5">>,
|
||||||
|
username => <<"mqttuser0005">>,
|
||||||
|
password => <<"mqttuser0005">>,
|
||||||
|
zone => external},
|
||||||
|
NonExistUser1 = #{clientid => <<"mqttuser6">>,
|
||||||
|
username => <<"mqttuser0006">>,
|
||||||
|
password => <<"mqttuser0006">>,
|
||||||
|
zone => external},
|
||||||
|
NonExistUser2 = #{clientid => <<"mqttuser7">>,
|
||||||
|
username => <<"mqttuser0005">>,
|
||||||
|
password => <<"mqttuser0006">>,
|
||||||
|
zone => external},
|
||||||
|
ct:log("MqttUser: ~p", [emqx_access_control:authenticate(MqttUser1)]),
|
||||||
|
?assertMatch({ok, #{auth_result := success}}, emqx_access_control:authenticate(MqttUser1)),
|
||||||
|
?assertMatch({ok, #{auth_result := success}}, emqx_access_control:authenticate(MqttUser2)),
|
||||||
|
?assertMatch({ok, #{auth_result := success}}, emqx_access_control:authenticate(MqttUser3)),
|
||||||
|
?assertMatch({ok, #{auth_result := success}}, emqx_access_control:authenticate(MqttUser4)),
|
||||||
|
?assertMatch({ok, #{auth_result := success}}, emqx_access_control:authenticate(MqttUser5)),
|
||||||
|
?assertEqual({error, not_authorized}, emqx_access_control:authenticate(NonExistUser1)),
|
||||||
|
?assertEqual({error, bad_username_or_password}, emqx_access_control:authenticate(NonExistUser2)).
|
||||||
|
|
||||||
|
t_check_acl(_) ->
|
||||||
|
MqttUser = #{clientid => <<"mqttuser1">>, username => <<"mqttuser0001">>, zone => external},
|
||||||
|
NoMqttUser = #{clientid => <<"mqttuser2">>, username => <<"mqttuser0007">>, zone => external},
|
||||||
|
allow = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/pub/1">>),
|
||||||
|
allow = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/pub/+">>),
|
||||||
|
allow = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/pub/#">>),
|
||||||
|
|
||||||
|
allow = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/sub/1">>),
|
||||||
|
allow = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/sub/+">>),
|
||||||
|
allow = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/sub/#">>),
|
||||||
|
|
||||||
|
allow = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/pubsub/1">>),
|
||||||
|
allow = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/pubsub/+">>),
|
||||||
|
allow = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/pubsub/#">>),
|
||||||
|
allow = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/pubsub/1">>),
|
||||||
|
allow = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/pubsub/+">>),
|
||||||
|
allow = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/pubsub/#">>),
|
||||||
|
|
||||||
|
deny = emqx_access_control:check_acl(NoMqttUser, publish, <<"mqttuser0001/req/mqttuser0001/+">>),
|
||||||
|
deny = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/req/mqttuser0002/+">>),
|
||||||
|
deny = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/req/+/mqttuser0002">>),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Helpers
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
set_special_configs(_, emqx) ->
|
||||||
|
application:set_env(emqx, allow_anonymous, false),
|
||||||
|
application:set_env(emqx, enable_acl_cache, false),
|
||||||
|
application:set_env(emqx, acl_nomatch, deny),
|
||||||
|
AclFilePath = filename:join(["test", "emqx_SUITE_data", "acl.conf"]),
|
||||||
|
application:set_env(emqx, acl_file,
|
||||||
|
emqx_ct_helpers:deps_path(emqx, AclFilePath)),
|
||||||
|
LoadedPluginPath = filename:join(["test", "emqx_SUITE_data", "loaded_plugins"]),
|
||||||
|
application:set_env(emqx, plugins_loaded_file,
|
||||||
|
emqx_ct_helpers:deps_path(emqx, LoadedPluginPath));
|
||||||
|
|
||||||
|
set_special_configs(Ssl, emqx_auth_ldap) ->
|
||||||
|
case Ssl == ssl of
|
||||||
|
true ->
|
||||||
|
LdapOpts = application:get_env(emqx_auth_ldap, ldap, []),
|
||||||
|
Path = emqx_ct_helpers:deps_path(emqx_auth_ldap, "test/certs/"),
|
||||||
|
SslOpts = [{verify, verify_peer},
|
||||||
|
{fail_if_no_peer_cert, true},
|
||||||
|
{server_name_indication, disable},
|
||||||
|
{keyfile, Path ++ "/client-key.pem"},
|
||||||
|
{certfile, Path ++ "/client-cert.pem"},
|
||||||
|
{cacertfile, Path ++ "/cacert.pem"}],
|
||||||
|
LdapOpts1 = lists:keystore(ssl, 1, LdapOpts, {ssl, true}),
|
||||||
|
LdapOpts2 = lists:keystore(sslopts, 1, LdapOpts1, {sslopts, SslOpts}),
|
||||||
|
LdapOpts3 = lists:keystore(port, 1, LdapOpts2, {port, 636}),
|
||||||
|
application:set_env(emqx_auth_ldap, ldap, LdapOpts3);
|
||||||
|
_ ->
|
||||||
|
ok
|
||||||
|
end,
|
||||||
|
application:set_env(emqx_auth_ldap, device_dn, "ou=testdevice, dc=emqx, dc=io").
|
||||||
|
|
|
@ -0,0 +1,114 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed 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.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_auth_ldap_bind_as_user_SUITE).
|
||||||
|
|
||||||
|
-compile(export_all).
|
||||||
|
-compile(no_warning_export).
|
||||||
|
|
||||||
|
-include_lib("emqx/include/emqx.hrl").
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
-include_lib("common_test/include/ct.hrl").
|
||||||
|
|
||||||
|
-define(PID, emqx_auth_ldap).
|
||||||
|
|
||||||
|
-define(APP, emqx_auth_ldap).
|
||||||
|
|
||||||
|
-define(DeviceDN, "ou=test_device,dc=emqx,dc=io").
|
||||||
|
|
||||||
|
-define(AuthDN, "ou=test_auth,dc=emqx,dc=io").
|
||||||
|
|
||||||
|
all() ->
|
||||||
|
[check_auth,
|
||||||
|
check_acl].
|
||||||
|
|
||||||
|
init_per_suite(Config) ->
|
||||||
|
emqx_ct_helpers:start_apps([emqx, emqx_auth_ldap], fun set_special_configs/1),
|
||||||
|
emqx_mod_acl_internal:unload([]),
|
||||||
|
Config.
|
||||||
|
|
||||||
|
end_per_suite(_Config) ->
|
||||||
|
emqx_ct_helpers:stop_apps([emqx_auth_ldap, emqx]).
|
||||||
|
|
||||||
|
check_auth(_) ->
|
||||||
|
MqttUser1 = #{clientid => <<"mqttuser1">>,
|
||||||
|
username => <<"user1">>,
|
||||||
|
password => <<"mqttuser0001">>,
|
||||||
|
zone => external},
|
||||||
|
MqttUser2 = #{clientid => <<"mqttuser2">>,
|
||||||
|
username => <<"user2">>,
|
||||||
|
password => <<"mqttuser0002">>,
|
||||||
|
zone => external},
|
||||||
|
NonExistUser1 = #{clientid => <<"mqttuser3">>,
|
||||||
|
username => <<"user3">>,
|
||||||
|
password => <<"mqttuser0003">>,
|
||||||
|
zone => external},
|
||||||
|
ct:log("MqttUser: ~p", [emqx_access_control:authenticate(MqttUser1)]),
|
||||||
|
?assertMatch({ok, #{auth_result := success}}, emqx_access_control:authenticate(MqttUser1)),
|
||||||
|
?assertMatch({ok, #{auth_result := success}}, emqx_access_control:authenticate(MqttUser2)),
|
||||||
|
?assertEqual({error, not_authorized}, emqx_access_control:authenticate(NonExistUser1)).
|
||||||
|
|
||||||
|
check_acl(_) ->
|
||||||
|
% emqx_modules:load_module(emqx_mod_acl_internal, false),
|
||||||
|
MqttUser = #{clientid => <<"mqttuser1">>, username => <<"user1">>, zone => external},
|
||||||
|
NoMqttUser = #{clientid => <<"mqttuser2">>, username => <<"user7">>, zone => external},
|
||||||
|
allow = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/pub/1">>),
|
||||||
|
allow = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/pub/+">>),
|
||||||
|
allow = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/pub/#">>),
|
||||||
|
|
||||||
|
allow = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/sub/1">>),
|
||||||
|
allow = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/sub/+">>),
|
||||||
|
allow = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/sub/#">>),
|
||||||
|
|
||||||
|
allow = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/pubsub/1">>),
|
||||||
|
allow = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/pubsub/+">>),
|
||||||
|
allow = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/pubsub/#">>),
|
||||||
|
allow = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/pubsub/1">>),
|
||||||
|
allow = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/pubsub/+">>),
|
||||||
|
allow = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/pubsub/#">>),
|
||||||
|
|
||||||
|
deny = emqx_access_control:check_acl(NoMqttUser, publish, <<"mqttuser0001/req/mqttuser0001/+">>),
|
||||||
|
deny = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/req/mqttuser0002/+">>),
|
||||||
|
deny = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/req/+/mqttuser0002">>),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
set_special_configs(emqx) ->
|
||||||
|
application:set_env(emqx, allow_anonymous, false),
|
||||||
|
application:set_env(emqx, enable_acl_cache, false),
|
||||||
|
application:set_env(emqx, acl_nomatch, deny),
|
||||||
|
AclFilePath = filename:join(["test", "emqx_SUITE_data", "acl.conf"]),
|
||||||
|
application:set_env(emqx, acl_file,
|
||||||
|
emqx_ct_helpers:deps_path(emqx, AclFilePath)),
|
||||||
|
LoadedPluginPath = filename:join(["test", "emqx_SUITE_data", "loaded_plugins"]),
|
||||||
|
application:set_env(emqx, plugins_loaded_file,
|
||||||
|
emqx_ct_helpers:deps_path(emqx, LoadedPluginPath));
|
||||||
|
|
||||||
|
set_special_configs(emqx_auth_ldap) ->
|
||||||
|
application:set_env(emqx_auth_ldap, bind_as_user, true),
|
||||||
|
application:set_env(emqx_auth_ldap, device_dn, "ou=testdevice, dc=emqx, dc=io"),
|
||||||
|
application:set_env(emqx_auth_ldap, custom_base_dn, "${device_dn}"),
|
||||||
|
%% auth.ldap.filters.1.key = mqttAccountName
|
||||||
|
%% auth.ldap.filters.1.value = ${user}
|
||||||
|
%% auth.ldap.filters.1.op = and
|
||||||
|
%% auth.ldap.filters.2.key = objectClass
|
||||||
|
%% auth.ldap.filters.1.value = mqttUser
|
||||||
|
application:set_env(emqx_auth_ldap, filters, [{"mqttAccountName", "${user}"},
|
||||||
|
"and",
|
||||||
|
{"objectClass", "mqttUser"}]);
|
||||||
|
|
||||||
|
set_special_configs(_App) ->
|
||||||
|
ok.
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
name: Run test cases
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run_test_cases:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
container:
|
||||||
|
image: erlang:22.1
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- name: run test cases
|
||||||
|
run: |
|
||||||
|
make xref
|
||||||
|
make eunit
|
||||||
|
make ct
|
||||||
|
make cover
|
||||||
|
- uses: actions/upload-artifact@v1
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: logs
|
||||||
|
path: _build/test/logs
|
||||||
|
- uses: actions/upload-artifact@v1
|
||||||
|
with:
|
||||||
|
name: cover
|
||||||
|
path: _build/test/cover
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
.eunit
|
||||||
|
deps
|
||||||
|
*.o
|
||||||
|
*.beam
|
||||||
|
*.plt
|
||||||
|
erl_crash.dump
|
||||||
|
ebin
|
||||||
|
rel/example_project
|
||||||
|
.concrete/DEV_MODE
|
||||||
|
.rebar
|
||||||
|
.erlang.mk/
|
||||||
|
emqx_auth_mnesia.d
|
||||||
|
data/
|
||||||
|
_build/
|
||||||
|
.DS_Store
|
||||||
|
cover/
|
||||||
|
ct.coverdata
|
||||||
|
eunit.coverdata
|
||||||
|
logs/
|
||||||
|
test/ct.cover.spec
|
||||||
|
rebar.lock
|
||||||
|
rebar3.crashdump
|
||||||
|
erlang.mk
|
||||||
|
.*.swp
|
||||||
|
.rebar3/
|
||||||
|
etc/emqx_auth_mnesia.conf.rendered
|
|
@ -0,0 +1,201 @@
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed 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.
|
|
@ -0,0 +1,2 @@
|
||||||
|
emqx_auth_mnesia
|
||||||
|
===============
|
|
@ -0,0 +1,20 @@
|
||||||
|
## Examples:
|
||||||
|
##auth.mnesia.1.login = admin
|
||||||
|
##auth.mnesia.1.password = public
|
||||||
|
##auth.mnesia.1.is_superuser = true
|
||||||
|
##auth.mnesia.2.login = feng@emqtt.io
|
||||||
|
##auth.mnesia.2.password = public
|
||||||
|
##auth.mnesia.2.is_superuser = false
|
||||||
|
##auth.mnesia.3.login = name~!@#$%^&*()_+
|
||||||
|
##auth.mnesia.3.password = pwsswd~!@#$%^&*()_+
|
||||||
|
##auth.mnesia.3.is_superuser = false
|
||||||
|
|
||||||
|
## Password hash.
|
||||||
|
##
|
||||||
|
## Value: plain | md5 | sha | sha256
|
||||||
|
auth.mnesia.password_hash = sha256
|
||||||
|
|
||||||
|
## Auth as username or auth as clientid.
|
||||||
|
##
|
||||||
|
## Value: username | clientid
|
||||||
|
auth.mnesia.as = username
|
|
@ -0,0 +1,35 @@
|
||||||
|
-define(APP, emqx_auth_mnesia).
|
||||||
|
|
||||||
|
-record(emqx_user, {
|
||||||
|
login,
|
||||||
|
password,
|
||||||
|
is_superuser
|
||||||
|
}).
|
||||||
|
|
||||||
|
-record(emqx_acl, {
|
||||||
|
login,
|
||||||
|
topic,
|
||||||
|
action,
|
||||||
|
allow
|
||||||
|
}).
|
||||||
|
|
||||||
|
-record(auth_metrics, {
|
||||||
|
success = 'client.auth.success',
|
||||||
|
failure = 'client.auth.failure',
|
||||||
|
ignore = 'client.auth.ignore'
|
||||||
|
}).
|
||||||
|
|
||||||
|
-record(acl_metrics, {
|
||||||
|
allow = 'client.acl.allow',
|
||||||
|
deny = 'client.acl.deny',
|
||||||
|
ignore = 'client.acl.ignore'
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(METRICS(Type), tl(tuple_to_list(#Type{}))).
|
||||||
|
-define(METRICS(Type, K), #Type{}#Type.K).
|
||||||
|
|
||||||
|
-define(AUTH_METRICS, ?METRICS(auth_metrics)).
|
||||||
|
-define(AUTH_METRICS(K), ?METRICS(auth_metrics, K)).
|
||||||
|
|
||||||
|
-define(ACL_METRICS, ?METRICS(acl_metrics)).
|
||||||
|
-define(ACL_METRICS(K), ?METRICS(acl_metrics, K)).
|
|
@ -0,0 +1,35 @@
|
||||||
|
%%-*- mode: erlang -*-
|
||||||
|
%% emqx_auth_mnesia config mapping
|
||||||
|
|
||||||
|
{mapping, "auth.mnesia.as", "emqx_auth_mnesia.as", [
|
||||||
|
{default, username},
|
||||||
|
{datatype, {enum, [username, clientid]}}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.mnesia.password_hash", "emqx_auth_mnesia.password_hash", [
|
||||||
|
{default, sha256},
|
||||||
|
{datatype, {enum, [plain, md5, sha, sha256]}}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.mnesia.$id.login", "emqx_auth_mnesia.userlist", [
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.mnesia.$id.password", "emqx_auth_mnesia.userlist", [
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.mnesia.$id.is_superuser", "emqx_auth_mnesia.userlist", [
|
||||||
|
{default, false},
|
||||||
|
{datatype, {enum, [false, true]}}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{translation, "emqx_auth_mnesia.userlist", fun(Conf) ->
|
||||||
|
Userlist = cuttlefish_variable:filter_by_prefix("auth.mnesia", Conf),
|
||||||
|
lists:foldl(
|
||||||
|
fun({["auth", "mnesia", Id, "login"], Username}, AccIn) ->
|
||||||
|
[{Username, cuttlefish:conf_get("auth.mnesia." ++ Id ++ ".password", Conf), cuttlefish:conf_get("auth.mnesia." ++ Id ++ ".is_superuser", Conf)} | AccIn];
|
||||||
|
(_, AccIn) ->
|
||||||
|
AccIn
|
||||||
|
end, [], Userlist)
|
||||||
|
end}.
|
|
@ -0,0 +1,29 @@
|
||||||
|
{minimum_otp_vsn, "21"}.
|
||||||
|
|
||||||
|
{deps,
|
||||||
|
[{emqx_passwd, {git, "https://github.com/emqx/emqx-passwd.git", {tag, "v1.1.1"}}},
|
||||||
|
{minirest, {git, "https://github.com/emqx/minirest.git", {tag, "0.3.1"}}}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{profiles,
|
||||||
|
[{test,
|
||||||
|
[{deps,
|
||||||
|
[{emqx_ct_helpers, {git, "https://github.com/emqx/emqx-ct-helpers", {tag, "v1.2.2"}}}
|
||||||
|
]}
|
||||||
|
]}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{erl_opts, [warn_unused_vars,
|
||||||
|
warn_shadow_vars,
|
||||||
|
warn_unused_import,
|
||||||
|
warn_obsolete_guard,
|
||||||
|
debug_info,
|
||||||
|
{parse_transform}]}.
|
||||||
|
|
||||||
|
{xref_checks, [undefined_function_calls, undefined_functions,
|
||||||
|
locals_not_used, deprecated_function_calls,
|
||||||
|
warnings_as_errors, deprecated_functions]}.
|
||||||
|
{cover_enabled, true}.
|
||||||
|
{cover_opts, [verbose]}.
|
||||||
|
{cover_export_enabled, true}.
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed 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.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_acl_mnesia).
|
||||||
|
|
||||||
|
-include("emqx_auth_mnesia.hrl").
|
||||||
|
|
||||||
|
%% ACL Callbacks
|
||||||
|
-export([ init/0
|
||||||
|
, register_metrics/0
|
||||||
|
, check_acl/5
|
||||||
|
, description/0
|
||||||
|
]).
|
||||||
|
|
||||||
|
init() ->
|
||||||
|
ok = ekka_mnesia:create_table(emqx_acl, [
|
||||||
|
{type, bag},
|
||||||
|
{disc_copies, [node()]},
|
||||||
|
{attributes, record_info(fields, emqx_acl)},
|
||||||
|
{storage_properties, [{ets, [{read_concurrency, true}]}]}]),
|
||||||
|
ok = ekka_mnesia:copy_table(emqx_user, disc_copies).
|
||||||
|
|
||||||
|
-spec(register_metrics() -> ok).
|
||||||
|
register_metrics() ->
|
||||||
|
lists:foreach(fun emqx_metrics:ensure/1, ?ACL_METRICS).
|
||||||
|
|
||||||
|
check_acl(ClientInfo, PubSub, Topic, NoMatchAction, #{key_as := As}) ->
|
||||||
|
Login = maps:get(As, ClientInfo),
|
||||||
|
case do_check_acl(Login, PubSub, Topic, NoMatchAction) of
|
||||||
|
ok -> emqx_metrics:inc(?ACL_METRICS(ignore)), ok;
|
||||||
|
{stop, allow} -> emqx_metrics:inc(?ACL_METRICS(allow)), {stop, allow};
|
||||||
|
{stop, deny} -> emqx_metrics:inc(?ACL_METRICS(deny)), {stop, deny}
|
||||||
|
end.
|
||||||
|
|
||||||
|
description() -> "Acl with Mnesia".
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Internal functions
|
||||||
|
%%-------------------------------------------------------------------
|
||||||
|
|
||||||
|
do_check_acl(Login, PubSub, Topic, _NoMatchAction) ->
|
||||||
|
case match(PubSub, Topic, emqx_auth_mnesia_cli:lookup_acl(Login)) of
|
||||||
|
allow -> {stop, allow};
|
||||||
|
deny -> {stop, deny};
|
||||||
|
_ ->
|
||||||
|
case match(PubSub, Topic, emqx_auth_mnesia_cli:lookup_acl(<<"$all">>)) of
|
||||||
|
allow -> {stop, allow};
|
||||||
|
deny -> {stop, deny};
|
||||||
|
_ -> ok
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
match(_PubSub, _Topic, []) ->
|
||||||
|
nomatch;
|
||||||
|
match(PubSub, Topic, [ #emqx_acl{topic = ACLTopic, action = Action, allow = Allow} | UserAcl]) ->
|
||||||
|
case match_actions(PubSub, Action) andalso match_topic(Topic, ACLTopic) of
|
||||||
|
true -> case Allow of
|
||||||
|
true -> allow;
|
||||||
|
_ -> deny
|
||||||
|
end;
|
||||||
|
false -> match(PubSub, Topic, UserAcl)
|
||||||
|
end.
|
||||||
|
|
||||||
|
match_topic(Topic, ACLTopic) when is_binary(Topic) ->
|
||||||
|
emqx_topic:match(Topic, ACLTopic).
|
||||||
|
|
||||||
|
match_actions(_, <<"pubsub">>) -> true;
|
||||||
|
match_actions(subscribe, <<"sub">>) -> true;
|
||||||
|
match_actions(publish, <<"pub">>) -> true;
|
||||||
|
match_actions(_, _) -> false.
|
|
@ -0,0 +1,148 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed 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.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_acl_mnesia_api).
|
||||||
|
|
||||||
|
-include("emqx_auth_mnesia.hrl").
|
||||||
|
|
||||||
|
-import(proplists, [get_value/2]).
|
||||||
|
|
||||||
|
-import(minirest, [return/1]).
|
||||||
|
|
||||||
|
-rest_api(#{name => list_emqx_acl,
|
||||||
|
method => 'GET',
|
||||||
|
path => "/mqtt_acl",
|
||||||
|
func => list,
|
||||||
|
descr => "List available mnesia in the cluster"
|
||||||
|
}).
|
||||||
|
|
||||||
|
-rest_api(#{name => lookup_emqx_acl,
|
||||||
|
method => 'GET',
|
||||||
|
path => "/mqtt_acl/:bin:login",
|
||||||
|
func => lookup,
|
||||||
|
descr => "Lookup mnesia in the cluster"
|
||||||
|
}).
|
||||||
|
|
||||||
|
-rest_api(#{name => add_emqx_acl,
|
||||||
|
method => 'POST',
|
||||||
|
path => "/mqtt_acl",
|
||||||
|
func => add,
|
||||||
|
descr => "Add mnesia in the cluster"
|
||||||
|
}).
|
||||||
|
|
||||||
|
-rest_api(#{name => delete_emqx_acl,
|
||||||
|
method => 'DELETE',
|
||||||
|
path => "/mqtt_acl/:bin:login/:bin:topic",
|
||||||
|
func => delete,
|
||||||
|
descr => "Delete mnesia in the cluster"
|
||||||
|
}).
|
||||||
|
|
||||||
|
-export([ list/2
|
||||||
|
, lookup/2
|
||||||
|
, add/2
|
||||||
|
, delete/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
list(_Bindings, Params) ->
|
||||||
|
return({ok, emqx_auth_mnesia_api:paginate(emqx_acl, Params, fun format/1)}).
|
||||||
|
|
||||||
|
lookup(#{login := Login}, _Params) ->
|
||||||
|
return({ok, format(emqx_auth_mnesia_cli:lookup_acl(urldecode(Login)))}).
|
||||||
|
|
||||||
|
add(_Bindings, Params) ->
|
||||||
|
[ P | _] = Params,
|
||||||
|
case is_list(P) of
|
||||||
|
true -> return(add_acl(Params, []));
|
||||||
|
false -> return(add_acl([Params], []))
|
||||||
|
end.
|
||||||
|
|
||||||
|
add_acl([ Params | ParamsN ], ReList ) ->
|
||||||
|
Login = urldecode(get_value(<<"login">>, Params)),
|
||||||
|
Topic = urldecode(get_value(<<"topic">>, Params)),
|
||||||
|
Action = urldecode(get_value(<<"action">>, Params)),
|
||||||
|
Allow = get_value(<<"allow">>, Params),
|
||||||
|
Re = case validate([login, topic, action, allow], [Login, Topic, Action, Allow]) of
|
||||||
|
ok ->
|
||||||
|
emqx_auth_mnesia_cli:add_acl(Login, Topic, Action, Allow);
|
||||||
|
Err -> Err
|
||||||
|
end,
|
||||||
|
add_acl(ParamsN, [{Login, format_msg(Re)} | ReList]);
|
||||||
|
|
||||||
|
add_acl([], ReList) ->
|
||||||
|
{ok, ReList}.
|
||||||
|
|
||||||
|
delete(#{login := Login, topic := Topic}, _) ->
|
||||||
|
return(emqx_auth_mnesia_cli:remove_acl(urldecode(Login), urldecode(Topic))).
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% Interval Funcs
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
format(#emqx_acl{login = Login, topic = Topic, action = Action, allow = Allow}) ->
|
||||||
|
#{login => Login, topic => Topic, action => Action, allow => Allow };
|
||||||
|
|
||||||
|
format([]) ->
|
||||||
|
#{};
|
||||||
|
|
||||||
|
format([#emqx_acl{login = Login, topic = Topic, action = Action, allow = Allow}]) ->
|
||||||
|
format(#emqx_acl{login = Login, topic = Topic, action = Action, allow = Allow});
|
||||||
|
|
||||||
|
format([ #emqx_acl{login = _Key, topic = _Topic, action = _Action, allow = _Allow}| _] = List) ->
|
||||||
|
format(List, []).
|
||||||
|
|
||||||
|
format([#emqx_acl{login = Login, topic = Topic, action = Action, allow = Allow} | List], ReList) ->
|
||||||
|
format(List, [ format(#emqx_acl{login = Login, topic = Topic, action = Action, allow = Allow}) | ReList]);
|
||||||
|
format([], ReList) -> ReList.
|
||||||
|
|
||||||
|
validate([], []) ->
|
||||||
|
ok;
|
||||||
|
validate([K|Keys], [V|Values]) ->
|
||||||
|
case do_validation(K, V) of
|
||||||
|
false -> {error, K};
|
||||||
|
true -> validate(Keys, Values)
|
||||||
|
end.
|
||||||
|
|
||||||
|
do_validation(login, V) when is_binary(V)
|
||||||
|
andalso byte_size(V) > 0 ->
|
||||||
|
true;
|
||||||
|
do_validation(topic, V) when is_binary(V)
|
||||||
|
andalso byte_size(V) > 0 ->
|
||||||
|
true;
|
||||||
|
do_validation(action, V) when is_binary(V) ->
|
||||||
|
case V =:= <<"pub">> orelse V =:= <<"sub">> orelse V =:= <<"pubsub">> of
|
||||||
|
true -> true;
|
||||||
|
false -> false
|
||||||
|
end;
|
||||||
|
do_validation(allow, V) when is_boolean(V) ->
|
||||||
|
true;
|
||||||
|
do_validation(_, _) ->
|
||||||
|
false.
|
||||||
|
|
||||||
|
format_msg(Message)
|
||||||
|
when is_atom(Message);
|
||||||
|
is_binary(Message) -> Message;
|
||||||
|
|
||||||
|
format_msg(Message) when is_tuple(Message) ->
|
||||||
|
iolist_to_binary(io_lib:format("~p", [Message])).
|
||||||
|
|
||||||
|
-if(?OTP_RELEASE >= 23).
|
||||||
|
urldecode(S) ->
|
||||||
|
[{R, _}] = uri_string:dissect_query(S), R.
|
||||||
|
-else.
|
||||||
|
urldecode(S) ->
|
||||||
|
http_uri:decode(S).
|
||||||
|
-endif.
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
{application, emqx_auth_mnesia,
|
||||||
|
[{description, "EMQ X Authentication with Mnesia"},
|
||||||
|
{vsn, "git"},
|
||||||
|
{modules, []},
|
||||||
|
{registered, []},
|
||||||
|
{applications, [kernel,stdlib,mnesia]},
|
||||||
|
{mod, {emqx_auth_mnesia_app,[]}},
|
||||||
|
{env, []},
|
||||||
|
{licenses, ["Apache-2.0"]},
|
||||||
|
{maintainers, ["EMQ X Team <contact@emqx.io>"]},
|
||||||
|
{links, [{"Homepage", "https://emqx.io/"},
|
||||||
|
{"Github", "https://github.com/emqx/emqx-auth-mnesia"}
|
||||||
|
]}
|
||||||
|
]}.
|
|
@ -0,0 +1,24 @@
|
||||||
|
%%-*- mode: erlang -*-
|
||||||
|
%% .app.src.script
|
||||||
|
|
||||||
|
RemoveLeadingV =
|
||||||
|
fun(Tag) ->
|
||||||
|
case re:run(Tag, "^[v|e]?[0-9]\.[0-9]\.([0-9]|(rc|beta|alpha)\.[0-9])", [{capture, none}]) of
|
||||||
|
nomatch ->
|
||||||
|
re:replace(Tag, "/", "-", [{return ,list}]);
|
||||||
|
_ ->
|
||||||
|
%% if it is a version number prefixed by 'v' or 'e', then remove it
|
||||||
|
re:replace(Tag, "[v|e]", "", [{return ,list}])
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
case os:getenv("EMQX_DEPS_DEFAULT_VSN") of
|
||||||
|
false -> CONFIG; % env var not defined
|
||||||
|
[] -> CONFIG; % env var set to empty string
|
||||||
|
Tag ->
|
||||||
|
[begin
|
||||||
|
AppConf0 = lists:keystore(vsn, 1, AppConf, {vsn, RemoveLeadingV(Tag)}),
|
||||||
|
{application, App, AppConf0}
|
||||||
|
end || Conf = {application, App, AppConf} <- CONFIG]
|
||||||
|
end.
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed 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.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_auth_mnesia).
|
||||||
|
|
||||||
|
-include("emqx_auth_mnesia.hrl").
|
||||||
|
|
||||||
|
-include_lib("emqx/include/emqx.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
-include_lib("emqx/include/types.hrl").
|
||||||
|
|
||||||
|
%% Auth callbacks
|
||||||
|
-export([ init/1
|
||||||
|
, register_metrics/0
|
||||||
|
, check/3
|
||||||
|
, description/0
|
||||||
|
]).
|
||||||
|
|
||||||
|
init(DefaultUsers) ->
|
||||||
|
ok = ekka_mnesia:create_table(emqx_user, [
|
||||||
|
{disc_copies, [node()]},
|
||||||
|
{attributes, record_info(fields, emqx_user)},
|
||||||
|
{storage_properties, [{ets, [{read_concurrency, true}]}]}]),
|
||||||
|
ok = lists:foreach(fun add_default_user/1, DefaultUsers),
|
||||||
|
ok = ekka_mnesia:copy_table(emqx_user, disc_copies).
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
add_default_user({Login, Password, IsSuperuser}) ->
|
||||||
|
emqx_auth_mnesia_cli:add_user(iolist_to_binary(Login), iolist_to_binary(Password), IsSuperuser).
|
||||||
|
|
||||||
|
-spec(register_metrics() -> ok).
|
||||||
|
register_metrics() ->
|
||||||
|
lists:foreach(fun emqx_metrics:ensure/1, ?AUTH_METRICS).
|
||||||
|
|
||||||
|
check(ClientInfo = #{password := Password}, AuthResult, #{hash_type := HashType, key_as := As}) ->
|
||||||
|
Login = maps:get(As, ClientInfo),
|
||||||
|
case emqx_auth_mnesia_cli:lookup_user(Login) of
|
||||||
|
[] ->
|
||||||
|
emqx_metrics:inc(?AUTH_METRICS(ignore)),
|
||||||
|
ok;
|
||||||
|
[User] ->
|
||||||
|
case emqx_passwd:check_pass({User#emqx_user.password, Password}, HashType) of
|
||||||
|
ok ->
|
||||||
|
emqx_metrics:inc(?AUTH_METRICS(success)),
|
||||||
|
{stop, AuthResult#{is_superuser => is_superuser(User),
|
||||||
|
anonymous => false,
|
||||||
|
auth_result => success}};
|
||||||
|
{error, Reason} ->
|
||||||
|
?LOG(error, "[Mnesia] Auth from mnesia failed: ~p", [Reason]),
|
||||||
|
emqx_metrics:inc(?AUTH_METRICS(failure)),
|
||||||
|
{stop, AuthResult#{auth_result => password_error, anonymous => false}}
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
description() -> "Authentication with Mnesia".
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Internal functions
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
is_superuser(#emqx_user{is_superuser = true}) ->
|
||||||
|
true;
|
||||||
|
is_superuser(_) ->
|
||||||
|
false.
|
|
@ -0,0 +1,201 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed 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.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_auth_mnesia_api).
|
||||||
|
|
||||||
|
-include_lib("stdlib/include/qlc.hrl").
|
||||||
|
|
||||||
|
-import(proplists, [get_value/2]).
|
||||||
|
|
||||||
|
-import(minirest, [return/1]).
|
||||||
|
|
||||||
|
-rest_api(#{name => list_emqx_user,
|
||||||
|
method => 'GET',
|
||||||
|
path => "/mqtt_user",
|
||||||
|
func => list,
|
||||||
|
descr => "List available mnesia in the cluster"
|
||||||
|
}).
|
||||||
|
|
||||||
|
-rest_api(#{name => lookup_emqx_user,
|
||||||
|
method => 'GET',
|
||||||
|
path => "/mqtt_user/:bin:login",
|
||||||
|
func => lookup,
|
||||||
|
descr => "Lookup mnesia in the cluster"
|
||||||
|
}).
|
||||||
|
|
||||||
|
-rest_api(#{name => add_emqx_user,
|
||||||
|
method => 'POST',
|
||||||
|
path => "/mqtt_user",
|
||||||
|
func => add,
|
||||||
|
descr => "Add mnesia in the cluster"
|
||||||
|
}).
|
||||||
|
|
||||||
|
-rest_api(#{name => update_emqx_user,
|
||||||
|
method => 'PUT',
|
||||||
|
path => "/mqtt_user/:bin:login",
|
||||||
|
func => update,
|
||||||
|
descr => "Update mnesia in the cluster"
|
||||||
|
}).
|
||||||
|
|
||||||
|
-rest_api(#{name => delete_emqx_user,
|
||||||
|
method => 'DELETE',
|
||||||
|
path => "/mqtt_user/:bin:login",
|
||||||
|
func => delete,
|
||||||
|
descr => "Delete mnesia in the cluster"
|
||||||
|
}).
|
||||||
|
|
||||||
|
-export([ list/2
|
||||||
|
, lookup/2
|
||||||
|
, add/2
|
||||||
|
, update/2
|
||||||
|
, delete/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
-export([paginate/3]).
|
||||||
|
|
||||||
|
list(_Bindings, Params) ->
|
||||||
|
return({ok, paginate(emqx_user, Params, fun format/1)}).
|
||||||
|
|
||||||
|
lookup(#{login := Login}, _Params) ->
|
||||||
|
return({ok, format(emqx_auth_mnesia_cli:lookup_user(urldecode(Login)))}).
|
||||||
|
|
||||||
|
add(_Bindings, Params) ->
|
||||||
|
[ P | _] = Params,
|
||||||
|
case is_list(P) of
|
||||||
|
true -> return(add_user(Params, []));
|
||||||
|
false -> return(add_user([Params], []))
|
||||||
|
end.
|
||||||
|
|
||||||
|
add_user([ Params | ParamsN ], ReList ) ->
|
||||||
|
Login = urldecode(get_value(<<"login">>, Params)),
|
||||||
|
Password = urldecode(get_value(<<"password">>, Params)),
|
||||||
|
IsSuperuser = get_value(<<"is_superuser">>, Params),
|
||||||
|
Re = case validate([login, password, is_superuser], [Login, Password, IsSuperuser]) of
|
||||||
|
ok ->
|
||||||
|
emqx_auth_mnesia_cli:add_user(Login, Password, IsSuperuser);
|
||||||
|
Err -> Err
|
||||||
|
end,
|
||||||
|
add_user(ParamsN, [{Login, format_msg(Re)} | ReList]);
|
||||||
|
|
||||||
|
add_user([], ReList) ->
|
||||||
|
{ok, ReList}.
|
||||||
|
|
||||||
|
update(#{login := Login}, Params) ->
|
||||||
|
Password = get_value(<<"password">>, Params),
|
||||||
|
IsSuperuser = get_value(<<"is_superuser">>, Params),
|
||||||
|
case validate([password, is_superuser], [Password, IsSuperuser]) of
|
||||||
|
ok -> return(emqx_auth_mnesia_cli:update_user(urldecode(Login), urldecode(Password), IsSuperuser));
|
||||||
|
Err -> return(Err)
|
||||||
|
end.
|
||||||
|
|
||||||
|
delete(#{login := Login}, _) ->
|
||||||
|
return(emqx_auth_mnesia_cli:remove_user(urldecode(Login))).
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% Paging Query
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
paginate(Tables, Params, RowFun) ->
|
||||||
|
Qh = query_handle(Tables),
|
||||||
|
Count = count(Tables),
|
||||||
|
Page = page(Params),
|
||||||
|
Limit = limit(Params),
|
||||||
|
Cursor = qlc:cursor(Qh),
|
||||||
|
case Page > 1 of
|
||||||
|
true -> qlc:next_answers(Cursor, (Page - 1) * Limit);
|
||||||
|
false -> ok
|
||||||
|
end,
|
||||||
|
Rows = qlc:next_answers(Cursor, Limit),
|
||||||
|
qlc:delete_cursor(Cursor),
|
||||||
|
#{meta => #{page => Page, limit => Limit, count => Count},
|
||||||
|
data => [RowFun(Row) || Row <- Rows]}.
|
||||||
|
|
||||||
|
query_handle(Table) when is_atom(Table) ->
|
||||||
|
qlc:q([R|| R <- ets:table(Table)]);
|
||||||
|
query_handle([Table]) when is_atom(Table) ->
|
||||||
|
qlc:q([R|| R <- ets:table(Table)]);
|
||||||
|
query_handle(Tables) ->
|
||||||
|
qlc:append([qlc:q([E || E <- ets:table(T)]) || T <- Tables]).
|
||||||
|
|
||||||
|
count(Table) when is_atom(Table) ->
|
||||||
|
ets:info(Table, size);
|
||||||
|
count([Table]) when is_atom(Table) ->
|
||||||
|
ets:info(Table, size);
|
||||||
|
count(Tables) ->
|
||||||
|
lists:sum([count(T) || T <- Tables]).
|
||||||
|
|
||||||
|
page(Params) ->
|
||||||
|
binary_to_integer(proplists:get_value(<<"_page">>, Params, <<"1">>)).
|
||||||
|
|
||||||
|
limit(Params) ->
|
||||||
|
case proplists:get_value(<<"_limit">>, Params) of
|
||||||
|
undefined -> 10;
|
||||||
|
Size -> binary_to_integer(Size)
|
||||||
|
end.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% Interval Funcs
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
format({emqx_user, Login, Password, IsSuperuser}) ->
|
||||||
|
#{login => Login,
|
||||||
|
password => Password,
|
||||||
|
is_superuser => IsSuperuser};
|
||||||
|
|
||||||
|
format([]) ->
|
||||||
|
#{};
|
||||||
|
|
||||||
|
format([{emqx_user, Login, Password, IsSuperuser}]) ->
|
||||||
|
#{login => Login,
|
||||||
|
password => Password,
|
||||||
|
is_superuser => IsSuperuser}.
|
||||||
|
|
||||||
|
validate([], []) ->
|
||||||
|
ok;
|
||||||
|
validate([K|Keys], [V|Values]) ->
|
||||||
|
case do_validation(K, V) of
|
||||||
|
false -> {error, K};
|
||||||
|
true -> validate(Keys, Values)
|
||||||
|
end.
|
||||||
|
|
||||||
|
do_validation(login, V) when is_binary(V)
|
||||||
|
andalso byte_size(V) > 0 ->
|
||||||
|
true;
|
||||||
|
do_validation(password, V) when is_binary(V)
|
||||||
|
andalso byte_size(V) > 0 ->
|
||||||
|
true;
|
||||||
|
do_validation(is_superuser, V) when is_boolean(V) ->
|
||||||
|
true;
|
||||||
|
do_validation(_, _) ->
|
||||||
|
false.
|
||||||
|
|
||||||
|
format_msg(Message)
|
||||||
|
when is_atom(Message);
|
||||||
|
is_binary(Message) -> Message;
|
||||||
|
|
||||||
|
format_msg(Message) when is_tuple(Message) ->
|
||||||
|
iolist_to_binary(io_lib:format("~p", [Message])).
|
||||||
|
|
||||||
|
-if(?OTP_RELEASE >= 23).
|
||||||
|
urldecode(S) ->
|
||||||
|
[{R, _}] = uri_string:dissect_query(S), R.
|
||||||
|
-else.
|
||||||
|
urldecode(S) ->
|
||||||
|
http_uri:decode(S).
|
||||||
|
-endif.
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed 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.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_auth_mnesia_app).
|
||||||
|
|
||||||
|
-behaviour(application).
|
||||||
|
|
||||||
|
-emqx_plugin(auth).
|
||||||
|
|
||||||
|
-include("emqx_auth_mnesia.hrl").
|
||||||
|
|
||||||
|
%% Application callbacks
|
||||||
|
-export([ start/2
|
||||||
|
, prep_stop/1
|
||||||
|
, stop/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Application callbacks
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
start(_StartType, _StartArgs) ->
|
||||||
|
{ok, Sup} = emqx_auth_mnesia_sup:start_link(),
|
||||||
|
emqx_ctl:register_command('mqtt-user', {emqx_auth_mnesia_cli, auth_cli}, []),
|
||||||
|
emqx_ctl:register_command('mqtt-acl', {emqx_auth_mnesia_cli, acl_cli}, []),
|
||||||
|
load_auth_hook(),
|
||||||
|
load_acl_hook(),
|
||||||
|
{ok, Sup}.
|
||||||
|
|
||||||
|
prep_stop(State) ->
|
||||||
|
emqx:unhook('client.authenticate', fun emqx_auth_mnesia:check/3),
|
||||||
|
emqx:unhook('client.check_acl', fun emqx_acl_mnesia:check_acl/5),
|
||||||
|
emqx_ctl:unregister_command('mqtt-user'),
|
||||||
|
emqx_ctl:unregister_command('mqtt-acl'),
|
||||||
|
State.
|
||||||
|
|
||||||
|
stop(_State) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
load_auth_hook() ->
|
||||||
|
DefaultUsers = application:get_env(?APP, userlist, []),
|
||||||
|
ok = emqx_auth_mnesia:init(DefaultUsers),
|
||||||
|
ok = emqx_auth_mnesia:register_metrics(),
|
||||||
|
Params = #{
|
||||||
|
hash_type => application:get_env(emqx_auth_mnesia, hash_type, sha256),
|
||||||
|
key_as => application:get_env(emqx_auth_mnesia, as, username)
|
||||||
|
},
|
||||||
|
emqx:hook('client.authenticate', fun emqx_auth_mnesia:check/3, [Params]).
|
||||||
|
|
||||||
|
load_acl_hook() ->
|
||||||
|
ok = emqx_acl_mnesia:init(),
|
||||||
|
ok = emqx_acl_mnesia:register_metrics(),
|
||||||
|
Params = #{
|
||||||
|
key_as => application:get_env(emqx_auth_mnesia, as, username)
|
||||||
|
},
|
||||||
|
emqx:hook('client.check_acl', fun emqx_acl_mnesia:check_acl/5, [Params]).
|
||||||
|
|
|
@ -0,0 +1,193 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed 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.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_auth_mnesia_cli).
|
||||||
|
|
||||||
|
-include("emqx_auth_mnesia.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
-define(TABLE, emqx_user).
|
||||||
|
%% Auth APIs
|
||||||
|
-export([ add_user/3
|
||||||
|
, update_user/3
|
||||||
|
, remove_user/1
|
||||||
|
, lookup_user/1
|
||||||
|
, all_users/0
|
||||||
|
]).
|
||||||
|
%% Acl APIs
|
||||||
|
-export([ add_acl/4
|
||||||
|
, remove_acl/2
|
||||||
|
, lookup_acl/1
|
||||||
|
, all_acls/0
|
||||||
|
]).
|
||||||
|
%% Cli
|
||||||
|
-export([ auth_cli/1
|
||||||
|
, acl_cli/1]).
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Auth APIs
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
%% @doc Add User
|
||||||
|
-spec(add_user(binary(), binary(), atom()) -> ok | {error, any()}).
|
||||||
|
add_user(Login, Password, IsSuperuser) ->
|
||||||
|
User = #emqx_user{login = Login, password = encrypted_data(Password), is_superuser = IsSuperuser},
|
||||||
|
ret(mnesia:transaction(fun insert_user/1, [User])).
|
||||||
|
|
||||||
|
insert_user(User = #emqx_user{login = Login}) ->
|
||||||
|
case mnesia:read(?TABLE, Login) of
|
||||||
|
[] -> mnesia:write(User);
|
||||||
|
[_|_] -> mnesia:abort(existed)
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @doc Update User
|
||||||
|
-spec(update_user(binary(), binary(), atom()) -> ok | {error, any()}).
|
||||||
|
update_user(Login, NewPassword, IsSuperuser) ->
|
||||||
|
User = #emqx_user{login = Login, password = encrypted_data(NewPassword), is_superuser = IsSuperuser},
|
||||||
|
ret(mnesia:transaction(fun do_update_user/1, [User])).
|
||||||
|
|
||||||
|
do_update_user(User = #emqx_user{login = Login}) ->
|
||||||
|
case mnesia:read(?TABLE, Login) of
|
||||||
|
[_|_] -> mnesia:write(User);
|
||||||
|
[] -> mnesia:abort(noexisted)
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @doc Lookup user by login
|
||||||
|
-spec(lookup_user(binary()) -> list()).
|
||||||
|
lookup_user(undefined) -> [];
|
||||||
|
lookup_user(Login) ->
|
||||||
|
case mnesia:dirty_read(?TABLE, Login) of
|
||||||
|
{error, Reason} ->
|
||||||
|
?LOG(error, "[Mnesia] do_check_user error: ~p~n", [Reason]),
|
||||||
|
[];
|
||||||
|
Re -> Re
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @doc Remove user
|
||||||
|
-spec(remove_user(binary()) -> ok | {error, any()}).
|
||||||
|
remove_user(Login) ->
|
||||||
|
ret(mnesia:transaction(fun mnesia:delete/1, [{?TABLE, Login}])).
|
||||||
|
|
||||||
|
%% @doc All logins
|
||||||
|
-spec(all_users() -> list()).
|
||||||
|
all_users() -> mnesia:dirty_all_keys(?TABLE).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Acl API
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
%% @doc Add Acls
|
||||||
|
-spec(add_acl(binary(), binary(), binary(), atom()) -> ok | {error, any()}).
|
||||||
|
add_acl(Login, Topic, Action, Allow) ->
|
||||||
|
Acls = #emqx_acl{login = Login, topic = Topic, action = Action, allow = Allow},
|
||||||
|
ret(mnesia:transaction(fun mnesia:write/1, [Acls])).
|
||||||
|
|
||||||
|
%% @doc Lookup acl by login
|
||||||
|
-spec(lookup_acl(binary()) -> list()).
|
||||||
|
lookup_acl(undefined) -> [];
|
||||||
|
lookup_acl(Login) ->
|
||||||
|
case mnesia:dirty_read(emqx_acl, Login) of
|
||||||
|
{error, Reason} ->
|
||||||
|
?LOG(error, "[Mnesia] do_check_acl error: ~p~n", [Reason]),
|
||||||
|
[];
|
||||||
|
Re -> Re
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @doc Remove acl
|
||||||
|
-spec(remove_acl(binary(), binary()) -> ok | {error, any()}).
|
||||||
|
remove_acl(Login, Topic) ->
|
||||||
|
[ ok = mnesia:dirty_delete_object(emqx_acl, #emqx_acl{login = Login, topic = Topic, action = Action, allow = Allow}) || [Action, Allow] <- ets:select(emqx_acl, [{{emqx_acl, Login, Topic,'$1','$2'}, [], ['$$']}])],
|
||||||
|
ok.
|
||||||
|
|
||||||
|
|
||||||
|
%% @doc All logins
|
||||||
|
-spec(all_acls() -> list()).
|
||||||
|
all_acls() -> mnesia:dirty_all_keys(emqx_acl).
|
||||||
|
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Internal functions
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
ret({atomic, ok}) -> ok;
|
||||||
|
ret({aborted, Error}) -> {error, Error}.
|
||||||
|
|
||||||
|
encrypted_data(Password) ->
|
||||||
|
HashType = application:get_env(emqx_auth_mnesia, hash_type, sha256),
|
||||||
|
emqx_passwd:hash(HashType, Password).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Auth APIs
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
%% User
|
||||||
|
auth_cli(["add", Login, Password, IsSuperuser]) ->
|
||||||
|
case add_user(iolist_to_binary(Login), iolist_to_binary(Password), IsSuperuser) of
|
||||||
|
ok -> emqx_ctl:print("ok~n");
|
||||||
|
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
|
||||||
|
end;
|
||||||
|
|
||||||
|
auth_cli(["update", Login, NewPassword, IsSuperuser]) ->
|
||||||
|
case update_user(iolist_to_binary(Login), iolist_to_binary(NewPassword), IsSuperuser) of
|
||||||
|
ok -> emqx_ctl:print("ok~n");
|
||||||
|
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
|
||||||
|
end;
|
||||||
|
|
||||||
|
auth_cli(["del", Login]) ->
|
||||||
|
case remove_user(iolist_to_binary(Login)) of
|
||||||
|
ok -> emqx_ctl:print("ok~n");
|
||||||
|
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
|
||||||
|
end;
|
||||||
|
|
||||||
|
auth_cli(["show", P]) ->
|
||||||
|
[emqx_ctl:print("User(login = ~p is_super = ~p)~n", [Login, IsSuperuser])
|
||||||
|
|| {_, Login, _Password, IsSuperuser} <- lookup_user(iolist_to_binary(P))];
|
||||||
|
|
||||||
|
auth_cli(["list"]) ->
|
||||||
|
[emqx_ctl:print("User(login = ~p)~n",[E])
|
||||||
|
|| E <- all_users()];
|
||||||
|
|
||||||
|
auth_cli(_) ->
|
||||||
|
emqx_ctl:usage([{"mqtt-user add <Login> <Password> <IsSuper>", "Add user"},
|
||||||
|
{"mqtt-user update <Login> <NewPassword> <IsSuper>", "Update user"},
|
||||||
|
{"mqtt-user delete <Login>", "Delete user"},
|
||||||
|
{"mqtt-user show <Login>", "Lookup user detail"},
|
||||||
|
{"mqtt-user list", "List all users"}]).
|
||||||
|
|
||||||
|
%% Acl
|
||||||
|
acl_cli(["add", Login, Topic, Action, Allow]) ->
|
||||||
|
case add_acl(iolist_to_binary(Login), iolist_to_binary(Topic), iolist_to_binary(Action), Allow) of
|
||||||
|
ok -> emqx_ctl:print("ok~n");
|
||||||
|
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
|
||||||
|
end;
|
||||||
|
|
||||||
|
acl_cli(["del", Login, Topic])->
|
||||||
|
case remove_acl(iolist_to_binary(Login), iolist_to_binary(Topic)) of
|
||||||
|
ok -> emqx_ctl:print("ok~n");
|
||||||
|
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
|
||||||
|
end;
|
||||||
|
|
||||||
|
acl_cli(["show", P]) ->
|
||||||
|
[emqx_ctl:print("Acl(login = ~p topic = ~p action = ~p allow = ~p)~n",[Login, Topic, Action, Allow])
|
||||||
|
|| {_, Login, Topic, Action, Allow} <- lookup_acl(iolist_to_binary(P)) ];
|
||||||
|
|
||||||
|
acl_cli(["list"]) ->
|
||||||
|
[emqx_ctl:print("Acl(login = ~p)~n",[E])
|
||||||
|
|| E <- all_acls() ];
|
||||||
|
|
||||||
|
acl_cli(_) ->
|
||||||
|
emqx_ctl:usage([{"mqtt-acl add <Login> <Topic> <Action> <Allow>", "Add acl"},
|
||||||
|
{"mqtt-acl show <Login>", "Lookup acl detail"},
|
||||||
|
{"mqtt-acl del <Login>", "Delete acl"},
|
||||||
|
{"mqtt-acl list","List all acls"}]).
|
|
@ -0,0 +1,36 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed 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.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_auth_mnesia_sup).
|
||||||
|
|
||||||
|
-behaviour(supervisor).
|
||||||
|
|
||||||
|
-include("emqx_auth_mnesia.hrl").
|
||||||
|
|
||||||
|
-export([start_link/0]).
|
||||||
|
|
||||||
|
%% Supervisor callbacks
|
||||||
|
-export([init/1]).
|
||||||
|
|
||||||
|
start_link() ->
|
||||||
|
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Supervisor callbacks
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
init([]) ->
|
||||||
|
{ok, {{one_for_one, 10, 100}, []}}.
|
|
@ -0,0 +1,279 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed 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.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_acl_mnesia_SUITE).
|
||||||
|
|
||||||
|
-compile(export_all).
|
||||||
|
|
||||||
|
-include("emqx_auth_mnesia.hrl").
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
-include_lib("common_test/include/ct.hrl").
|
||||||
|
|
||||||
|
-import(emqx_ct_http, [ request_api/3
|
||||||
|
, request_api/5
|
||||||
|
, get_http_data/1
|
||||||
|
, create_default_app/0
|
||||||
|
, default_auth_header/0
|
||||||
|
]).
|
||||||
|
|
||||||
|
-define(HOST, "http://127.0.0.1:8081/").
|
||||||
|
-define(API_VERSION, "v4").
|
||||||
|
-define(BASE_PATH, "api").
|
||||||
|
|
||||||
|
all() ->
|
||||||
|
emqx_ct:all(?MODULE).
|
||||||
|
|
||||||
|
groups() ->
|
||||||
|
[].
|
||||||
|
|
||||||
|
init_per_suite(Config) ->
|
||||||
|
emqx_ct_helpers:start_apps([emqx_management, emqx_auth_mnesia], fun set_special_configs/1),
|
||||||
|
create_default_app(),
|
||||||
|
Config.
|
||||||
|
|
||||||
|
end_per_suite(_Config) ->
|
||||||
|
emqx_ct_helpers:stop_apps([emqx_management, emqx_auth_mnesia]).
|
||||||
|
|
||||||
|
init_per_testcase(t_check_acl_as_clientid, Config) ->
|
||||||
|
emqx:hook('client.check_acl', fun emqx_acl_mnesia:check_acl/5, [#{key_as => clientid}]),
|
||||||
|
Config;
|
||||||
|
|
||||||
|
init_per_testcase(_, Config) ->
|
||||||
|
emqx:hook('client.check_acl', fun emqx_acl_mnesia:check_acl/5, [#{key_as => username}]),
|
||||||
|
Config.
|
||||||
|
|
||||||
|
end_per_testcase(_, Config) ->
|
||||||
|
emqx:unhook('client.check_acl', fun emqx_acl_mnesia:check_acl/5),
|
||||||
|
Config.
|
||||||
|
|
||||||
|
set_special_configs(emqx) ->
|
||||||
|
application:set_env(emqx, allow_anonymous, true),
|
||||||
|
application:set_env(emqx, enable_acl_cache, false),
|
||||||
|
LoadedPluginPath = filename:join(["test", "emqx_SUITE_data", "loaded_plugins"]),
|
||||||
|
application:set_env(emqx, plugins_loaded_file,
|
||||||
|
emqx_ct_helpers:deps_path(emqx, LoadedPluginPath));
|
||||||
|
|
||||||
|
set_special_configs(_App) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% Testcases
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
t_management(_Config) ->
|
||||||
|
clean_all_acls(),
|
||||||
|
?assertEqual("Acl with Mnesia", emqx_acl_mnesia:description()),
|
||||||
|
?assertEqual([], emqx_auth_mnesia_cli:all_acls()),
|
||||||
|
|
||||||
|
ok = emqx_auth_mnesia_cli:add_acl(<<"test_username">>, <<"Topic/A">>, <<"sub">>, true),
|
||||||
|
ok = emqx_auth_mnesia_cli:add_acl(<<"test_username">>, <<"Topic/B">>, <<"pub">>, true),
|
||||||
|
ok = emqx_auth_mnesia_cli:add_acl(<<"test_username">>, <<"Topic/C">>, <<"pubsub">>, true),
|
||||||
|
|
||||||
|
?assertEqual([{emqx_acl,<<"test_username">>,<<"Topic/A">>,<<"sub">>, true},
|
||||||
|
{emqx_acl,<<"test_username">>,<<"Topic/B">>,<<"pub">>, true},
|
||||||
|
{emqx_acl,<<"test_username">>,<<"Topic/C">>,<<"pubsub">>, true}],emqx_auth_mnesia_cli:lookup_acl(<<"test_username">>)),
|
||||||
|
ok = emqx_auth_mnesia_cli:remove_acl(<<"test_username">>, <<"Topic/A">>),
|
||||||
|
?assertEqual([{emqx_acl,<<"test_username">>,<<"Topic/B">>,<<"pub">>, true},
|
||||||
|
{emqx_acl,<<"test_username">>,<<"Topic/C">>,<<"pubsub">>, true}], emqx_auth_mnesia_cli:lookup_acl(<<"test_username">>)),
|
||||||
|
|
||||||
|
|
||||||
|
ok = emqx_auth_mnesia_cli:add_acl(<<"$all">>, <<"Topic/A">>, <<"sub">>, true),
|
||||||
|
ok = emqx_auth_mnesia_cli:add_acl(<<"$all">>, <<"Topic/B">>, <<"pub">>, true),
|
||||||
|
ok = emqx_auth_mnesia_cli:add_acl(<<"$all">>, <<"Topic/C">>, <<"pubsub">>, true),
|
||||||
|
|
||||||
|
?assertEqual([{emqx_acl,<<"$all">>,<<"Topic/A">>,<<"sub">>, true},
|
||||||
|
{emqx_acl,<<"$all">>,<<"Topic/B">>,<<"pub">>, true},
|
||||||
|
{emqx_acl,<<"$all">>,<<"Topic/C">>,<<"pubsub">>, true}],emqx_auth_mnesia_cli:lookup_acl(<<"$all">>)),
|
||||||
|
ok = emqx_auth_mnesia_cli:remove_acl(<<"$all">>, <<"Topic/A">>),
|
||||||
|
?assertEqual([{emqx_acl,<<"$all">>,<<"Topic/B">>,<<"pub">>, true},
|
||||||
|
{emqx_acl,<<"$all">>,<<"Topic/C">>,<<"pubsub">>, true}], emqx_auth_mnesia_cli:lookup_acl(<<"$all">>)).
|
||||||
|
|
||||||
|
t_check_acl_as_clientid(_) ->
|
||||||
|
clean_all_acls(),
|
||||||
|
emqx_modules:load_module(emqx_mod_acl_internal, false),
|
||||||
|
|
||||||
|
User1 = #{zone => external, clientid => <<"test_clientid">>},
|
||||||
|
User2 = #{zone => external, clientid => <<"no_exist">>},
|
||||||
|
|
||||||
|
ok = emqx_auth_mnesia_cli:add_acl(<<"test_clientid">>, <<"#">>, <<"sub">>, false),
|
||||||
|
ok = emqx_auth_mnesia_cli:add_acl(<<"test_clientid">>, <<"+/A">>, <<"pub">>, false),
|
||||||
|
ok = emqx_auth_mnesia_cli:add_acl(<<"test_clientid">>, <<"Topic/A/B">>, <<"pubsub">>, true),
|
||||||
|
|
||||||
|
deny = emqx_access_control:check_acl(User1, subscribe, <<"Any">>),
|
||||||
|
deny = emqx_access_control:check_acl(User1, publish, <<"Any/A">>),
|
||||||
|
allow = emqx_access_control:check_acl(User1, publish, <<"Any/C">>),
|
||||||
|
allow = emqx_access_control:check_acl(User1, publish, <<"Topic/A/B">>),
|
||||||
|
|
||||||
|
allow = emqx_access_control:check_acl(User2, subscribe, <<"Topic/C">>),
|
||||||
|
allow = emqx_access_control:check_acl(User2, publish, <<"Topic/D">>).
|
||||||
|
|
||||||
|
t_check_acl_as_username(_Config) ->
|
||||||
|
clean_all_acls(),
|
||||||
|
emqx_modules:load_module(emqx_mod_acl_internal, false),
|
||||||
|
|
||||||
|
User1 = #{zone => external, username => <<"test_username">>},
|
||||||
|
User2 = #{zone => external, username => <<"no_exist">>},
|
||||||
|
|
||||||
|
ok = emqx_auth_mnesia_cli:add_acl(<<"test_username">>, <<"Topic/A">>, <<"sub">>, true),
|
||||||
|
ok = emqx_auth_mnesia_cli:add_acl(<<"test_username">>, <<"Topic/B">>, <<"pub">>, true),
|
||||||
|
ok = emqx_auth_mnesia_cli:add_acl(<<"test_username">>, <<"Topic/A/B">>, <<"pubsub">>, false),
|
||||||
|
allow = emqx_access_control:check_acl(User1, subscribe, <<"Topic/A">>),
|
||||||
|
allow = emqx_access_control:check_acl(User1, subscribe, <<"Topic/B">>),
|
||||||
|
deny = emqx_access_control:check_acl(User1, subscribe, <<"Topic/A/B">>),
|
||||||
|
allow = emqx_access_control:check_acl(User1, publish, <<"Topic/A">>),
|
||||||
|
allow = emqx_access_control:check_acl(User1, publish, <<"Topic/B">>),
|
||||||
|
deny = emqx_access_control:check_acl(User1, publish, <<"Topic/A/B">>),
|
||||||
|
|
||||||
|
allow = emqx_access_control:check_acl(User2, subscribe, <<"Topic/C">>),
|
||||||
|
allow = emqx_access_control:check_acl(User2, publish, <<"Topic/D">>).
|
||||||
|
|
||||||
|
t_check_acl_as_all(_) ->
|
||||||
|
clean_all_acls(),
|
||||||
|
emqx_modules:load_module(emqx_mod_acl_internal, false),
|
||||||
|
|
||||||
|
ok = emqx_auth_mnesia_cli:add_acl(<<"$all">>, <<"Topic/A">>, <<"sub">>, false),
|
||||||
|
ok = emqx_auth_mnesia_cli:add_acl(<<"$all">>, <<"Topic/B">>, <<"pub">>, false),
|
||||||
|
ok = emqx_auth_mnesia_cli:add_acl(<<"$all">>, <<"Topic/A/B">>, <<"pubsub">>, true),
|
||||||
|
|
||||||
|
User1 = #{zone => external, username => <<"test_username">>},
|
||||||
|
User2 = #{zone => external, username => <<"no_exist">>},
|
||||||
|
|
||||||
|
ok = emqx_auth_mnesia_cli:add_acl(<<"test_username">>, <<"Topic/A">>, <<"sub">>, true),
|
||||||
|
ok = emqx_auth_mnesia_cli:add_acl(<<"test_username">>, <<"Topic/B">>, <<"pub">>, true),
|
||||||
|
ok = emqx_auth_mnesia_cli:add_acl(<<"test_username">>, <<"Topic/A/B">>, <<"pubsub">>, false),
|
||||||
|
|
||||||
|
allow = emqx_access_control:check_acl(User1, subscribe, <<"Topic/A">>),
|
||||||
|
allow = emqx_access_control:check_acl(User1, subscribe, <<"Topic/B">>),
|
||||||
|
deny = emqx_access_control:check_acl(User1, subscribe, <<"Topic/A/B">>),
|
||||||
|
allow = emqx_access_control:check_acl(User1, publish, <<"Topic/A">>),
|
||||||
|
allow = emqx_access_control:check_acl(User1, publish, <<"Topic/B">>),
|
||||||
|
deny = emqx_access_control:check_acl(User1, publish, <<"Topic/A/B">>),
|
||||||
|
|
||||||
|
deny = emqx_access_control:check_acl(User2, subscribe, <<"Topic/A">>),
|
||||||
|
deny = emqx_access_control:check_acl(User2, publish, <<"Topic/B">>),
|
||||||
|
allow = emqx_access_control:check_acl(User2, subscribe, <<"Topic/A/B">>),
|
||||||
|
allow = emqx_access_control:check_acl(User2, publish, <<"Topic/A/B">>),
|
||||||
|
allow = emqx_access_control:check_acl(User2, subscribe, <<"Topic/C">>),
|
||||||
|
allow = emqx_access_control:check_acl(User2, publish, <<"Topic/D">>).
|
||||||
|
|
||||||
|
t_rest_api(_Config) ->
|
||||||
|
clean_all_acls(),
|
||||||
|
|
||||||
|
{ok, Result} = request_http_rest_list(),
|
||||||
|
[] = get_http_data(Result),
|
||||||
|
|
||||||
|
Params = #{<<"login">> => <<"test_username">>, <<"topic">> => <<"Topic/A">>, <<"action">> => <<"pubsub">>, <<"allow">> => true},
|
||||||
|
{ok, _} = request_http_rest_add(Params),
|
||||||
|
{ok, Result1} = request_http_rest_lookup(<<"test_username">>),
|
||||||
|
#{<<"login">> := <<"test_username">>, <<"topic">> := <<"Topic/A">>, <<"action">> := <<"pubsub">>, <<"allow">> := true} = get_http_data(Result1),
|
||||||
|
|
||||||
|
Params1 = [
|
||||||
|
#{<<"login">> => <<"$all">>, <<"topic">> => <<"+/A">>, <<"action">> => <<"pub">>, <<"allow">> => true},
|
||||||
|
#{<<"login">> => <<"test_username">>, <<"topic">> => <<"+/A">>, <<"action">> => <<"pub">>, <<"allow">> => true},
|
||||||
|
#{<<"login">> => <<"test_username/1">>, <<"topic">> => <<"#">>, <<"action">> => <<"sub">>, <<"allow">> => true},
|
||||||
|
#{<<"login">> => <<"test_username/2">>, <<"topic">> => <<"+/A">>, <<"action">> => <<"error_format">>, <<"allow">> => true}
|
||||||
|
],
|
||||||
|
{ok, Result2} = request_http_rest_add(Params1),
|
||||||
|
#{
|
||||||
|
<<"$all">> := <<"ok">>,
|
||||||
|
<<"test_username">> := <<"ok">>,
|
||||||
|
<<"test_username/1">> := <<"ok">>,
|
||||||
|
<<"test_username/2">> := <<"{error,action}">>
|
||||||
|
} = get_http_data(Result2),
|
||||||
|
|
||||||
|
{ok, Result3} = request_http_rest_lookup(<<"test_username">>),
|
||||||
|
[#{<<"login">> := <<"test_username">>, <<"topic">> := <<"+/A">>, <<"action">> := <<"pub">>, <<"allow">> := true},
|
||||||
|
#{<<"login">> := <<"test_username">>, <<"topic">> := <<"Topic/A">>, <<"action">> := <<"pubsub">>, <<"allow">> := true}]
|
||||||
|
= get_http_data(Result3),
|
||||||
|
|
||||||
|
{ok, Result4} = request_http_rest_lookup(<<"$all">>),
|
||||||
|
#{<<"login">> := <<"$all">>, <<"topic">> := <<"+/A">>, <<"action">> := <<"pub">>, <<"allow">> := true}
|
||||||
|
= get_http_data(Result4),
|
||||||
|
|
||||||
|
{ok, _} = request_http_rest_delete(<<"$all">>, <<"+/A">>),
|
||||||
|
{ok, _} = request_http_rest_delete(<<"test_username">>, <<"+/A">>),
|
||||||
|
{ok, _} = request_http_rest_delete(<<"test_username">>, <<"Topic/A">>),
|
||||||
|
{ok, _} = request_http_rest_delete(<<"test_username/1">>, <<"#">>),
|
||||||
|
{ok, Result5} = request_http_rest_list(),
|
||||||
|
[] = get_http_data(Result5).
|
||||||
|
|
||||||
|
|
||||||
|
t_run_command(_) ->
|
||||||
|
clean_all_acls(),
|
||||||
|
?assertEqual(ok, emqx_ctl:run_command(["mqtt-acl", "add", "TestUser", "Topic/A", "sub", true])),
|
||||||
|
?assertEqual([{emqx_acl,<<"TestUser">>,<<"Topic/A">>,<<"sub">>, true}],emqx_auth_mnesia_cli:lookup_acl(<<"TestUser">>)),
|
||||||
|
|
||||||
|
?assertEqual(ok, emqx_ctl:run_command(["mqtt-acl", "del", "TestUser", "Topic/A"])),
|
||||||
|
?assertEqual([],emqx_auth_mnesia_cli:lookup_acl(<<"TestUser">>)),
|
||||||
|
|
||||||
|
?assertEqual(ok, emqx_ctl:run_command(["mqtt-acl", "show", "TestUser"])),
|
||||||
|
?assertEqual(ok, emqx_ctl:run_command(["mqtt-acl", "list"])),
|
||||||
|
?assertEqual(ok, emqx_ctl:run_command(["mqtt-acl"])).
|
||||||
|
|
||||||
|
t_cli(_) ->
|
||||||
|
meck:new(emqx_ctl, [non_strict, passthrough]),
|
||||||
|
meck:expect(emqx_ctl, print, fun(Arg) -> emqx_ctl:format(Arg) end),
|
||||||
|
meck:expect(emqx_ctl, print, fun(Msg, Arg) -> emqx_ctl:format(Msg, Arg) end),
|
||||||
|
meck:expect(emqx_ctl, usage, fun(Usages) -> emqx_ctl:format_usage(Usages) end),
|
||||||
|
meck:expect(emqx_ctl, usage, fun(Cmd, Descr) -> emqx_ctl:format_usage(Cmd, Descr) end),
|
||||||
|
|
||||||
|
clean_all_acls(),
|
||||||
|
?assertMatch({match, _}, re:run(emqx_auth_mnesia_cli:acl_cli(["add", "TestUser", "Topic/A", "sub", true]), "ok")),
|
||||||
|
?assertMatch(["Acl(login = <<\"TestUser\">> topic = <<\"Topic/A\">> action = <<\"sub\">> allow = true)\n"], emqx_auth_mnesia_cli:acl_cli(["show", "TestUser"])),
|
||||||
|
?assertMatch(["Acl(login = <<\"TestUser\">>)\n"], emqx_auth_mnesia_cli:acl_cli(["list"])),
|
||||||
|
|
||||||
|
?assertMatch({match, _}, re:run(emqx_auth_mnesia_cli:acl_cli(["del", "TestUser", "Topic/A"]), "ok")),
|
||||||
|
?assertMatch([], emqx_auth_mnesia_cli:acl_cli(["show", "TestUser"])),
|
||||||
|
?assertMatch([], emqx_auth_mnesia_cli:acl_cli(["list"])),
|
||||||
|
|
||||||
|
?assertMatch({match, _}, re:run(emqx_auth_mnesia_cli:acl_cli([]), "mqtt-acl")),
|
||||||
|
|
||||||
|
meck:unload(emqx_ctl).
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% Helpers
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
clean_all_acls() ->
|
||||||
|
[ mnesia:dirty_delete({emqx_acl, Login})
|
||||||
|
|| Login <- mnesia:dirty_all_keys(emqx_acl)].
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% HTTP Request
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
request_http_rest_list() ->
|
||||||
|
request_api(get, uri(), default_auth_header()).
|
||||||
|
|
||||||
|
request_http_rest_lookup(Login) ->
|
||||||
|
request_api(get, uri([Login]), default_auth_header()).
|
||||||
|
|
||||||
|
request_http_rest_add(Params) ->
|
||||||
|
request_api(post, uri(), [], default_auth_header(), Params).
|
||||||
|
|
||||||
|
request_http_rest_delete(Login, Topic) ->
|
||||||
|
request_api(delete, uri([Login, Topic]), default_auth_header()).
|
||||||
|
|
||||||
|
uri() -> uri([]).
|
||||||
|
uri(Parts) when is_list(Parts) ->
|
||||||
|
NParts = [b2l(E) || E <- Parts],
|
||||||
|
?HOST ++ filename:join([?BASE_PATH, ?API_VERSION, "mqtt_acl"| NParts]).
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
b2l(B) when is_binary(B) ->
|
||||||
|
http_uri:encode(binary_to_list(B));
|
||||||
|
b2l(L) when is_list(L) ->
|
||||||
|
http_uri:encode(L).
|
|
@ -0,0 +1,259 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed 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.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_auth_mnesia_SUITE).
|
||||||
|
|
||||||
|
-compile(export_all).
|
||||||
|
|
||||||
|
-include("emqx_auth_mnesia.hrl").
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
-include_lib("common_test/include/ct.hrl").
|
||||||
|
|
||||||
|
-import(emqx_ct_http, [ request_api/3
|
||||||
|
, request_api/5
|
||||||
|
, get_http_data/1
|
||||||
|
, create_default_app/0
|
||||||
|
, default_auth_header/0
|
||||||
|
]).
|
||||||
|
|
||||||
|
-define(HOST, "http://127.0.0.1:8081/").
|
||||||
|
-define(API_VERSION, "v4").
|
||||||
|
-define(BASE_PATH, "api").
|
||||||
|
|
||||||
|
all() ->
|
||||||
|
emqx_ct:all(?MODULE).
|
||||||
|
|
||||||
|
groups() ->
|
||||||
|
[].
|
||||||
|
|
||||||
|
init_per_suite(Config) ->
|
||||||
|
ok = emqx_ct_helpers:start_apps([emqx_management, emqx_auth_mnesia], fun set_special_configs/1),
|
||||||
|
create_default_app(),
|
||||||
|
Config.
|
||||||
|
|
||||||
|
end_per_suite(_Config) ->
|
||||||
|
emqx_ct_helpers:stop_apps([emqx_management, emqx_auth_mnesia]).
|
||||||
|
|
||||||
|
init_per_testcase(t_check_as_clientid, Config) ->
|
||||||
|
Params = #{
|
||||||
|
hash_type => application:get_env(emqx_auth_mnesia, hash_type, sha256),
|
||||||
|
key_as => clientid
|
||||||
|
},
|
||||||
|
emqx:hook('client.authenticate', fun emqx_auth_mnesia:check/3, [Params]),
|
||||||
|
Config;
|
||||||
|
|
||||||
|
init_per_testcase(_, Config) ->
|
||||||
|
Params = #{
|
||||||
|
hash_type => application:get_env(emqx_auth_mnesia, hash_type, sha256),
|
||||||
|
key_as => username
|
||||||
|
},
|
||||||
|
emqx:hook('client.authenticate', fun emqx_auth_mnesia:check/3, [Params]),
|
||||||
|
Config.
|
||||||
|
|
||||||
|
end_per_suite(_, Config) ->
|
||||||
|
emqx:unhook('client.authenticate', fun emqx_auth_mnesia:check/3),
|
||||||
|
Config.
|
||||||
|
|
||||||
|
set_special_configs(emqx) ->
|
||||||
|
application:set_env(emqx, allow_anonymous, true),
|
||||||
|
application:set_env(emqx, enable_acl_cache, false),
|
||||||
|
LoadedPluginPath = filename:join(["test", "emqx_SUITE_data", "loaded_plugins"]),
|
||||||
|
application:set_env(emqx, plugins_loaded_file,
|
||||||
|
emqx_ct_helpers:deps_path(emqx, LoadedPluginPath));
|
||||||
|
|
||||||
|
set_special_configs(_App) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% Testcases
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
t_check_as_username(_Config) ->
|
||||||
|
clean_all_users(),
|
||||||
|
|
||||||
|
ok = emqx_auth_mnesia_cli:add_user(<<"test_username">>, <<"password">>, true),
|
||||||
|
{error, existed} = emqx_auth_mnesia_cli:add_user(<<"test_username">>, <<"password">>, true),
|
||||||
|
|
||||||
|
ok = emqx_auth_mnesia_cli:update_user(<<"test_username">>, <<"new_password">>, false),
|
||||||
|
{error,noexisted} = emqx_auth_mnesia_cli:update_user(<<"no_existed_user">>, <<"password">>, true),
|
||||||
|
|
||||||
|
[<<"test_username">>] = emqx_auth_mnesia_cli:all_users(),
|
||||||
|
[{emqx_user, <<"test_username">>, _HashedPass, false}] =
|
||||||
|
emqx_auth_mnesia_cli:lookup_user(<<"test_username">>),
|
||||||
|
|
||||||
|
User1 = #{username => <<"test_username">>,
|
||||||
|
password => <<"new_password">>,
|
||||||
|
zone => external},
|
||||||
|
|
||||||
|
{ok, #{is_superuser := false,
|
||||||
|
auth_result := success,
|
||||||
|
anonymous := false}} = emqx_access_control:authenticate(User1),
|
||||||
|
|
||||||
|
{error,password_error} = emqx_access_control:authenticate(User1#{password => <<"error_password">>}),
|
||||||
|
|
||||||
|
ok = emqx_auth_mnesia_cli:remove_user(<<"test_username">>),
|
||||||
|
{ok, #{auth_result := success,
|
||||||
|
anonymous := true }} = emqx_access_control:authenticate(User1).
|
||||||
|
|
||||||
|
t_check_as_clientid(_Config) ->
|
||||||
|
clean_all_users(),
|
||||||
|
|
||||||
|
ok = emqx_auth_mnesia_cli:add_user(<<"test_clientid">>, <<"password">>, false),
|
||||||
|
{error, existed} = emqx_auth_mnesia_cli:add_user(<<"test_clientid">>, <<"password">>, false),
|
||||||
|
|
||||||
|
ok = emqx_auth_mnesia_cli:update_user(<<"test_clientid">>, <<"new_password">>, true),
|
||||||
|
{error,noexisted} = emqx_auth_mnesia_cli:update_user(<<"no_existed_user">>, <<"password">>, true),
|
||||||
|
|
||||||
|
[<<"test_clientid">>] = emqx_auth_mnesia_cli:all_users(),
|
||||||
|
[{emqx_user, <<"test_clientid">>, _HashedPass, true}] =
|
||||||
|
emqx_auth_mnesia_cli:lookup_user(<<"test_clientid">>),
|
||||||
|
|
||||||
|
User1 = #{clientid => <<"test_clientid">>,
|
||||||
|
password => <<"new_password">>,
|
||||||
|
zone => external},
|
||||||
|
|
||||||
|
{ok, #{is_superuser := true,
|
||||||
|
auth_result := success,
|
||||||
|
anonymous := false}} = emqx_access_control:authenticate(User1),
|
||||||
|
|
||||||
|
{error,password_error} = emqx_access_control:authenticate(User1#{password => <<"error_password">>}),
|
||||||
|
|
||||||
|
ok = emqx_auth_mnesia_cli:remove_user(<<"test_clientid">>),
|
||||||
|
{ok, #{auth_result := success,
|
||||||
|
anonymous := true }} = emqx_access_control:authenticate(User1).
|
||||||
|
|
||||||
|
t_rest_api(_Config) ->
|
||||||
|
clean_all_users(),
|
||||||
|
|
||||||
|
{ok, Result1} = request_http_rest_list(),
|
||||||
|
[] = get_http_data(Result1),
|
||||||
|
|
||||||
|
Params = #{<<"login">> => <<"test_username">>, <<"password">> => <<"password">>, <<"is_superuser">> => true},
|
||||||
|
{ok, _} = request_http_rest_add(Params),
|
||||||
|
|
||||||
|
Params1 = [
|
||||||
|
#{<<"login">> => <<"test_username">>, <<"password">> => <<"password">>, <<"is_superuser">> => true},
|
||||||
|
#{<<"login">> => <<"test_username/1">>, <<"password">> => <<"password">>, <<"is_superuser">> => error_format},
|
||||||
|
#{<<"login">> => <<"test_username/2">>, <<"password">> => <<"password">>, <<"is_superuser">> => true}
|
||||||
|
],
|
||||||
|
{ok, Result2} = request_http_rest_add(Params1),
|
||||||
|
#{
|
||||||
|
<<"test_username">> := <<"{error,existed}">>,
|
||||||
|
<<"test_username/1">> := <<"{error,is_superuser}">>,
|
||||||
|
<<"test_username/2">> := <<"ok">>
|
||||||
|
} = get_http_data(Result2),
|
||||||
|
|
||||||
|
{ok, Result3} = request_http_rest_lookup(<<"test_username">>),
|
||||||
|
#{<<"login">> := <<"test_username">>, <<"is_superuser">> := true} = get_http_data(Result3),
|
||||||
|
|
||||||
|
{ok, _} = request_http_rest_update(<<"test_username">>, <<"new_password">>, error_format),
|
||||||
|
{ok, _} = request_http_rest_update(<<"error_username">>, <<"new_password">>, false),
|
||||||
|
|
||||||
|
{ok, _} = request_http_rest_update(<<"test_username">>, <<"new_password">>, false),
|
||||||
|
{ok, Result4} = request_http_rest_lookup(<<"test_username">>),
|
||||||
|
#{<<"login">> := <<"test_username">>, <<"is_superuser">> := false} = get_http_data(Result4),
|
||||||
|
|
||||||
|
User1 = #{username => <<"test_username">>,
|
||||||
|
password => <<"new_password">>,
|
||||||
|
zone => external},
|
||||||
|
|
||||||
|
{ok, #{is_superuser := false,
|
||||||
|
auth_result := success,
|
||||||
|
anonymous := false}} = emqx_access_control:authenticate(User1),
|
||||||
|
|
||||||
|
{ok, _} = request_http_rest_delete(<<"test_username">>),
|
||||||
|
{ok, #{auth_result := success,
|
||||||
|
anonymous := true }} = emqx_access_control:authenticate(User1).
|
||||||
|
|
||||||
|
t_run_command(_) ->
|
||||||
|
clean_all_users(),
|
||||||
|
?assertEqual(ok, emqx_ctl:run_command(["mqtt-user", "add", "TestUser", "Password", false])),
|
||||||
|
?assertMatch([{emqx_user, <<"TestUser">>, _, false}], emqx_auth_mnesia_cli:lookup_user(<<"TestUser">>)),
|
||||||
|
|
||||||
|
?assertEqual(ok, emqx_ctl:run_command(["mqtt-user", "update", "TestUser", "NewPassword", true])),
|
||||||
|
?assertMatch([{emqx_user, <<"TestUser">>, _, true}], emqx_auth_mnesia_cli:lookup_user(<<"TestUser">>)),
|
||||||
|
|
||||||
|
?assertEqual(ok, emqx_ctl:run_command(["mqtt-user", "del", "TestUser"])),
|
||||||
|
?assertMatch([], emqx_auth_mnesia_cli:lookup_user(<<"TestUser">>)),
|
||||||
|
|
||||||
|
?assertEqual(ok, emqx_ctl:run_command(["mqtt-user", "show", "TestUser"])),
|
||||||
|
?assertEqual(ok, emqx_ctl:run_command(["mqtt-user", "list"])),
|
||||||
|
?assertEqual(ok, emqx_ctl:run_command(["mqtt-user"])).
|
||||||
|
|
||||||
|
t_cli(_) ->
|
||||||
|
meck:new(emqx_ctl, [non_strict, passthrough]),
|
||||||
|
meck:expect(emqx_ctl, print, fun(Arg) -> emqx_ctl:format(Arg) end),
|
||||||
|
meck:expect(emqx_ctl, print, fun(Msg, Arg) -> emqx_ctl:format(Msg, Arg) end),
|
||||||
|
meck:expect(emqx_ctl, usage, fun(Usages) -> emqx_ctl:format_usage(Usages) end),
|
||||||
|
meck:expect(emqx_ctl, usage, fun(Cmd, Descr) -> emqx_ctl:format_usage(Cmd, Descr) end),
|
||||||
|
|
||||||
|
clean_all_users(),
|
||||||
|
|
||||||
|
?assertMatch({match, _}, re:run(emqx_auth_mnesia_cli:auth_cli(["add", "TestUser", "Password", true]), "ok")),
|
||||||
|
?assertMatch({match, _}, re:run(emqx_auth_mnesia_cli:auth_cli(["add", "TestUser", "Password", true]), "Error")),
|
||||||
|
|
||||||
|
?assertMatch({match, _}, re:run(emqx_auth_mnesia_cli:auth_cli(["update", "NoExisted", "Password", false]), "Error")),
|
||||||
|
?assertMatch({match, _}, re:run(emqx_auth_mnesia_cli:auth_cli(["update", "TestUser", "Password", false]), "ok")),
|
||||||
|
|
||||||
|
?assertMatch(["User(login = <<\"TestUser\">> is_super = false)\n"], emqx_auth_mnesia_cli:auth_cli(["show", "TestUser"])),
|
||||||
|
?assertMatch(["User(login = <<\"TestUser\">>)\n"], emqx_auth_mnesia_cli:auth_cli(["list"])),
|
||||||
|
|
||||||
|
?assertMatch({match, _}, re:run(emqx_auth_mnesia_cli:auth_cli(["del", "TestUser"]), "ok")),
|
||||||
|
?assertMatch([], emqx_auth_mnesia_cli:auth_cli(["show", "TestUser"])),
|
||||||
|
?assertMatch([], emqx_auth_mnesia_cli:auth_cli(["list"])),
|
||||||
|
|
||||||
|
?assertMatch({match, _}, re:run(emqx_auth_mnesia_cli:auth_cli([]), "mqtt-user")),
|
||||||
|
|
||||||
|
meck:unload(emqx_ctl).
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% Helpers
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
clean_all_users() ->
|
||||||
|
[ mnesia:dirty_delete({emqx_user, Login})
|
||||||
|
|| Login <- mnesia:dirty_all_keys(emqx_user)].
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% HTTP Request
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
request_http_rest_list() ->
|
||||||
|
request_api(get, uri(), default_auth_header()).
|
||||||
|
|
||||||
|
request_http_rest_lookup(Login) ->
|
||||||
|
request_api(get, uri([Login]), default_auth_header()).
|
||||||
|
|
||||||
|
request_http_rest_add(Params) ->
|
||||||
|
request_api(post, uri(), [], default_auth_header(), Params).
|
||||||
|
|
||||||
|
request_http_rest_update(Login, Password, IsSuperuser) ->
|
||||||
|
Params = #{<<"password">> => Password, <<"is_superuser">> => IsSuperuser},
|
||||||
|
request_api(put, uri([Login]), [], default_auth_header(), Params).
|
||||||
|
|
||||||
|
request_http_rest_delete(Login) ->
|
||||||
|
request_api(delete, uri([Login]), default_auth_header()).
|
||||||
|
|
||||||
|
uri() -> uri([]).
|
||||||
|
uri(Parts) when is_list(Parts) ->
|
||||||
|
NParts = [b2l(E) || E <- Parts],
|
||||||
|
?HOST ++ filename:join([?BASE_PATH, ?API_VERSION, "mqtt_user"| NParts]).
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
b2l(B) when is_binary(B) ->
|
||||||
|
binary_to_list(B);
|
||||||
|
b2l(L) when is_list(L) ->
|
||||||
|
L.
|
|
@ -0,0 +1,59 @@
|
||||||
|
name: Run test cases
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run_test_cases:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
mongo_tag:
|
||||||
|
- 3
|
||||||
|
- 4
|
||||||
|
network_type:
|
||||||
|
- ipv4
|
||||||
|
- ipv6
|
||||||
|
connect_type:
|
||||||
|
- ssl
|
||||||
|
- tcp
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: install docker-compose
|
||||||
|
run: |
|
||||||
|
sudo curl -L "https://github.com/docker/compose/releases/download/1.25.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||||
|
sudo chmod +x /usr/local/bin/docker-compose
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- name: run test cases
|
||||||
|
env:
|
||||||
|
MONGO_TAG: ${{ matrix.mongo_tag }}
|
||||||
|
NETWORK_TYPE: ${{ matrix.network_type }}
|
||||||
|
CONNECT_TYPE: ${{ matrix.connect_type }}
|
||||||
|
run: |
|
||||||
|
set -e -u -x
|
||||||
|
if [ "$NETWORK_TYPE" = "ipv6" ];then docker network create --driver bridge --ipv6 --subnet fd15:555::/64 tests_emqx_bridge --attachable; fi
|
||||||
|
if [ "$CONNECT_TYPE" = "ssl" ]; then
|
||||||
|
docker-compose -f ./docker-compose-ssl.yml -p tests up -d
|
||||||
|
docker exec -i $(docker ps -a -f name=tests_erlang_1 -q) sh -c "echo 'auth.mongo.ssl = true' >> /emqx_auth_mongo/etc/emqx_auth_mongo.conf"
|
||||||
|
docker exec -i $(docker ps -a -f name=tests_erlang_1 -q) sh -c "echo 'auth.mongo.ssl_opts.cacertfile = /emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/ca.pem' >> /emqx_auth_mongo/etc/emqx_auth_mongo.conf"
|
||||||
|
docker exec -i $(docker ps -a -f name=tests_erlang_1 -q) sh -c "echo 'auth.mongo.ssl_opts.certfile = /emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/client-cert.pem' >> /emqx_auth_mongo/etc/emqx_auth_mongo.conf"
|
||||||
|
docker exec -i $(docker ps -a -f name=tests_erlang_1 -q) sh -c "echo 'auth.mongo.ssl_opts.keyfile = /emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/client-key.pem' >> /emqx_auth_mongo/etc/emqx_auth_mongo.conf"
|
||||||
|
else
|
||||||
|
docker-compose -f ./docker-compose.yml -p tests up -d
|
||||||
|
fi
|
||||||
|
if [ "$NETWORK_TYPE" != "ipv6" ];then
|
||||||
|
docker exec -i $(docker ps -a -f name=tests_erlang_1 -q) sh -c "sed -i '/auth.mongo.server/c auth.mongo.server = mongo_server:27017' /emqx_auth_mongo/etc/emqx_auth_mongo.conf"
|
||||||
|
else
|
||||||
|
ipv6_address=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.GlobalIPv6Address}}{{end}}' $(docker ps -a -f name=tests_mongo_server_1 -q))
|
||||||
|
docker exec -i $(docker ps -a -f name=tests_erlang_1 -q) sh -c "sed -i '/auth.mongo.server/c auth.mongo.server = $ipv6_address:27017' /emqx_auth_mongo/etc/emqx_auth_mongo.conf"
|
||||||
|
fi
|
||||||
|
docker exec -i tests_erlang_1 sh -c "make -C /emqx_auth_mongo xref"
|
||||||
|
docker exec -i tests_erlang_1 sh -c "make -C /emqx_auth_mongo eunit"
|
||||||
|
docker exec -i tests_erlang_1 sh -c "make -C /emqx_auth_mongo ct"
|
||||||
|
docker exec -i tests_erlang_1 sh -c "make -C /emqx_auth_mongo cover"
|
||||||
|
- uses: actions/upload-artifact@v1
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
name: logs_mongo${{ matrix.mongo_tag}}_${{ matrix.network_type }}
|
||||||
|
path: _build/test/logs
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
.eunit
|
||||||
|
deps
|
||||||
|
*.o
|
||||||
|
*.beam
|
||||||
|
*.plt
|
||||||
|
erl_crash.dump
|
||||||
|
ebin
|
||||||
|
rel/example_project
|
||||||
|
.concrete/DEV_MODE
|
||||||
|
.rebar
|
||||||
|
.DS_Store
|
||||||
|
.erlang.mk/
|
||||||
|
emqx_auth_mongo.d
|
||||||
|
ct.coverdata
|
||||||
|
logs/
|
||||||
|
test/ct.cover.spec
|
||||||
|
data/
|
||||||
|
cover/
|
||||||
|
eunit.coverdata
|
||||||
|
_build/
|
||||||
|
rebar.lock
|
||||||
|
erlang.mk
|
||||||
|
etc/emqx_auth_mongo.conf.rendered
|
||||||
|
.rebar3
|
|
@ -0,0 +1,31 @@
|
||||||
|
|
||||||
|
2.0.7 (2017-01-20)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Tag 2.0.7 - use `cuttlefish:unset()` for commented ACL/super config
|
||||||
|
|
||||||
|
2.0.1 (2016-11-30)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Tag 2.0.1
|
||||||
|
|
||||||
|
2.0-beta.1 (2016-08-24)
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
gen_conf
|
||||||
|
|
||||||
|
1.1.3-beta (2016-08-19)
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
Bump version to 1.1.3
|
||||||
|
|
||||||
|
1.1.2-beta (2016-06-30)
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
Bump version to 1.1.2
|
||||||
|
|
||||||
|
1.1-beta (2016-05-28)
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
First public release
|
||||||
|
|
|
@ -0,0 +1,201 @@
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright {yyyy} {name of copyright owner}
|
||||||
|
|
||||||
|
Licensed 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.
|
|
@ -0,0 +1,192 @@
|
||||||
|
emqx_auth_mongo
|
||||||
|
===============
|
||||||
|
|
||||||
|
EMQ X Authentication/ACL with MongoDB
|
||||||
|
|
||||||
|
Build the Plugin
|
||||||
|
----------------
|
||||||
|
|
||||||
|
```
|
||||||
|
make & make tests
|
||||||
|
```
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
-------------
|
||||||
|
|
||||||
|
File: etc/emqx_auth_mongo.conf
|
||||||
|
|
||||||
|
```
|
||||||
|
## MongoDB Topology Type.
|
||||||
|
##
|
||||||
|
## Value: single | unknown | sharded | rs
|
||||||
|
auth.mongo.type = single
|
||||||
|
|
||||||
|
## Sets the set name if type is rs.
|
||||||
|
##
|
||||||
|
## Value: String
|
||||||
|
## auth.mongo.rs_set_name =
|
||||||
|
|
||||||
|
## MongoDB server list.
|
||||||
|
##
|
||||||
|
## Value: String
|
||||||
|
##
|
||||||
|
## Examples: 127.0.0.1:27017,127.0.0.2:27017...
|
||||||
|
auth.mongo.server = 127.0.0.1:27017
|
||||||
|
|
||||||
|
## MongoDB pool size
|
||||||
|
##
|
||||||
|
## Value: Number
|
||||||
|
auth.mongo.pool = 8
|
||||||
|
|
||||||
|
## MongoDB login user.
|
||||||
|
##
|
||||||
|
## Value: String
|
||||||
|
## auth.mongo.login =
|
||||||
|
|
||||||
|
## MongoDB password.
|
||||||
|
##
|
||||||
|
## Value: String
|
||||||
|
## auth.mongo.password =
|
||||||
|
|
||||||
|
## MongoDB AuthSource
|
||||||
|
##
|
||||||
|
## Value: String
|
||||||
|
## Default: mqtt
|
||||||
|
## auth.mongo.auth_source = admin
|
||||||
|
|
||||||
|
## MongoDB database
|
||||||
|
##
|
||||||
|
## Value: String
|
||||||
|
auth.mongo.database = mqtt
|
||||||
|
|
||||||
|
## MongoDB write mode.
|
||||||
|
##
|
||||||
|
## Value: unsafe | safe
|
||||||
|
## auth.mongo.w_mode =
|
||||||
|
|
||||||
|
## Mongo read mode.
|
||||||
|
##
|
||||||
|
## Value: master | slave_ok
|
||||||
|
## auth.mongo.r_mode =
|
||||||
|
|
||||||
|
## MongoDB topology options.
|
||||||
|
auth.mongo.topology.pool_size = 1
|
||||||
|
auth.mongo.topology.max_overflow = 0
|
||||||
|
## auth.mongo.topology.overflow_ttl = 1000
|
||||||
|
## auth.mongo.topology.overflow_check_period = 1000
|
||||||
|
## auth.mongo.topology.local_threshold_ms = 1000
|
||||||
|
## auth.mongo.topology.connect_timeout_ms = 20000
|
||||||
|
## auth.mongo.topology.socket_timeout_ms = 100
|
||||||
|
## auth.mongo.topology.server_selection_timeout_ms = 30000
|
||||||
|
## auth.mongo.topology.wait_queue_timeout_ms = 1000
|
||||||
|
## auth.mongo.topology.heartbeat_frequency_ms = 10000
|
||||||
|
## auth.mongo.topology.min_heartbeat_frequency_ms = 1000
|
||||||
|
|
||||||
|
## Authentication query.
|
||||||
|
auth.mongo.auth_query.collection = mqtt_user
|
||||||
|
|
||||||
|
auth.mongo.auth_query.password_field = password
|
||||||
|
|
||||||
|
## Password hash.
|
||||||
|
##
|
||||||
|
## Value: plain | md5 | sha | sha256 | bcrypt
|
||||||
|
auth.mongo.auth_query.password_hash = sha256
|
||||||
|
|
||||||
|
## sha256 with salt suffix
|
||||||
|
## auth.mongo.auth_query.password_hash = sha256,salt
|
||||||
|
|
||||||
|
## sha256 with salt prefix
|
||||||
|
## auth.mongo.auth_query.password_hash = salt,sha256
|
||||||
|
|
||||||
|
## bcrypt with salt prefix
|
||||||
|
## auth.mongo.auth_query.password_hash = salt,bcrypt
|
||||||
|
|
||||||
|
## pbkdf2 with macfun iterations dklen
|
||||||
|
## macfun: md4, md5, ripemd160, sha, sha224, sha256, sha384, sha512
|
||||||
|
## auth.mongo.auth_query.password_hash = pbkdf2,sha256,1000,20
|
||||||
|
|
||||||
|
auth.mongo.auth_query.selector = username=%u
|
||||||
|
|
||||||
|
## Enable superuser query.
|
||||||
|
auth.mongo.super_query = on
|
||||||
|
|
||||||
|
auth.mongo.super_query.collection = mqtt_user
|
||||||
|
|
||||||
|
auth.mongo.super_query.super_field = is_superuser
|
||||||
|
|
||||||
|
auth.mongo.super_query.selector = username=%u
|
||||||
|
|
||||||
|
## Enable ACL query.
|
||||||
|
auth.mongo.acl_query = on
|
||||||
|
|
||||||
|
auth.mongo.acl_query.collection = mqtt_acl
|
||||||
|
|
||||||
|
auth.mongo.acl_query.selector = username=%u
|
||||||
|
```
|
||||||
|
|
||||||
|
Load the Plugin
|
||||||
|
---------------
|
||||||
|
|
||||||
|
```
|
||||||
|
./bin/emqx_ctl plugins load emqx_auth_mongo
|
||||||
|
```
|
||||||
|
|
||||||
|
MongoDB Database
|
||||||
|
----------------
|
||||||
|
|
||||||
|
```
|
||||||
|
use mqtt
|
||||||
|
db.createCollection("mqtt_user")
|
||||||
|
db.createCollection("mqtt_acl")
|
||||||
|
db.mqtt_user.ensureIndex({"username":1})
|
||||||
|
```
|
||||||
|
|
||||||
|
mqtt_user Collection
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
username: "user",
|
||||||
|
password: "password hash",
|
||||||
|
salt: "password salt",
|
||||||
|
is_superuser: boolean (true, false),
|
||||||
|
created: "datetime"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For example:
|
||||||
|
```
|
||||||
|
db.mqtt_user.insert({username: "test", password: "password hash", salt: "password salt", is_superuser: false})
|
||||||
|
db.mqtt_user.insert({username: "root", is_superuser: true})
|
||||||
|
```
|
||||||
|
|
||||||
|
mqtt_acl Collection
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
username: "username",
|
||||||
|
clientid: "clientid",
|
||||||
|
publish: ["topic1", "topic2", ...],
|
||||||
|
subscribe: ["subtop1", "subtop2", ...],
|
||||||
|
pubsub: ["topic/#", "topic1", ...]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
db.mqtt_acl.insert({username: "test", publish: ["t/1", "t/2"], subscribe: ["user/%u", "client/%c"]})
|
||||||
|
db.mqtt_acl.insert({username: "admin", pubsub: ["#"]})
|
||||||
|
```
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
|
Apache License Version 2.0
|
||||||
|
|
||||||
|
Author
|
||||||
|
------
|
||||||
|
|
||||||
|
EMQ X Team.
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
erlang:
|
||||||
|
image: erlang:22.1
|
||||||
|
volumes:
|
||||||
|
- ./:/emqx_auth_mongo
|
||||||
|
networks:
|
||||||
|
- emqx_bridge
|
||||||
|
depends_on:
|
||||||
|
- mongo_server
|
||||||
|
tty: true
|
||||||
|
|
||||||
|
mongo_server:
|
||||||
|
image: mongo:${MONGO_TAG}
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
MONGO_INITDB_DATABASE: mqtt
|
||||||
|
volumes:
|
||||||
|
- ./test/emqx_auth_mongo_SUITE_data/mongodb.pem/:/etc/certs/mongodb.pem
|
||||||
|
networks:
|
||||||
|
- emqx_bridge
|
||||||
|
command:
|
||||||
|
--ipv6
|
||||||
|
--bind_ip_all
|
||||||
|
--sslMode requireSSL
|
||||||
|
--sslPEMKeyFile /etc/certs/mongodb.pem
|
||||||
|
|
||||||
|
networks:
|
||||||
|
emqx_bridge:
|
||||||
|
driver: bridge
|
|
@ -0,0 +1,27 @@
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
erlang:
|
||||||
|
image: erlang:22.1
|
||||||
|
volumes:
|
||||||
|
- ./:/emqx_auth_mongo
|
||||||
|
networks:
|
||||||
|
- emqx_bridge
|
||||||
|
depends_on:
|
||||||
|
- mongo_server
|
||||||
|
tty: true
|
||||||
|
|
||||||
|
mongo_server:
|
||||||
|
image: mongo:${MONGO_TAG}
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
MONGO_INITDB_DATABASE: mqtt
|
||||||
|
networks:
|
||||||
|
- emqx_bridge
|
||||||
|
command:
|
||||||
|
--ipv6
|
||||||
|
--bind_ip_all
|
||||||
|
|
||||||
|
networks:
|
||||||
|
emqx_bridge:
|
||||||
|
driver: bridge
|
|
@ -0,0 +1,172 @@
|
||||||
|
##--------------------------------------------------------------------
|
||||||
|
## MongoDB Auth/ACL Plugin
|
||||||
|
##--------------------------------------------------------------------
|
||||||
|
|
||||||
|
## MongoDB Topology Type.
|
||||||
|
##
|
||||||
|
## Value: single | unknown | sharded | rs
|
||||||
|
auth.mongo.type = single
|
||||||
|
|
||||||
|
## The set name if type is rs.
|
||||||
|
##
|
||||||
|
## Value: String
|
||||||
|
## auth.mongo.rs_set_name =
|
||||||
|
|
||||||
|
## MongoDB server list.
|
||||||
|
##
|
||||||
|
## Value: String
|
||||||
|
##
|
||||||
|
## Examples: 127.0.0.1:27017,127.0.0.2:27017...
|
||||||
|
auth.mongo.server = 127.0.0.1:27017
|
||||||
|
|
||||||
|
## MongoDB pool size
|
||||||
|
##
|
||||||
|
## Value: Number
|
||||||
|
auth.mongo.pool = 8
|
||||||
|
|
||||||
|
## MongoDB login user.
|
||||||
|
##
|
||||||
|
## Value: String
|
||||||
|
## auth.mongo.login =
|
||||||
|
|
||||||
|
## MongoDB password.
|
||||||
|
##
|
||||||
|
## Value: String
|
||||||
|
## auth.mongo.password =
|
||||||
|
|
||||||
|
## MongoDB AuthSource
|
||||||
|
##
|
||||||
|
## Value: String
|
||||||
|
## Default: mqtt
|
||||||
|
## auth.mongo.auth_source = admin
|
||||||
|
|
||||||
|
## MongoDB database
|
||||||
|
##
|
||||||
|
## Value: String
|
||||||
|
auth.mongo.database = mqtt
|
||||||
|
|
||||||
|
## MongoDB query timeout
|
||||||
|
##
|
||||||
|
## Value: Duration
|
||||||
|
## auth.mongo.query_timeout = 5s
|
||||||
|
|
||||||
|
## Whether to enable SSL connection.
|
||||||
|
##
|
||||||
|
## Value: true | false
|
||||||
|
## auth.mongo.ssl = false
|
||||||
|
|
||||||
|
## SSL keyfile.
|
||||||
|
##
|
||||||
|
## Value: File
|
||||||
|
## auth.mongo.ssl_opts.keyfile =
|
||||||
|
|
||||||
|
## SSL certfile.
|
||||||
|
##
|
||||||
|
## Value: File
|
||||||
|
## auth.mongo.ssl_opts.certfile =
|
||||||
|
|
||||||
|
## SSL cacertfile.
|
||||||
|
##
|
||||||
|
## Value: File
|
||||||
|
## auth.mongo.ssl_opts.cacertfile =
|
||||||
|
|
||||||
|
## MongoDB write mode.
|
||||||
|
##
|
||||||
|
## Value: unsafe | safe
|
||||||
|
## auth.mongo.w_mode =
|
||||||
|
|
||||||
|
## Mongo read mode.
|
||||||
|
##
|
||||||
|
## Value: master | slave_ok
|
||||||
|
## auth.mongo.r_mode =
|
||||||
|
|
||||||
|
## MongoDB topology options.
|
||||||
|
auth.mongo.topology.pool_size = 1
|
||||||
|
auth.mongo.topology.max_overflow = 0
|
||||||
|
## auth.mongo.topology.overflow_ttl = 1000
|
||||||
|
## auth.mongo.topology.overflow_check_period = 1000
|
||||||
|
## auth.mongo.topology.local_threshold_ms = 1000
|
||||||
|
## auth.mongo.topology.connect_timeout_ms = 20000
|
||||||
|
## auth.mongo.topology.socket_timeout_ms = 100
|
||||||
|
## auth.mongo.topology.server_selection_timeout_ms = 30000
|
||||||
|
## auth.mongo.topology.wait_queue_timeout_ms = 1000
|
||||||
|
## auth.mongo.topology.heartbeat_frequency_ms = 10000
|
||||||
|
## auth.mongo.topology.min_heartbeat_frequency_ms = 1000
|
||||||
|
|
||||||
|
## -------------------------------------------------
|
||||||
|
## Auth Query
|
||||||
|
## -------------------------------------------------
|
||||||
|
## Password hash.
|
||||||
|
##
|
||||||
|
## Value: plain | md5 | sha | sha256 | bcrypt
|
||||||
|
auth.mongo.auth_query.password_hash = sha256
|
||||||
|
|
||||||
|
## sha256 with salt suffix
|
||||||
|
## auth.mongo.auth_query.password_hash = sha256,salt
|
||||||
|
|
||||||
|
## sha256 with salt prefix
|
||||||
|
## auth.mongo.auth_query.password_hash = salt,sha256
|
||||||
|
|
||||||
|
## bcrypt with salt prefix
|
||||||
|
## auth.mongo.auth_query.password_hash = salt,bcrypt
|
||||||
|
|
||||||
|
## pbkdf2 with macfun iterations dklen
|
||||||
|
## macfun: md4, md5, ripemd160, sha, sha224, sha256, sha384, sha512
|
||||||
|
## auth.mongo.auth_query.password_hash = pbkdf2,sha256,1000,20
|
||||||
|
|
||||||
|
## Authentication query.
|
||||||
|
auth.mongo.auth_query.collection = mqtt_user
|
||||||
|
|
||||||
|
## Password mainly fields
|
||||||
|
##
|
||||||
|
## Value: password | password,salt
|
||||||
|
auth.mongo.auth_query.password_field = password
|
||||||
|
|
||||||
|
## Authentication Selector.
|
||||||
|
##
|
||||||
|
## Variables:
|
||||||
|
## - %u: username
|
||||||
|
## - %c: clientid
|
||||||
|
## - %C: common name of client TLS cert
|
||||||
|
## - %d: subject of client TLS cert
|
||||||
|
##
|
||||||
|
## auth.mongo.auth_query.selector = {Field}={Placeholder}
|
||||||
|
auth.mongo.auth_query.selector = username=%u
|
||||||
|
|
||||||
|
## -------------------------------------------------
|
||||||
|
## Super User Query
|
||||||
|
## -------------------------------------------------
|
||||||
|
auth.mongo.super_query.collection = mqtt_user
|
||||||
|
auth.mongo.super_query.super_field = is_superuser
|
||||||
|
#auth.mongo.super_query.selector = username=%u, clientid=%c
|
||||||
|
auth.mongo.super_query.selector = username=%u
|
||||||
|
|
||||||
|
## ACL Selector.
|
||||||
|
##
|
||||||
|
## Multiple selectors could be combined with '$or'
|
||||||
|
## when query acl from mongo.
|
||||||
|
##
|
||||||
|
## e.g.
|
||||||
|
##
|
||||||
|
## With following 2 selectors configured:
|
||||||
|
##
|
||||||
|
## auth.mongo.acl_query.selector.1 = username=%u
|
||||||
|
## auth.mongo.acl_query.selector.2 = username=$all
|
||||||
|
##
|
||||||
|
## And if a client connected using username 'ilyas',
|
||||||
|
## then the following mongo command will be used to
|
||||||
|
## retrieve acl entries:
|
||||||
|
##
|
||||||
|
## db.mqtt_acl.find({$or: [{username: "ilyas"}, {username: "$all"}]});
|
||||||
|
##
|
||||||
|
## Variables:
|
||||||
|
## - %u: username
|
||||||
|
## - %c: clientid
|
||||||
|
##
|
||||||
|
## Examples:
|
||||||
|
##
|
||||||
|
## auth.mongo.acl_query.selector.1 = username=%u,clientid=%c
|
||||||
|
## auth.mongo.acl_query.selector.2 = username=$all
|
||||||
|
## auth.mongo.acl_query.selector.3 = clientid=$all
|
||||||
|
auth.mongo.acl_query.collection = mqtt_acl
|
||||||
|
auth.mongo.acl_query.selector = username=%u
|
|
@ -0,0 +1,37 @@
|
||||||
|
|
||||||
|
-define(APP, emqx_auth_mongo).
|
||||||
|
|
||||||
|
-define(DEFAULT_SELECTORS, [{<<"username">>, <<"%u">>}]).
|
||||||
|
|
||||||
|
-record(superquery, {collection = <<"mqtt_user">>,
|
||||||
|
field = <<"is_superuser">>,
|
||||||
|
selector = {<<"username">>, <<"%u">>}}).
|
||||||
|
|
||||||
|
-record(authquery, {collection = <<"mqtt_user">>,
|
||||||
|
field = <<"password">>,
|
||||||
|
hash = sha256,
|
||||||
|
selector = {<<"username">>, <<"%u">>}}).
|
||||||
|
|
||||||
|
-record(aclquery, {collection = <<"mqtt_acl">>,
|
||||||
|
selector = {<<"username">>, <<"%u">>}}).
|
||||||
|
|
||||||
|
-record(auth_metrics, {
|
||||||
|
success = 'client.auth.success',
|
||||||
|
failure = 'client.auth.failure',
|
||||||
|
ignore = 'client.auth.ignore'
|
||||||
|
}).
|
||||||
|
|
||||||
|
-record(acl_metrics, {
|
||||||
|
allow = 'client.acl.allow',
|
||||||
|
deny = 'client.acl.deny',
|
||||||
|
ignore = 'client.acl.ignore'
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(METRICS(Type), tl(tuple_to_list(#Type{}))).
|
||||||
|
-define(METRICS(Type, K), #Type{}#Type.K).
|
||||||
|
|
||||||
|
-define(AUTH_METRICS, ?METRICS(auth_metrics)).
|
||||||
|
-define(AUTH_METRICS(K), ?METRICS(auth_metrics, K)).
|
||||||
|
|
||||||
|
-define(ACL_METRICS, ?METRICS(acl_metrics)).
|
||||||
|
-define(ACL_METRICS(K), ?METRICS(acl_metrics, K)).
|
|
@ -0,0 +1,292 @@
|
||||||
|
%%-*- mode: erlang -*-
|
||||||
|
%% emqx_auth_mongo config mapping
|
||||||
|
|
||||||
|
{mapping, "auth.mongo.type", "emqx_auth_mongo.server", [
|
||||||
|
{default, single},
|
||||||
|
{datatype, {enum, [single, unknown, sharded, rs]}}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.mongo.rs_set_name", "emqx_auth_mongo.server", [
|
||||||
|
{default, "mqtt"},
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.mongo.server", "emqx_auth_mongo.server", [
|
||||||
|
{default, "127.0.0.1:27017"},
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.mongo.pool", "emqx_auth_mongo.server", [
|
||||||
|
{default, 8},
|
||||||
|
{datatype, integer}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.mongo.login", "emqx_auth_mongo.server", [
|
||||||
|
{default, ""},
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.mongo.password", "emqx_auth_mongo.server", [
|
||||||
|
{default, ""},
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.mongo.database", "emqx_auth_mongo.server", [
|
||||||
|
{default, "mqtt"},
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.mongo.auth_source", "emqx_auth_mongo.server", [
|
||||||
|
{default, "mqtt"},
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.mongo.ssl", "emqx_auth_mongo.server", [
|
||||||
|
{default, false},
|
||||||
|
{datatype, {enum, [true, false]}}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.mongo.ssl_opts.keyfile", "emqx_auth_mongo.server", [
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.mongo.ssl_opts.certfile", "emqx_auth_mongo.server", [
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.mongo.ssl_opts.cacertfile", "emqx_auth_mongo.server", [
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.mongo.w_mode", "emqx_auth_mongo.server", [
|
||||||
|
{default, undef},
|
||||||
|
{datatype, {enum, [safe, unsafe, undef]}}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.mongo.r_mode", "emqx_auth_mongo.server", [
|
||||||
|
{default, undef},
|
||||||
|
{datatype, {enum, [master, slave_ok, undef]}}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.mongo.topology.$name", "emqx_auth_mongo.server", [
|
||||||
|
{datatype, integer}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{translation, "emqx_auth_mongo.server", fun(Conf) ->
|
||||||
|
H = cuttlefish:conf_get("auth.mongo.server", Conf),
|
||||||
|
Hosts = string:tokens(H, ","),
|
||||||
|
Type0 = cuttlefish:conf_get("auth.mongo.type", Conf),
|
||||||
|
Pool = cuttlefish:conf_get("auth.mongo.pool", Conf),
|
||||||
|
Login = cuttlefish:conf_get("auth.mongo.login", Conf),
|
||||||
|
Passwd = cuttlefish:conf_get("auth.mongo.password", Conf),
|
||||||
|
DB = cuttlefish:conf_get("auth.mongo.database", Conf),
|
||||||
|
AuthSrc = cuttlefish:conf_get("auth.mongo.auth_source", Conf),
|
||||||
|
R = cuttlefish:conf_get("auth.mongo.w_mode", Conf),
|
||||||
|
W = cuttlefish:conf_get("auth.mongo.r_mode", Conf),
|
||||||
|
Login0 = case Login =:= [] of
|
||||||
|
true -> [];
|
||||||
|
false -> [{login, list_to_binary(Login)}]
|
||||||
|
end,
|
||||||
|
Passwd0 = case Passwd =:= [] of
|
||||||
|
true -> [];
|
||||||
|
false -> [{password, list_to_binary(Passwd)}]
|
||||||
|
end,
|
||||||
|
W0 = case W =:= undef of
|
||||||
|
true -> [];
|
||||||
|
false -> [{w_mode, W}]
|
||||||
|
end,
|
||||||
|
R0 = case R =:= undef of
|
||||||
|
true -> [];
|
||||||
|
false -> [{r_mode, R}]
|
||||||
|
end,
|
||||||
|
Ssl = case cuttlefish:conf_get("auth.mongo.ssl", Conf) of
|
||||||
|
true ->
|
||||||
|
Filter = fun(Opts) -> [{K, V} || {K, V} <- Opts, V =/= undefined] end,
|
||||||
|
SslOpts = fun(Prefix) ->
|
||||||
|
Filter([{keyfile, cuttlefish:conf_get(Prefix ++ ".keyfile", Conf, undefined)},
|
||||||
|
{certfile, cuttlefish:conf_get(Prefix ++ ".certfile", Conf, undefined)},
|
||||||
|
{cacertfile, cuttlefish:conf_get(Prefix ++ ".cacertfile", Conf, undefined)}])
|
||||||
|
end,
|
||||||
|
[{ssl, true}, {ssl_opts, SslOpts("auth.mongo.ssl_opts")}];
|
||||||
|
false ->
|
||||||
|
[]
|
||||||
|
end,
|
||||||
|
WorkerOptions = [{database, list_to_binary(DB)}, {auth_source, list_to_binary(AuthSrc)}]
|
||||||
|
++ Login0 ++ Passwd0 ++ W0 ++ R0 ++ Ssl,
|
||||||
|
|
||||||
|
Vars = cuttlefish_variable:fuzzy_matches(["auth", "mongo", "topology", "$name"], Conf),
|
||||||
|
Options = lists:map(fun({_, Name}) ->
|
||||||
|
Name2 = case Name of
|
||||||
|
"local_threshold_ms" -> "localThresholdMS";
|
||||||
|
"connect_timeout_ms" -> "connectTimeoutMS";
|
||||||
|
"socket_timeout_ms" -> "socketTimeoutMS";
|
||||||
|
"server_selection_timeout_ms" -> "serverSelectionTimeoutMS";
|
||||||
|
"wait_queue_timeout_ms" -> "waitQueueTimeoutMS";
|
||||||
|
"heartbeat_frequency_ms" -> "heartbeatFrequencyMS";
|
||||||
|
"min_heartbeat_frequency_ms" -> "minHeartbeatFrequencyMS";
|
||||||
|
_ -> Name
|
||||||
|
end,
|
||||||
|
{list_to_atom(Name2), cuttlefish:conf_get("auth.mongo.topology."++Name, Conf)}
|
||||||
|
end, Vars),
|
||||||
|
|
||||||
|
Type = case Type0 =:= rs of
|
||||||
|
true -> {Type0, list_to_binary(cuttlefish:conf_get("auth.mongo.rs_set_name", Conf))};
|
||||||
|
false -> Type0
|
||||||
|
end,
|
||||||
|
[{type, Type},
|
||||||
|
{hosts, Hosts},
|
||||||
|
{options, Options},
|
||||||
|
{worker_options, WorkerOptions},
|
||||||
|
{auto_reconnect, 1},
|
||||||
|
{pool_size, Pool}]
|
||||||
|
end}.
|
||||||
|
|
||||||
|
%% The mongodb operation timeout is specified by the value of `cursor_timeout` from application config,
|
||||||
|
%% or `infinity` if `cursor_timeout` not specified
|
||||||
|
{mapping, "auth.mongo.query_timeout", "mongodb.cursor_timeout", [
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{translation, "mongodb.cursor_timeout", fun(Conf) ->
|
||||||
|
case cuttlefish:conf_get("auth.mongo.query_timeout", Conf, undefined) of
|
||||||
|
undefined -> infinity;
|
||||||
|
Duration ->
|
||||||
|
case cuttlefish_duration:parse(Duration, ms) of
|
||||||
|
{error, Reason} -> error(Reason);
|
||||||
|
Ms when is_integer(Ms) -> Ms
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end}.
|
||||||
|
|
||||||
|
{mapping, "auth.mongo.auth_query.collection", "emqx_auth_mongo.auth_query", [
|
||||||
|
{default, "mqtt_user"},
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.mongo.auth_query.password_field", "emqx_auth_mongo.auth_query", [
|
||||||
|
{default, "password"},
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.mongo.auth_query.password_hash", "emqx_auth_mongo.auth_query", [
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.mongo.auth_query.selector", "emqx_auth_mongo.auth_query", [
|
||||||
|
{default, ""},
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{translation, "emqx_auth_mongo.auth_query", fun(Conf) ->
|
||||||
|
case cuttlefish:conf_get("auth.mongo.auth_query.collection", Conf) of
|
||||||
|
undefined -> cuttlefish:unset();
|
||||||
|
Collection ->
|
||||||
|
PasswordField = cuttlefish:conf_get("auth.mongo.auth_query.password_field", Conf),
|
||||||
|
PasswordHash = cuttlefish:conf_get("auth.mongo.auth_query.password_hash", Conf),
|
||||||
|
SelectorStr = cuttlefish:conf_get("auth.mongo.auth_query.selector", Conf),
|
||||||
|
SelectorList =
|
||||||
|
lists:map(fun(Selector) ->
|
||||||
|
case string:tokens(Selector, "=") of
|
||||||
|
[Field, Val] -> {list_to_binary(Field), list_to_binary(Val)};
|
||||||
|
_ -> {<<"username">>, <<"%u">>}
|
||||||
|
end
|
||||||
|
end, string:tokens(SelectorStr, ", ")),
|
||||||
|
|
||||||
|
PasswordFields = [list_to_binary(Field) || Field <- string:tokens(PasswordField, ",")],
|
||||||
|
HashValue =
|
||||||
|
case string:tokens(PasswordHash, ",") of
|
||||||
|
[Hash] -> list_to_atom(Hash);
|
||||||
|
[Prefix, Suffix] -> {list_to_atom(Prefix), list_to_atom(Suffix)};
|
||||||
|
[Hash, MacFun, Iterations, Dklen] -> {list_to_atom(Hash), list_to_atom(MacFun), list_to_integer(Iterations), list_to_integer(Dklen)};
|
||||||
|
_ -> plain
|
||||||
|
end,
|
||||||
|
[{collection, Collection},
|
||||||
|
{password_field, PasswordFields},
|
||||||
|
{password_hash, HashValue},
|
||||||
|
{selector, SelectorList}]
|
||||||
|
end
|
||||||
|
end}.
|
||||||
|
|
||||||
|
{mapping, "auth.mongo.super_query", "emqx_auth_mongo.super_query", [
|
||||||
|
{default, off},
|
||||||
|
{datatype, flag}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.mongo.super_query.collection", "emqx_auth_mongo.super_query", [
|
||||||
|
{default, "mqtt_user"},
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.mongo.super_query.super_field", "emqx_auth_mongo.super_query", [
|
||||||
|
{default, "is_superuser"},
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.mongo.super_query.selector", "emqx_auth_mongo.super_query", [
|
||||||
|
{default, ""},
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{translation, "emqx_auth_mongo.super_query", fun(Conf) ->
|
||||||
|
case cuttlefish:conf_get("auth.mongo.super_query.collection", Conf) of
|
||||||
|
undefined -> cuttlefish:unset();
|
||||||
|
Collection ->
|
||||||
|
SuperField = cuttlefish:conf_get("auth.mongo.super_query.super_field", Conf),
|
||||||
|
SelectorStr = cuttlefish:conf_get("auth.mongo.super_query.selector", Conf),
|
||||||
|
SelectorList =
|
||||||
|
lists:map(fun(Selector) ->
|
||||||
|
case string:tokens(Selector, "=") of
|
||||||
|
[Field, Val] -> {list_to_binary(Field), list_to_binary(Val)};
|
||||||
|
_ -> {<<"username">>, <<"%u">>}
|
||||||
|
end
|
||||||
|
end, string:tokens(SelectorStr, ", ")),
|
||||||
|
[{collection, Collection}, {super_field, SuperField}, {selector, SelectorList}]
|
||||||
|
end
|
||||||
|
end}.
|
||||||
|
|
||||||
|
{mapping, "auth.mongo.acl_query", "emqx_auth_mongo.acl_query", [
|
||||||
|
{default, off},
|
||||||
|
{datatype, flag}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.mongo.acl_query.collection", "emqx_auth_mongo.acl_query", [
|
||||||
|
{default, "mqtt_user"},
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.mongo.acl_query.selector", "emqx_auth_mongo.acl_query", [
|
||||||
|
{default, ""},
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
{mapping, "auth.mongo.acl_query.selector.$id", "emqx_auth_mongo.acl_query", [
|
||||||
|
{default, ""},
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{translation, "emqx_auth_mongo.acl_query", fun(Conf) ->
|
||||||
|
case cuttlefish:conf_get("auth.mongo.acl_query.collection", Conf) of
|
||||||
|
undefined -> cuttlefish:unset();
|
||||||
|
Collection ->
|
||||||
|
SelectorStrList =
|
||||||
|
lists:map(
|
||||||
|
fun
|
||||||
|
({["auth","mongo","acl_query","selector"], ConfEntry}) ->
|
||||||
|
ConfEntry;
|
||||||
|
({["auth","mongo","acl_query","selector", _], ConfEntry}) ->
|
||||||
|
ConfEntry
|
||||||
|
end,
|
||||||
|
cuttlefish_variable:filter_by_prefix("auth.mongo.acl_query.selector", Conf)),
|
||||||
|
SelectorListList =
|
||||||
|
lists:map(
|
||||||
|
fun(SelectorStr) ->
|
||||||
|
lists:map(fun(Selector) ->
|
||||||
|
case string:tokens(Selector, "=") of
|
||||||
|
[Field, Val] -> {list_to_binary(Field), list_to_binary(Val)};
|
||||||
|
_ -> {<<"username">>, <<"%u">>}
|
||||||
|
end
|
||||||
|
end, string:tokens(SelectorStr, ", "))
|
||||||
|
end,
|
||||||
|
SelectorStrList),
|
||||||
|
[{collection, Collection}, {selector, SelectorListList}]
|
||||||
|
end
|
||||||
|
end}.
|
|
@ -0,0 +1,35 @@
|
||||||
|
{deps,
|
||||||
|
[{mongodb, {git,"https://github.com/emqx/mongodb-erlang", {tag, "v3.0.7"}}},
|
||||||
|
{ecpool, {git,"https://github.com/emqx/ecpool", {tag, "v0.4.2"}}},
|
||||||
|
{emqx_passwd, {git, "https://github.com/emqx/emqx-passwd", {tag, "v1.1.1"}}}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{edoc_opts, [{preprocess, true}]}.
|
||||||
|
{erl_opts, [warn_unused_vars,
|
||||||
|
warn_shadow_vars,
|
||||||
|
warn_unused_import,
|
||||||
|
warn_obsolete_guard,
|
||||||
|
debug_info,
|
||||||
|
compressed,
|
||||||
|
{parse_transform}
|
||||||
|
]}.
|
||||||
|
{overrides, [{add, [{erl_opts, [compressed]}]}]}.
|
||||||
|
|
||||||
|
{xref_checks, [undefined_function_calls, undefined_functions,
|
||||||
|
locals_not_used, deprecated_function_calls,
|
||||||
|
warnings_as_errors, deprecated_functions
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{cover_enabled, true}.
|
||||||
|
{cover_opts, [verbose]}.
|
||||||
|
{cover_export_enabled, true}.
|
||||||
|
|
||||||
|
{profiles,
|
||||||
|
[{test,
|
||||||
|
[{deps,
|
||||||
|
[{emqx_ct_helper, {git, "https://github.com/emqx/emqx-ct-helper", {tag, "1.2.2"}}},
|
||||||
|
{emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.2.0"}}}
|
||||||
|
]},
|
||||||
|
{erl_opts, [debug_info]}
|
||||||
|
]}
|
||||||
|
]}.
|
|
@ -0,0 +1,91 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed 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.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_acl_mongo).
|
||||||
|
|
||||||
|
-include("emqx_auth_mongo.hrl").
|
||||||
|
-include_lib("emqx/include/emqx.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
|
%% ACL callbacks
|
||||||
|
-export([ register_metrics/0
|
||||||
|
, check_acl/5
|
||||||
|
, description/0
|
||||||
|
]).
|
||||||
|
-spec(register_metrics() -> ok).
|
||||||
|
register_metrics() ->
|
||||||
|
lists:foreach(fun emqx_metrics:ensure/1, ?ACL_METRICS).
|
||||||
|
|
||||||
|
check_acl(#{username := <<$$, _/binary>>}, _PubSub, _Topic, _AclResult, _State) ->
|
||||||
|
ok;
|
||||||
|
|
||||||
|
check_acl(ClientInfo, PubSub, Topic, _AclResult, Env = #{aclquery := AclQuery}) ->
|
||||||
|
#aclquery{collection = Coll, selector = SelectorList} = AclQuery,
|
||||||
|
Pool = maps:get(pool, Env, ?APP),
|
||||||
|
SelectorMapList =
|
||||||
|
lists:map(fun(Selector) ->
|
||||||
|
maps:from_list(emqx_auth_mongo:replvars(Selector, ClientInfo))
|
||||||
|
end, SelectorList),
|
||||||
|
case emqx_auth_mongo:query_multi(Pool, Coll, SelectorMapList) of
|
||||||
|
[] -> ok;
|
||||||
|
Rows ->
|
||||||
|
try match(ClientInfo, Topic, topics(PubSub, Rows)) of
|
||||||
|
matched -> emqx_metrics:inc(?ACL_METRICS(allow)),
|
||||||
|
{stop, allow};
|
||||||
|
nomatch -> emqx_metrics:inc(?ACL_METRICS(deny)),
|
||||||
|
{stop, deny}
|
||||||
|
catch
|
||||||
|
_Err:Reason->
|
||||||
|
?LOG(error, "[MongoDB] Check mongo ~p ACL failed, got ACL config: ~p, error: :~p",
|
||||||
|
[PubSub, Rows, Reason]),
|
||||||
|
emqx_metrics:inc(?ACL_METRICS(ignore)),
|
||||||
|
ignore
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
|
||||||
|
match(_ClientInfo, _Topic, []) ->
|
||||||
|
nomatch;
|
||||||
|
match(ClientInfo, Topic, [TopicFilter|More]) ->
|
||||||
|
case emqx_topic:match(Topic, feedvar(ClientInfo, TopicFilter)) of
|
||||||
|
true -> matched;
|
||||||
|
false -> match(ClientInfo, Topic, More)
|
||||||
|
end.
|
||||||
|
|
||||||
|
topics(publish, Rows) ->
|
||||||
|
lists:foldl(fun(Row, Acc) ->
|
||||||
|
Topics = maps:get(<<"publish">>, Row, []) ++ maps:get(<<"pubsub">>, Row, []),
|
||||||
|
lists:umerge(Acc, Topics)
|
||||||
|
end, [], Rows);
|
||||||
|
|
||||||
|
topics(subscribe, Rows) ->
|
||||||
|
lists:foldl(fun(Row, Acc) ->
|
||||||
|
Topics = maps:get(<<"subscribe">>, Row, []) ++ maps:get(<<"pubsub">>, Row, []),
|
||||||
|
lists:umerge(Acc, Topics)
|
||||||
|
end, [], Rows).
|
||||||
|
|
||||||
|
feedvar(#{clientid := ClientId, username := Username}, Str) ->
|
||||||
|
lists:foldl(fun({Var, Val}, Acc) ->
|
||||||
|
feedvar(Acc, Var, Val)
|
||||||
|
end, Str, [{"%u", Username}, {"%c", ClientId}]).
|
||||||
|
|
||||||
|
feedvar(Str, _Var, undefined) ->
|
||||||
|
Str;
|
||||||
|
feedvar(Str, Var, Val) ->
|
||||||
|
re:replace(Str, Var, Val, [global, {return, binary}]).
|
||||||
|
|
||||||
|
description() -> "ACL with MongoDB".
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
{application, emqx_auth_mongo,
|
||||||
|
[{description, "EMQ X Authentication/ACL with MongoDB"},
|
||||||
|
{vsn, "git"},
|
||||||
|
{modules, []},
|
||||||
|
{registered, [emqx_auth_mongo_sup]},
|
||||||
|
{applications, [kernel,stdlib,mongodb,ecpool,emqx_passwd]},
|
||||||
|
{mod, {emqx_auth_mongo_app,[]}},
|
||||||
|
{env, []},
|
||||||
|
{licenses, ["Apache-2.0"]},
|
||||||
|
{maintainers, ["EMQ X Team <contact@emqx.io>"]},
|
||||||
|
{links, [{"Homepage", "https://emqx.io/"},
|
||||||
|
{"Github", "https://github.com/emqx/emqx-auth-mongo"}
|
||||||
|
]}
|
||||||
|
]}.
|
|
@ -0,0 +1,24 @@
|
||||||
|
%%-*- mode: erlang -*-
|
||||||
|
%% .app.src.script
|
||||||
|
|
||||||
|
RemoveLeadingV =
|
||||||
|
fun(Tag) ->
|
||||||
|
case re:run(Tag, "^[v|e]?[0-9]\.[0-9]\.([0-9]|(rc|beta|alpha)\.[0-9])", [{capture, none}]) of
|
||||||
|
nomatch ->
|
||||||
|
re:replace(Tag, "/", "-", [{return ,list}]);
|
||||||
|
_ ->
|
||||||
|
%% if it is a version number prefixed by 'v' or 'e', then remove it
|
||||||
|
re:replace(Tag, "[v|e]", "", [{return ,list}])
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
case os:getenv("EMQX_DEPS_DEFAULT_VSN") of
|
||||||
|
false -> CONFIG; % env var not defined
|
||||||
|
[] -> CONFIG; % env var set to empty string
|
||||||
|
Tag ->
|
||||||
|
[begin
|
||||||
|
AppConf0 = lists:keystore(vsn, 1, AppConf, {vsn, RemoveLeadingV(Tag)}),
|
||||||
|
{application, App, AppConf0}
|
||||||
|
end || Conf = {application, App, AppConf} <- CONFIG]
|
||||||
|
end.
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
%%-*-: erlang -*-
|
||||||
|
{"4.2.3",
|
||||||
|
[
|
||||||
|
{"4.2.2", [
|
||||||
|
{load_module, emqx_auth_mongo_app, brutal_purge, soft_purge, []},
|
||||||
|
{load_module, emqx_auth_mongo, brutal_purge, soft_purge, []},
|
||||||
|
{load_module, emqx_acl_mongo, brutal_purge, soft_purge, [emqx_auth_mongo]}
|
||||||
|
]},
|
||||||
|
{"4.2.1", [
|
||||||
|
{load_module, emqx_auth_mongo_app, brutal_purge, soft_purge, []},
|
||||||
|
{load_module, emqx_auth_mongo, brutal_purge, soft_purge, []},
|
||||||
|
{load_module, emqx_acl_mongo, brutal_purge, soft_purge, [emqx_auth_mongo]}
|
||||||
|
]},
|
||||||
|
{"4.2.0", [
|
||||||
|
{load_module, emqx_auth_mongo_app, brutal_purge, soft_purge, []},
|
||||||
|
{load_module, emqx_auth_mongo, brutal_purge, soft_purge, []},
|
||||||
|
{load_module, emqx_acl_mongo, brutal_purge, soft_purge, [emqx_auth_mongo]}
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{"4.2.2", [
|
||||||
|
{load_module, emqx_auth_mongo_app, brutal_purge, soft_purge, []},
|
||||||
|
{load_module, emqx_auth_mongo, brutal_purge, soft_purge, []},
|
||||||
|
{load_module, emqx_acl_mongo, brutal_purge, soft_purge, [emqx_auth_mongo]}
|
||||||
|
]},
|
||||||
|
{"4.2.1", [
|
||||||
|
{load_module, emqx_auth_mongo_app, brutal_purge, soft_purge, []},
|
||||||
|
{load_module, emqx_auth_mongo, brutal_purge, soft_purge, []},
|
||||||
|
{load_module, emqx_acl_mongo, brutal_purge, soft_purge, [emqx_auth_mongo]}
|
||||||
|
]},
|
||||||
|
{"4.2.0", [
|
||||||
|
{load_module, emqx_auth_mongo_app, brutal_purge, soft_purge, []},
|
||||||
|
{load_module, emqx_auth_mongo, brutal_purge, soft_purge, []},
|
||||||
|
{load_module, emqx_acl_mongo, brutal_purge, soft_purge, [emqx_auth_mongo]}
|
||||||
|
]}
|
||||||
|
]
|
||||||
|
}.
|
|
@ -0,0 +1,134 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed 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.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_auth_mongo).
|
||||||
|
|
||||||
|
-behaviour(ecpool_worker).
|
||||||
|
|
||||||
|
-include("emqx_auth_mongo.hrl").
|
||||||
|
-include_lib("emqx/include/emqx.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
-include_lib("emqx/include/types.hrl").
|
||||||
|
|
||||||
|
-export([ register_metrics/0
|
||||||
|
, check/3
|
||||||
|
, description/0
|
||||||
|
]).
|
||||||
|
|
||||||
|
-export([ replvar/2
|
||||||
|
, replvars/2
|
||||||
|
, connect/1
|
||||||
|
, query/3
|
||||||
|
, query_multi/3
|
||||||
|
]).
|
||||||
|
|
||||||
|
-spec(register_metrics() -> ok).
|
||||||
|
register_metrics() ->
|
||||||
|
lists:foreach(fun emqx_metrics:ensure/1, ?AUTH_METRICS).
|
||||||
|
|
||||||
|
check(ClientInfo = #{password := Password}, AuthResult,
|
||||||
|
Env = #{authquery := AuthQuery, superquery := SuperQuery}) ->
|
||||||
|
#authquery{collection = Collection, field = Fields,
|
||||||
|
hash = HashType, selector = Selector} = AuthQuery,
|
||||||
|
Pool = maps:get(pool, Env, ?APP),
|
||||||
|
case query(Pool, Collection, maps:from_list(replvars(Selector, ClientInfo))) of
|
||||||
|
undefined -> emqx_metrics:inc(?AUTH_METRICS(ignore));
|
||||||
|
{error, Reason} ->
|
||||||
|
?LOG(error, "[MongoDB] Can't connect to MongoDB server: ~0p", [Reason]),
|
||||||
|
ok = emqx_metrics:inc(?AUTH_METRICS(failure)),
|
||||||
|
{stop, AuthResult#{auth_result => not_authorized, anonymous => false}};
|
||||||
|
UserMap ->
|
||||||
|
Result = case [maps:get(Field, UserMap, undefined) || Field <- Fields] of
|
||||||
|
[undefined] -> {error, password_error};
|
||||||
|
[PassHash] ->
|
||||||
|
check_pass({PassHash, Password}, HashType);
|
||||||
|
[PassHash, Salt|_] ->
|
||||||
|
check_pass({PassHash, Salt, Password}, HashType)
|
||||||
|
end,
|
||||||
|
case Result of
|
||||||
|
ok ->
|
||||||
|
ok = emqx_metrics:inc(?AUTH_METRICS(success)),
|
||||||
|
{stop, AuthResult#{is_superuser => is_superuser(Pool, SuperQuery, ClientInfo),
|
||||||
|
anonymous => false,
|
||||||
|
auth_result => success}};
|
||||||
|
{error, Error} ->
|
||||||
|
?LOG(error, "[MongoDB] check auth fail: ~p", [Error]),
|
||||||
|
ok = emqx_metrics:inc(?AUTH_METRICS(failure)),
|
||||||
|
{stop, AuthResult#{auth_result => Error, anonymous => false}}
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
check_pass(Password, HashType) ->
|
||||||
|
case emqx_passwd:check_pass(Password, HashType) of
|
||||||
|
ok -> ok;
|
||||||
|
{error, _Reason} -> {error, not_authorized}
|
||||||
|
end.
|
||||||
|
|
||||||
|
description() -> "Authentication with MongoDB".
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Is Superuser?
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-spec(is_superuser(string(), maybe(#superquery{}), emqx_types:clientinfo()) -> boolean()).
|
||||||
|
is_superuser(_Pool, undefined, _ClientInfo) ->
|
||||||
|
false;
|
||||||
|
is_superuser(Pool, #superquery{collection = Coll, field = Field, selector = Selector}, ClientInfo) ->
|
||||||
|
Row = query(Pool, Coll, maps:from_list(replvars(Selector, ClientInfo))),
|
||||||
|
case maps:get(Field, Row, false) of
|
||||||
|
true -> true;
|
||||||
|
_False -> false
|
||||||
|
end.
|
||||||
|
|
||||||
|
replvars(VarList, ClientInfo) ->
|
||||||
|
lists:map(fun(Var) -> replvar(Var, ClientInfo) end, VarList).
|
||||||
|
|
||||||
|
replvar({Field, <<"%u">>}, #{username := Username}) ->
|
||||||
|
{Field, Username};
|
||||||
|
replvar({Field, <<"%c">>}, #{clientid := ClientId}) ->
|
||||||
|
{Field, ClientId};
|
||||||
|
replvar({Field, <<"%C">>}, #{cn := CN}) ->
|
||||||
|
{Field, CN};
|
||||||
|
replvar({Field, <<"%d">>}, #{dn := DN}) ->
|
||||||
|
{Field, DN};
|
||||||
|
replvar(Selector, _ClientInfo) ->
|
||||||
|
Selector.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% MongoDB Connect/Query
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
connect(Opts) ->
|
||||||
|
Type = proplists:get_value(type, Opts, single),
|
||||||
|
Hosts = proplists:get_value(hosts, Opts, []),
|
||||||
|
Options = proplists:get_value(options, Opts, []),
|
||||||
|
WorkerOptions = proplists:get_value(worker_options, Opts, []),
|
||||||
|
mongo_api:connect(Type, Hosts, Options, WorkerOptions).
|
||||||
|
|
||||||
|
query(Pool, Collection, Selector) ->
|
||||||
|
ecpool:with_client(Pool, fun(Conn) -> mongo_api:find_one(Conn, Collection, Selector, #{}) end).
|
||||||
|
|
||||||
|
query_multi(Pool, Collection, SelectorList) ->
|
||||||
|
lists:reverse(lists:flatten(lists:foldl(fun(Selector, Acc1) ->
|
||||||
|
Batch = ecpool:with_client(Pool, fun(Conn) ->
|
||||||
|
case mongo_api:find(Conn, Collection, Selector, #{}) of
|
||||||
|
[] -> [];
|
||||||
|
{ok, Cursor} ->
|
||||||
|
mc_cursor:foldl(fun(O, Acc2) -> [O|Acc2] end, [], Cursor, 1000)
|
||||||
|
end
|
||||||
|
end),
|
||||||
|
[Batch|Acc1]
|
||||||
|
end, [], SelectorList))).
|
|
@ -0,0 +1,87 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed 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.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_auth_mongo_app).
|
||||||
|
|
||||||
|
-behaviour(application).
|
||||||
|
|
||||||
|
-emqx_plugin(auth).
|
||||||
|
|
||||||
|
-include("emqx_auth_mongo.hrl").
|
||||||
|
|
||||||
|
-import(proplists, [get_value/3]).
|
||||||
|
|
||||||
|
%% Application callbacks
|
||||||
|
-export([ start/2
|
||||||
|
, prep_stop/1
|
||||||
|
, stop/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Application callbacks
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
start(_StartType, _StartArgs) ->
|
||||||
|
{ok, Sup} = emqx_auth_mongo_sup:start_link(),
|
||||||
|
with_env(auth_query, fun reg_authmod/1),
|
||||||
|
with_env(acl_query, fun reg_aclmod/1),
|
||||||
|
{ok, Sup}.
|
||||||
|
|
||||||
|
prep_stop(State) ->
|
||||||
|
ok = emqx:unhook('client.authenticate', fun emqx_auth_mongo:check/3),
|
||||||
|
ok = emqx:unhook('client.check_acl', fun emqx_acl_mongo:check_acl/5),
|
||||||
|
State.
|
||||||
|
|
||||||
|
stop(_State) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
reg_authmod(AuthQuery) ->
|
||||||
|
emqx_auth_mongo:register_metrics(),
|
||||||
|
SuperQuery = r(super_query, application:get_env(?APP, super_query, undefined)),
|
||||||
|
ok = emqx:hook('client.authenticate', fun emqx_auth_mongo:check/3,
|
||||||
|
[#{authquery => AuthQuery, superquery => SuperQuery, pool => ?APP}]).
|
||||||
|
|
||||||
|
reg_aclmod(AclQuery) ->
|
||||||
|
emqx_acl_mongo:register_metrics(),
|
||||||
|
ok = emqx:hook('client.check_acl', fun emqx_acl_mongo:check_acl/5, [#{aclquery => AclQuery, pool => ?APP}]).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Internal functions
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
with_env(Name, Fun) ->
|
||||||
|
case application:get_env(?APP, Name) of
|
||||||
|
undefined -> ok;
|
||||||
|
{ok, Config} -> Fun(r(Name, Config))
|
||||||
|
end.
|
||||||
|
|
||||||
|
r(super_query, undefined) ->
|
||||||
|
undefined;
|
||||||
|
r(super_query, Config) ->
|
||||||
|
#superquery{collection = list_to_binary(get_value(collection, Config, "mqtt_user")),
|
||||||
|
field = list_to_binary(get_value(super_field, Config, "is_superuser")),
|
||||||
|
selector = get_value(selector, Config, ?DEFAULT_SELECTORS)};
|
||||||
|
|
||||||
|
r(auth_query, Config) ->
|
||||||
|
#authquery{collection = list_to_binary(get_value(collection, Config, "mqtt_user")),
|
||||||
|
field = get_value(password_field, Config, [<<"password">>]),
|
||||||
|
hash = get_value(password_hash, Config, sha256),
|
||||||
|
selector = get_value(selector, Config, ?DEFAULT_SELECTORS)};
|
||||||
|
|
||||||
|
r(acl_query, Config) ->
|
||||||
|
#aclquery{collection = list_to_binary(get_value(collection, Config, "mqtt_acl")),
|
||||||
|
selector = get_value(selector, Config, [?DEFAULT_SELECTORS])}.
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed 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.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_auth_mongo_sup).
|
||||||
|
|
||||||
|
-behaviour(supervisor).
|
||||||
|
|
||||||
|
-include("emqx_auth_mongo.hrl").
|
||||||
|
|
||||||
|
-export([start_link/0]).
|
||||||
|
|
||||||
|
-export([init/1]).
|
||||||
|
|
||||||
|
start_link() ->
|
||||||
|
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||||
|
|
||||||
|
init([]) ->
|
||||||
|
{ok, PoolEnv} = application:get_env(?APP, server),
|
||||||
|
PoolSpec = ecpool:pool_spec(?APP, ?APP, ?APP, PoolEnv),
|
||||||
|
{ok, {{one_for_all, 10, 100}, [PoolSpec]}}.
|
||||||
|
|
|
@ -0,0 +1,174 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed 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.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_auth_mongo_SUITE).
|
||||||
|
|
||||||
|
-compile(export_all).
|
||||||
|
-compile(nowarn_export_all).
|
||||||
|
|
||||||
|
-include_lib("emqx/include/emqx.hrl").
|
||||||
|
-include_lib("common_test/include/ct.hrl").
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
|
-define(APP, emqx_auth_mongo).
|
||||||
|
|
||||||
|
-define(POOL(App), ecpool_worker:client(gproc_pool:pick_worker({ecpool, App}))).
|
||||||
|
|
||||||
|
-define(MONGO_CL_ACL, <<"mqtt_acl">>).
|
||||||
|
-define(MONGO_CL_USER, <<"mqtt_user">>).
|
||||||
|
|
||||||
|
-define(INIT_ACL, [{<<"username">>, <<"testuser">>, <<"clientid">>, <<"null">>, <<"subscribe">>, [<<"#">>]},
|
||||||
|
{<<"username">>, <<"dashboard">>, <<"clientid">>, <<"null">>, <<"pubsub">>, [<<"$SYS/#">>]},
|
||||||
|
{<<"username">>, <<"user3">>, <<"clientid">>, <<"null">>, <<"publish">>, [<<"a/b/c">>]}]).
|
||||||
|
|
||||||
|
-define(INIT_AUTH, [{<<"username">>, <<"plain">>, <<"password">>, <<"plain">>, <<"salt">>, <<"salt">>, <<"is_superuser">>, true},
|
||||||
|
{<<"username">>, <<"md5">>, <<"password">>, <<"1bc29b36f623ba82aaf6724fd3b16718">>, <<"salt">>, <<"salt">>, <<"is_superuser">>, false},
|
||||||
|
{<<"username">>, <<"sha">>, <<"password">>, <<"d8f4590320e1343a915b6394170650a8f35d6926">>, <<"salt">>, <<"salt">>, <<"is_superuser">>, false},
|
||||||
|
{<<"username">>, <<"sha256">>, <<"password">>, <<"5d5b09f6dcb2d53a5fffc60c4ac0d55fabdf556069d6631545f42aa6e3500f2e">>, <<"salt">>, <<"salt">>, <<"is_superuser">>, false},
|
||||||
|
{<<"username">>, <<"pbkdf2_password">>, <<"password">>, <<"cdedb5281bb2f801565a1122b2563515">>, <<"salt">>, <<"ATHENA.MIT.EDUraeburn">>, <<"is_superuser">>, false},
|
||||||
|
{<<"username">>, <<"bcrypt_foo">>, <<"password">>, <<"$2a$12$sSS8Eg.ovVzaHzi1nUHYK.HbUIOdlQI0iS22Q5rd5z.JVVYH6sfm6">>, <<"salt">>, <<"$2a$12$sSS8Eg.ovVzaHzi1nUHYK.">>, <<"is_superuser">>, false}
|
||||||
|
]).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Setups
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
all() ->
|
||||||
|
emqx_ct:all(?MODULE).
|
||||||
|
|
||||||
|
init_per_suite(Cfg) ->
|
||||||
|
emqx_ct_helpers:start_apps([emqx_auth_mongo], fun set_special_confs/1),
|
||||||
|
emqx_modules:load_module(emqx_mod_acl_internal, false),
|
||||||
|
init_mongo_data(),
|
||||||
|
Cfg.
|
||||||
|
|
||||||
|
end_per_suite(_Cfg) ->
|
||||||
|
deinit_mongo_data(),
|
||||||
|
emqx_ct_helpers:stop_apps([emqx_auth_mongo]).
|
||||||
|
|
||||||
|
set_special_confs(emqx) ->
|
||||||
|
application:set_env(emqx, acl_nomatch, deny),
|
||||||
|
application:set_env(emqx, acl_file,
|
||||||
|
emqx_ct_helpers:deps_path(emqx, "test/emqx_SUITE_data/acl.conf")),
|
||||||
|
application:set_env(emqx, allow_anonymous, false),
|
||||||
|
application:set_env(emqx, enable_acl_cache, false),
|
||||||
|
application:set_env(emqx, plugins_loaded_file,
|
||||||
|
emqx_ct_helpers:deps_path(emqx, "test/emqx_SUITE_data/loaded_plugins"));
|
||||||
|
set_special_confs(_App) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
init_mongo_data() ->
|
||||||
|
%% Users
|
||||||
|
{ok, Connection} = ?POOL(?APP),
|
||||||
|
mongo_api:delete(Connection, ?MONGO_CL_USER, {}),
|
||||||
|
?assertMatch({{true, _}, _}, mongo_api:insert(Connection, ?MONGO_CL_USER, ?INIT_AUTH)),
|
||||||
|
%% ACLs
|
||||||
|
mongo_api:delete(Connection, ?MONGO_CL_ACL, {}),
|
||||||
|
?assertMatch({{true, _}, _}, mongo_api:insert(Connection, ?MONGO_CL_ACL, ?INIT_ACL)).
|
||||||
|
|
||||||
|
deinit_mongo_data() ->
|
||||||
|
{ok, Connection} = ?POOL(?APP),
|
||||||
|
mongo_api:delete(Connection, ?MONGO_CL_USER, {}),
|
||||||
|
mongo_api:delete(Connection, ?MONGO_CL_ACL, {}).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Test cases
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
t_check_auth(_) ->
|
||||||
|
Plain = #{zone => external, clientid => <<"client1">>, username => <<"plain">>},
|
||||||
|
Plain1 = #{zone => external, clientid => <<"client1">>, username => <<"plain2">>},
|
||||||
|
Md5 = #{zone => external, clientid => <<"md5">>, username => <<"md5">>},
|
||||||
|
Sha = #{zone => external, clientid => <<"sha">>, username => <<"sha">>},
|
||||||
|
Sha256 = #{zone => external, clientid => <<"sha256">>, username => <<"sha256">>},
|
||||||
|
Pbkdf2 = #{zone => external, clientid => <<"pbkdf2_password">>, username => <<"pbkdf2_password">>},
|
||||||
|
Bcrypt = #{zone => external, clientid => <<"bcrypt_foo">>, username => <<"bcrypt_foo">>},
|
||||||
|
User1 = #{zone => external, clientid => <<"bcrypt_foo">>, username => <<"user">>},
|
||||||
|
reload({auth_query, [{password_hash, plain}]}),
|
||||||
|
%% With exactly username/password, connection success
|
||||||
|
{ok, #{is_superuser := true}} = emqx_access_control:authenticate(Plain#{password => <<"plain">>}),
|
||||||
|
%% With exactly username and wrong password, connection fail
|
||||||
|
{error, _} = emqx_access_control:authenticate(Plain#{password => <<"error_pwd">>}),
|
||||||
|
%% With wrong username and wrong password, emqx_auth_mongo auth fail, then allow anonymous authentication
|
||||||
|
{error, _} = emqx_access_control:authenticate(Plain1#{password => <<"error_pwd">>}),
|
||||||
|
%% With wrong username and exactly password, emqx_auth_mongo auth fail, then allow anonymous authentication
|
||||||
|
{error, _} = emqx_access_control:authenticate(Plain1#{password => <<"plain">>}),
|
||||||
|
reload({auth_query, [{password_hash, md5}]}),
|
||||||
|
{ok, #{is_superuser := false}} = emqx_access_control:authenticate(Md5#{password => <<"md5">>}),
|
||||||
|
reload({auth_query, [{password_hash, sha}]}),
|
||||||
|
{ok, #{is_superuser := false}} = emqx_access_control:authenticate(Sha#{password => <<"sha">>}),
|
||||||
|
reload({auth_query, [{password_hash, sha256}]}),
|
||||||
|
{ok, #{is_superuser := false}} = emqx_access_control:authenticate(Sha256#{password => <<"sha256">>}),
|
||||||
|
%%pbkdf2 sha
|
||||||
|
reload({auth_query, [{password_hash, {pbkdf2, sha, 1, 16}}, {password_field, [<<"password">>, <<"salt">>]}]}),
|
||||||
|
{ok, #{is_superuser := false}} = emqx_access_control:authenticate(Pbkdf2#{password => <<"password">>}),
|
||||||
|
reload({auth_query, [{password_hash, {salt, bcrypt}}]}),
|
||||||
|
{ok, #{is_superuser := false}} = emqx_access_control:authenticate(Bcrypt#{password => <<"foo">>}),
|
||||||
|
{error, _} = emqx_access_control:authenticate(User1#{password => <<"foo">>}).
|
||||||
|
|
||||||
|
t_check_acl(_) ->
|
||||||
|
{ok, Connection} = ?POOL(?APP),
|
||||||
|
User1 = #{zone => external, clientid => <<"client1">>, username => <<"testuser">>},
|
||||||
|
User2 = #{zone => external, clientid => <<"client2">>, username => <<"dashboard">>},
|
||||||
|
User3 = #{zone => external, clientid => <<"client2">>, username => <<"user3">>},
|
||||||
|
User4 = #{zone => external, clientid => <<"$$client2">>, username => <<"$$user3">>},
|
||||||
|
3 = mongo_api:count(Connection, ?MONGO_CL_ACL, {}, 17),
|
||||||
|
%% ct log output
|
||||||
|
allow = emqx_access_control:check_acl(User1, subscribe, <<"users/testuser/1">>),
|
||||||
|
deny = emqx_access_control:check_acl(User1, subscribe, <<"$SYS/testuser/1">>),
|
||||||
|
deny = emqx_access_control:check_acl(User2, subscribe, <<"a/b/c">>),
|
||||||
|
allow = emqx_access_control:check_acl(User2, subscribe, <<"$SYS/testuser/1">>),
|
||||||
|
allow = emqx_access_control:check_acl(User3, publish, <<"a/b/c">>),
|
||||||
|
deny = emqx_access_control:check_acl(User3, publish, <<"c">>),
|
||||||
|
allow = emqx_access_control:check_acl(User4, publish, <<"a/b/c">>).
|
||||||
|
|
||||||
|
t_acl_super(_) ->
|
||||||
|
reload({auth_query, [{password_hash, plain}, {password_field, [<<"password">>]}]}),
|
||||||
|
{ok, C} = emqtt:start_link([{clientid, <<"simpleClient">>},
|
||||||
|
{username, <<"plain">>},
|
||||||
|
{password, <<"plain">>}]),
|
||||||
|
{ok, _} = emqtt:connect(C),
|
||||||
|
timer:sleep(10),
|
||||||
|
emqtt:subscribe(C, <<"TopicA">>, qos2),
|
||||||
|
timer:sleep(1000),
|
||||||
|
emqtt:publish(C, <<"TopicA">>, <<"Payload">>, qos2),
|
||||||
|
timer:sleep(1000),
|
||||||
|
receive
|
||||||
|
{publish, #{payload := Payload}} ->
|
||||||
|
?assertEqual(<<"Payload">>, Payload)
|
||||||
|
after
|
||||||
|
1000 ->
|
||||||
|
ct:fail({receive_timeout, <<"Payload">>}),
|
||||||
|
ok
|
||||||
|
end,
|
||||||
|
emqtt:disconnect(C).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Utils
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
reload({Par, Vals}) when is_list(Vals) ->
|
||||||
|
application:stop(?APP),
|
||||||
|
{ok, TupleVals} = application:get_env(?APP, Par),
|
||||||
|
NewVals =
|
||||||
|
lists:filtermap(fun({K, V}) ->
|
||||||
|
case lists:keymember(K, 1, Vals) of
|
||||||
|
false ->{true, {K, V}};
|
||||||
|
_ -> false
|
||||||
|
end
|
||||||
|
end, TupleVals),
|
||||||
|
application:set_env(?APP, Par, lists:append(NewVals, Vals)),
|
||||||
|
application:start(?APP).
|
|
@ -0,0 +1,27 @@
|
||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEpAIBAAKCAQEA0kGUBi9NDp65jgdxKfizIfuSr2wpwb44yM9SuP4oUQSULOA2
|
||||||
|
4iFpLR/c5FAYHU81y9Vx91dQjdZfffaBZuv2zVvteXUkol8Nez7boKbo2E41MTew
|
||||||
|
8edtNKZAQVvnaHAC2NCZxjchCzUCDEoUUcl+cIERZ8R48FBqK5iTVcMRIx1akwus
|
||||||
|
+dhBqP0ykA5TGOWZkJrLM9aUXSPQha9+wXlOpkvu0Ur2nkX8PPJnifWao9UShSar
|
||||||
|
ll1IqPZNCSlZMwcFYcQNBCpdvITUUYlHvMRQV64bUpOxUGDuJkQL3dLKBlNuBRlJ
|
||||||
|
BcjBAKw7rFnwwHZcMmQ9tan/dZzpzwjo/T0XjwIDAQABAoIBAQCSHvUqnzDkWjcG
|
||||||
|
l/Fzg92qXlYBCCC0/ugj1sHcwvVt6Mq5rVE3MpUPwTcYjPlVVTlD4aEEjm/zQuq2
|
||||||
|
ddxUlOS+r4aIhHrjRT/vSS4FpjnoKeIZxGR6maVxk6DQS3i1QjMYT1CvSpzyVvKH
|
||||||
|
a+xXMrtmoKxh+085ZAmFJtIuJhUA2yEa4zggCxWnvz8ecLClUPfVDPhdLBHc3KmL
|
||||||
|
CRpHEC6L/wanvDPRdkkzfKyaJuIJlTDaCg63AY5sDkTW2I57iI/nJ3haSeidfQKz
|
||||||
|
39EfbnM1A/YprIakafjAu3frBIsjBVcxwGihZmL/YriTHjOggJF841kT5zFkkv2L
|
||||||
|
/530Wk6xAoGBAOqZLZ4DIi/zLndEOz1mRbUfjc7GQUdYplBnBwJ22VdS0P4TOXnd
|
||||||
|
UbJth2MA92NM7ocTYVFl4TVIZY/Y+Prxk7KQdHWzR7JPpKfx9OEVgtSqV0vF9eGI
|
||||||
|
rKp79Y1T4Mvc3UcQCXX6TP7nHLihEzpS8odm2LW4txrOiLsn4Fq/IWrLAoGBAOVv
|
||||||
|
6U4tm3lImotUupKLZPKEBYwruo9qRysoug9FiorP4TjaBVOfltiiHbAQD6aGfVtN
|
||||||
|
SZpZZtrs17wL7Xl4db5asgMcZd+8Hkfo5siR7AuGW9FZloOjDcXb5wCh9EvjJ74J
|
||||||
|
Cjw7RqyVymq9t7IP6wnVwj5Ck48YhlOZCz/mzlnNAoGAWq7NYFgLvgc9feLFF23S
|
||||||
|
IjpJQZWHJEITP98jaYNxbfzYRm49+GphqxwFinKULjFNvq7yHlnIXSVYBOu1CqOZ
|
||||||
|
GRwXuGuNmlKI7lZr9xmukfAqgGLMMdr4C4qRF4lFyufcLRz42z7exmWlx4ST/yaT
|
||||||
|
E13hBRWayeTuG5JFei6Jh1MCgYEAqmX4LyC+JFBgvvQZcLboLRkSCa18bADxhENG
|
||||||
|
FAuAvmFvksqRRC71WETmqZj0Fqgxt7pp3KFjO1rFSprNLvbg85PmO1s+6fCLyLpX
|
||||||
|
lESTu2d5D71qhK93jigooxalGitFm+SY3mzjq0/AOpBWOn+J/w7rqVPGxXLgaHv0
|
||||||
|
l+vx+00CgYBOvo9/ImjwYii2jFl+sHEoCzlvpITi2temRlT2j6ulSjCLJgjwEFw9
|
||||||
|
8e+vvfQumQOsutakUVyURrkMGNDiNlIv8kv5YLCCkrwN22E6Ghyi69MJUvHQXkc/
|
||||||
|
QZhjn/luyfpB5f/BeHFS2bkkxAXo+cfG45ApY3Qfz6/7o+H+vDa6/A==
|
||||||
|
-----END RSA PRIVATE KEY-----
|
|
@ -0,0 +1,19 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDAzCCAeugAwIBAgIBATANBgkqhkiG9w0BAQsFADA8MTowOAYDVQQDDDFNeVNR
|
||||||
|
TF9TZXJ2ZXJfOC4wLjE5X0F1dG9fR2VuZXJhdGVkX0NBX0NlcnRpZmljYXRlMB4X
|
||||||
|
DTIwMDYxMTAzMzg0NloXDTMwMDYwOTAzMzg0NlowPDE6MDgGA1UEAwwxTXlTUUxf
|
||||||
|
U2VydmVyXzguMC4xOV9BdXRvX0dlbmVyYXRlZF9DQV9DZXJ0aWZpY2F0ZTCCASIw
|
||||||
|
DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANJBlAYvTQ6euY4HcSn4syH7kq9s
|
||||||
|
KcG+OMjPUrj+KFEElCzgNuIhaS0f3ORQGB1PNcvVcfdXUI3WX332gWbr9s1b7Xl1
|
||||||
|
JKJfDXs+26Cm6NhONTE3sPHnbTSmQEFb52hwAtjQmcY3IQs1AgxKFFHJfnCBEWfE
|
||||||
|
ePBQaiuYk1XDESMdWpMLrPnYQaj9MpAOUxjlmZCayzPWlF0j0IWvfsF5TqZL7tFK
|
||||||
|
9p5F/DzyZ4n1mqPVEoUmq5ZdSKj2TQkpWTMHBWHEDQQqXbyE1FGJR7zEUFeuG1KT
|
||||||
|
sVBg7iZEC93SygZTbgUZSQXIwQCsO6xZ8MB2XDJkPbWp/3Wc6c8I6P09F48CAwEA
|
||||||
|
AaMQMA4wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEADKz6bIpP5anp
|
||||||
|
GgLB0jkclRWuMlS4qqIt4itSsMXPJ/ezpHwECixmgW2TIQl6S1woRkUeMxhT2/Ay
|
||||||
|
Sn/7aKxuzRagyE5NEGOvrOuAP5RO2ZdNJ/X3/Rh533fK1sOTEEbSsWUvW6iSkZef
|
||||||
|
rsfZBVP32xBhRWkKRdLeLB4W99ADMa0IrTmZPCXHSSE2V4e1o6zWLXcOZeH1Qh8N
|
||||||
|
SkelBweR+8r1Fbvy1r3s7eH7DCbYoGEDVLQGOLvzHKBisQHmoDnnF5E9g1eeNRdg
|
||||||
|
o+vhOKfYCOzeNREJIqS42PHcGhdNRk90ycigPmfUJclz1mDHoMjKR2S5oosTpr65
|
||||||
|
tNPx3CL7GA==
|
||||||
|
-----END CERTIFICATE-----
|
|
@ -0,0 +1,19 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDBDCCAeygAwIBAgIBAzANBgkqhkiG9w0BAQsFADA8MTowOAYDVQQDDDFNeVNR
|
||||||
|
TF9TZXJ2ZXJfOC4wLjE5X0F1dG9fR2VuZXJhdGVkX0NBX0NlcnRpZmljYXRlMB4X
|
||||||
|
DTIwMDYxMTAzMzg0N1oXDTMwMDYwOTAzMzg0N1owQDE+MDwGA1UEAww1TXlTUUxf
|
||||||
|
U2VydmVyXzguMC4xOV9BdXRvX0dlbmVyYXRlZF9DbGllbnRfQ2VydGlmaWNhdGUw
|
||||||
|
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVYSWpOvCTupz82fc85Opv
|
||||||
|
EQ7rkB8X2oOMyBCpkyHKBIr1ZQgRDWBp9UVOASq3GnSElm6+T3Kb1QbOffa8GIlw
|
||||||
|
sjAueKdq5L2eSkmPIEQ7eoO5kEW+4V866hE1LeL/PmHg2lGP0iqZiJYtElhHNQO8
|
||||||
|
3y9I7cm3xWMAA3SSWikVtpJRn3qIp2QSrH+tK+/HHbE5QwtPxdir4ULSCSOaM5Yh
|
||||||
|
Wi5Oto88TZqe1v7SXC864JVvO4LuS7TuSreCdWZyPXTJFBFeCEWSAxonKZrqHbBe
|
||||||
|
CwKML6/0NuzjaQ51c2tzmVI6xpHj3nnu4cSRx6Jf9WBm+35vm0wk4pohX3ptdzeV
|
||||||
|
AgMBAAGjDTALMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEBAByQ5zSNeFUH
|
||||||
|
Aw7JlpZHtHaSEeiiyBHke20ziQ07BK1yi/ms2HAWwQkpZv149sjNuIRH8pkTmkZn
|
||||||
|
g8PDzSefjLbC9AsWpWV0XNV22T/cdobqLqMBDDZ2+5bsV+jTrOigWd9/AHVZ93PP
|
||||||
|
IJN8HJn6rtvo2l1bh/CdsX14uVSdofXnuWGabNTydqtMvmCerZsdf6qKqLL+PYwm
|
||||||
|
RDpgWiRUY7KPBSSlKm/9lJzA+bOe4dHeJzxWFVCJcbpoiTFs1je1V8kKQaHtuW39
|
||||||
|
ifX6LTKUMlwEECCbDKM8Yq2tm8NjkjCcnFDtKg8zKGPUu+jrFMN5otiC3wnKcP7r
|
||||||
|
O9EkaPcgYH8=
|
||||||
|
-----END CERTIFICATE-----
|
|
@ -0,0 +1,27 @@
|
||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEowIBAAKCAQEA1WElqTrwk7qc/Nn3POTqbxEO65AfF9qDjMgQqZMhygSK9WUI
|
||||||
|
EQ1gafVFTgEqtxp0hJZuvk9ym9UGzn32vBiJcLIwLninauS9nkpJjyBEO3qDuZBF
|
||||||
|
vuFfOuoRNS3i/z5h4NpRj9IqmYiWLRJYRzUDvN8vSO3Jt8VjAAN0klopFbaSUZ96
|
||||||
|
iKdkEqx/rSvvxx2xOUMLT8XYq+FC0gkjmjOWIVouTraPPE2antb+0lwvOuCVbzuC
|
||||||
|
7ku07kq3gnVmcj10yRQRXghFkgMaJyma6h2wXgsCjC+v9Dbs42kOdXNrc5lSOsaR
|
||||||
|
49557uHEkceiX/VgZvt+b5tMJOKaIV96bXc3lQIDAQABAoIBAF7yjXmSOn7h6P0y
|
||||||
|
WCuGiTLG2mbDiLJqj2LTm2Z5i+2Cu/qZ7E76Ls63TxF4v3MemH5vGfQhEhR5ZD/6
|
||||||
|
GRJ1sKKvB3WGRqjwA9gtojHH39S/nWGy6vYW/vMOOH37XyjIr3EIdIaUtFQBTSHd
|
||||||
|
Kd71niYrAbVn6fyWHolhADwnVmTMOl5OOAhCdEF4GN3b5aIhIu8BJ7EUzTtHBJIj
|
||||||
|
CAEfjZFjDs1y1cIgGFJkuIQxMfCpq5recU2qwip7YO6fk//WEjOPu7kSf5IEswL8
|
||||||
|
jg1dea9rGBV6KaD2xsgsC6Ll6Sb4BbsrHMfflG3K2Lk3RdVqqTFp1Fn1PTLQE/1S
|
||||||
|
S/SZPYECgYEA9qYcHKHd0+Q5Ty5wgpxKGa4UCWkpwvfvyv4bh8qlmxueB+l2AIdo
|
||||||
|
ZvkM8gTPagPQ3WypAyC2b9iQu70uOJo1NizTtKnpjDdN1YpDjISJuS/P0x73gZwy
|
||||||
|
gmoM5AzMtN4D6IbxXtXnPaYICvwLKU80ouEN5ZPM4/ODLUu6gsp0v2UCgYEA3Xgi
|
||||||
|
zMC4JF0vEKEaK0H6QstaoXUmw/lToZGH3TEojBIkb/2LrHUclygtONh9kJSFb89/
|
||||||
|
jbmRRLAOrx3HZKCNGUmF4H9k5OQyAIv6OGBinvLGqcbqnyNlI+Le8zxySYwKMlEj
|
||||||
|
EMrBCLmSyi0CGFrbZ3mlj/oCET/ql9rNvcK+DHECgYAEx5dH3sMjtgp+RFId1dWB
|
||||||
|
xePRgt4yTwewkVgLO5wV82UOljGZNQaK6Eyd7AXw8f38LHzh+KJQbIvxd2sL4cEi
|
||||||
|
OaAoohpKg0/Y0YMZl//rPMf0OWdmdZZs/I0fZjgZUSwWN3c59T8z7KG/RL8an9RP
|
||||||
|
S7kvN7wCttdV61/D5RR6GQKBgDxCe/WKWpBKaovzydMLWLTj7/0Oi0W3iXHkzzr4
|
||||||
|
LTgvl4qBSofaNbVLUUKuZTv5rXUG2IYPf99YqCYtzBstNDc1MiAriaBeFtzfOW4t
|
||||||
|
i6gEFtoLLbuvPc3N5Sv5vn8Ug5G9UfU3td5R4AbyyCcoUZqOFuZd+EIJSiOXfXOs
|
||||||
|
kVmBAoGBAIU9aPAqhU5LX902oq8KsrpdySONqv5mtoStvl3wo95WIqXNEsFY60wO
|
||||||
|
q02jKQmJJ2MqhkJm2EoF2Mq8+40EZ5sz8LdgeQ/M0yQ9lAhPi4rftwhpe55Ma9dk
|
||||||
|
SE9X1c/DMCBEaIjJqVXdy0/EeArwpb8sHkguVVAZUWxzD+phm1gs
|
||||||
|
-----END RSA PRIVATE KEY-----
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue