├── .github └── workflows │ └── run_test_cases.yaml ├── .gitignore ├── CHANGES ├── LICENSE ├── Makefile ├── README.md ├── docker-compose-ssl.yml ├── docker-compose.yml ├── etc └── emqx_auth_mongo.conf ├── include └── emqx_auth_mongo.hrl ├── priv └── emqx_auth_mongo.schema ├── rebar.config ├── rebar.config.script ├── src ├── emqx_acl_mongo.erl ├── emqx_auth_mongo.app.src ├── emqx_auth_mongo.app.src.script ├── emqx_auth_mongo.appup.src ├── emqx_auth_mongo.erl ├── emqx_auth_mongo_app.erl └── emqx_auth_mongo_sup.erl └── test ├── emqx_auth_mongo_SUITE.erl └── emqx_auth_mongo_SUITE_data ├── ca-key.pem ├── ca.pem ├── client-cert.pem ├── client-key.pem ├── mongodb.pem ├── private_key.pem ├── public_key.pem ├── server-cert.pem └── server-key.pem /.github/workflows/run_test_cases.yaml: -------------------------------------------------------------------------------- 1 | name: Run test cases 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | run_test_cases: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | mongo_tag: 12 | - 3 13 | - 4 14 | network_type: 15 | - ipv4 16 | - ipv6 17 | connect_type: 18 | - ssl 19 | - tcp 20 | 21 | steps: 22 | - name: install docker-compose 23 | run: | 24 | 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 25 | sudo chmod +x /usr/local/bin/docker-compose 26 | - uses: actions/checkout@v1 27 | - name: run test cases 28 | env: 29 | MONGO_TAG: ${{ matrix.mongo_tag }} 30 | NETWORK_TYPE: ${{ matrix.network_type }} 31 | CONNECT_TYPE: ${{ matrix.connect_type }} 32 | run: | 33 | set -e -u -x 34 | if [ "$NETWORK_TYPE" = "ipv6" ];then docker network create --driver bridge --ipv6 --subnet fd15:555::/64 tests_emqx_bridge --attachable; fi 35 | if [ "$CONNECT_TYPE" = "ssl" ]; then 36 | docker-compose -f ./docker-compose-ssl.yml -p tests up -d 37 | 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" 38 | 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" 39 | 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" 40 | 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" 41 | else 42 | docker-compose -f ./docker-compose.yml -p tests up -d 43 | fi 44 | if [ "$NETWORK_TYPE" != "ipv6" ];then 45 | 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" 46 | else 47 | ipv6_address=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.GlobalIPv6Address}}{{end}}' $(docker ps -a -f name=tests_mongo_server_1 -q)) 48 | 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" 49 | fi 50 | docker exec -i tests_erlang_1 sh -c "make -C /emqx_auth_mongo xref" 51 | docker exec -i tests_erlang_1 sh -c "make -C /emqx_auth_mongo eunit" 52 | docker exec -i tests_erlang_1 sh -c "make -C /emqx_auth_mongo ct" 53 | docker exec -i tests_erlang_1 sh -c "make -C /emqx_auth_mongo cover" 54 | - uses: actions/upload-artifact@v1 55 | if: failure() 56 | with: 57 | name: logs_mongo${{ matrix.mongo_tag}}_${{ matrix.network_type }} 58 | path: _build/test/logs 59 | 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .eunit 2 | deps 3 | *.o 4 | *.beam 5 | *.plt 6 | erl_crash.dump 7 | ebin 8 | rel/example_project 9 | .concrete/DEV_MODE 10 | .rebar 11 | .DS_Store 12 | .erlang.mk/ 13 | emqx_auth_mongo.d 14 | ct.coverdata 15 | logs/ 16 | test/ct.cover.spec 17 | data/ 18 | cover/ 19 | eunit.coverdata 20 | _build/ 21 | rebar.lock 22 | erlang.mk 23 | etc/emqx_auth_mongo.conf.rendered 24 | .rebar3 25 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | 2 | 2.0.7 (2017-01-20) 3 | ------------------ 4 | 5 | Tag 2.0.7 - use `cuttlefish:unset()` for commented ACL/super config 6 | 7 | 2.0.1 (2016-11-30) 8 | ------------------ 9 | 10 | Tag 2.0.1 11 | 12 | 2.0-beta.1 (2016-08-24) 13 | ----------------------- 14 | 15 | gen_conf 16 | 17 | 1.1.3-beta (2016-08-19) 18 | ----------------------- 19 | 20 | Bump version to 1.1.3 21 | 22 | 1.1.2-beta (2016-06-30) 23 | ----------------------- 24 | 25 | Bump version to 1.1.2 26 | 27 | 1.1-beta (2016-05-28) 28 | --------------------- 29 | 30 | First public release 31 | 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ## shallow clone for speed 2 | 3 | REBAR_GIT_CLONE_OPTIONS += --depth 1 4 | export REBAR_GIT_CLONE_OPTIONS 5 | 6 | REBAR = rebar3 7 | all: compile 8 | 9 | compile: 10 | $(REBAR) compile 11 | 12 | clean: distclean 13 | 14 | ct: compile 15 | $(REBAR) as test ct -v 16 | 17 | eunit: compile 18 | $(REBAR) as test eunit 19 | 20 | xref: 21 | $(REBAR) xref 22 | 23 | cover: 24 | $(REBAR) cover 25 | 26 | distclean: 27 | @rm -rf _build 28 | @rm -f data/app.*.config data/vm.*.args rebar.lock 29 | 30 | CUTTLEFISH_SCRIPT = _build/default/lib/cuttlefish/cuttlefish 31 | 32 | $(CUTTLEFISH_SCRIPT): 33 | @${REBAR} get-deps 34 | @if [ ! -f cuttlefish ]; then make -C _build/default/lib/cuttlefish; fi 35 | 36 | app.config: $(CUTTLEFISH_SCRIPT) etc/emqx_auth_mongo.conf 37 | $(verbose) $(CUTTLEFISH_SCRIPT) -l info -e etc/ -c etc/emqx_auth_mongo.conf -i priv/emqx_auth_mongo.schema -d data 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | emqx_auth_mongo 2 | =============== 3 | 4 | EMQX Authentication/ACL with MongoDB 5 | 6 | Build the Plugin 7 | ---------------- 8 | 9 | ``` 10 | make & make tests 11 | ``` 12 | 13 | Configuration 14 | ------------- 15 | 16 | File: etc/emqx_auth_mongo.conf 17 | 18 | ``` 19 | ## MongoDB Topology Type. 20 | ## 21 | ## Value: single | unknown | sharded | rs 22 | auth.mongo.type = single 23 | 24 | ## Sets the set name if type is rs. 25 | ## 26 | ## Value: String 27 | ## auth.mongo.rs_set_name = 28 | 29 | ## MongoDB server list. 30 | ## 31 | ## Value: String 32 | ## 33 | ## Examples: 127.0.0.1:27017,127.0.0.2:27017... 34 | auth.mongo.server = 127.0.0.1:27017 35 | 36 | ## MongoDB pool size 37 | ## 38 | ## Value: Number 39 | auth.mongo.pool = 8 40 | 41 | ## MongoDB login user. 42 | ## 43 | ## Value: String 44 | ## auth.mongo.login = 45 | 46 | ## MongoDB password. 47 | ## 48 | ## Value: String 49 | ## auth.mongo.password = 50 | 51 | ## MongoDB AuthSource 52 | ## 53 | ## Value: String 54 | ## Default: mqtt 55 | ## auth.mongo.auth_source = admin 56 | 57 | ## MongoDB database 58 | ## 59 | ## Value: String 60 | auth.mongo.database = mqtt 61 | 62 | ## MongoDB write mode. 63 | ## 64 | ## Value: unsafe | safe 65 | ## auth.mongo.w_mode = 66 | 67 | ## Mongo read mode. 68 | ## 69 | ## Value: master | slave_ok 70 | ## auth.mongo.r_mode = 71 | 72 | ## MongoDB topology options. 73 | auth.mongo.topology.pool_size = 1 74 | auth.mongo.topology.max_overflow = 0 75 | ## auth.mongo.topology.overflow_ttl = 1000 76 | ## auth.mongo.topology.overflow_check_period = 1000 77 | ## auth.mongo.topology.local_threshold_ms = 1000 78 | ## auth.mongo.topology.connect_timeout_ms = 20000 79 | ## auth.mongo.topology.socket_timeout_ms = 100 80 | ## auth.mongo.topology.server_selection_timeout_ms = 30000 81 | ## auth.mongo.topology.wait_queue_timeout_ms = 1000 82 | ## auth.mongo.topology.heartbeat_frequency_ms = 10000 83 | ## auth.mongo.topology.min_heartbeat_frequency_ms = 1000 84 | 85 | ## Authentication query. 86 | auth.mongo.auth_query.collection = mqtt_user 87 | 88 | auth.mongo.auth_query.password_field = password 89 | 90 | ## Password hash. 91 | ## 92 | ## Value: plain | md5 | sha | sha256 | bcrypt 93 | auth.mongo.auth_query.password_hash = sha256 94 | 95 | ## sha256 with salt suffix 96 | ## auth.mongo.auth_query.password_hash = sha256,salt 97 | 98 | ## sha256 with salt prefix 99 | ## auth.mongo.auth_query.password_hash = salt,sha256 100 | 101 | ## bcrypt with salt prefix 102 | ## auth.mongo.auth_query.password_hash = salt,bcrypt 103 | 104 | ## pbkdf2 with macfun iterations dklen 105 | ## macfun: md4, md5, ripemd160, sha, sha224, sha256, sha384, sha512 106 | ## auth.mongo.auth_query.password_hash = pbkdf2,sha256,1000,20 107 | 108 | auth.mongo.auth_query.selector = username=%u 109 | 110 | ## Enable superuser query. 111 | auth.mongo.super_query = on 112 | 113 | auth.mongo.super_query.collection = mqtt_user 114 | 115 | auth.mongo.super_query.super_field = is_superuser 116 | 117 | auth.mongo.super_query.selector = username=%u 118 | 119 | ## Enable ACL query. 120 | auth.mongo.acl_query = on 121 | 122 | auth.mongo.acl_query.collection = mqtt_acl 123 | 124 | auth.mongo.acl_query.selector = username=%u 125 | ``` 126 | 127 | Load the Plugin 128 | --------------- 129 | 130 | ``` 131 | ./bin/emqx_ctl plugins load emqx_auth_mongo 132 | ``` 133 | 134 | MongoDB Database 135 | ---------------- 136 | 137 | ``` 138 | use mqtt 139 | db.createCollection("mqtt_user") 140 | db.createCollection("mqtt_acl") 141 | db.mqtt_user.ensureIndex({"username":1}) 142 | ``` 143 | 144 | mqtt_user Collection 145 | -------------------- 146 | 147 | ``` 148 | { 149 | username: "user", 150 | password: "password hash", 151 | salt: "password salt", 152 | is_superuser: boolean (true, false), 153 | created: "datetime" 154 | } 155 | ``` 156 | 157 | For example: 158 | ``` 159 | db.mqtt_user.insert({username: "test", password: "password hash", salt: "password salt", is_superuser: false}) 160 | db.mqtt_user.insert({username: "root", is_superuser: true}) 161 | ``` 162 | 163 | mqtt_acl Collection 164 | ------------------- 165 | 166 | ``` 167 | { 168 | username: "username", 169 | clientid: "clientid", 170 | publish: ["topic1", "topic2", ...], 171 | subscribe: ["subtop1", "subtop2", ...], 172 | pubsub: ["topic/#", "topic1", ...] 173 | } 174 | ``` 175 | 176 | For example: 177 | 178 | ``` 179 | db.mqtt_acl.insert({username: "test", publish: ["t/1", "t/2"], subscribe: ["user/%u", "client/%c"]}) 180 | db.mqtt_acl.insert({username: "admin", pubsub: ["#"]}) 181 | ``` 182 | 183 | License 184 | ------- 185 | 186 | Apache License Version 2.0 187 | 188 | Author 189 | ------ 190 | 191 | EMQX Team. 192 | 193 | -------------------------------------------------------------------------------- /docker-compose-ssl.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | erlang: 5 | image: erlang:22.1 6 | volumes: 7 | - ./:/emqx_auth_mongo 8 | networks: 9 | - emqx_bridge 10 | depends_on: 11 | - mongo_server 12 | tty: true 13 | 14 | mongo_server: 15 | image: mongo:${MONGO_TAG} 16 | restart: always 17 | environment: 18 | MONGO_INITDB_DATABASE: mqtt 19 | volumes: 20 | - ./test/emqx_auth_mongo_SUITE_data/mongodb.pem/:/etc/certs/mongodb.pem 21 | networks: 22 | - emqx_bridge 23 | command: 24 | --ipv6 25 | --bind_ip_all 26 | --sslMode requireSSL 27 | --sslPEMKeyFile /etc/certs/mongodb.pem 28 | 29 | networks: 30 | emqx_bridge: 31 | driver: bridge 32 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | erlang: 5 | image: erlang:22.1 6 | volumes: 7 | - ./:/emqx_auth_mongo 8 | networks: 9 | - emqx_bridge 10 | depends_on: 11 | - mongo_server 12 | tty: true 13 | 14 | mongo_server: 15 | image: mongo:${MONGO_TAG} 16 | restart: always 17 | environment: 18 | MONGO_INITDB_DATABASE: mqtt 19 | networks: 20 | - emqx_bridge 21 | command: 22 | --ipv6 23 | --bind_ip_all 24 | 25 | networks: 26 | emqx_bridge: 27 | driver: bridge 28 | -------------------------------------------------------------------------------- /etc/emqx_auth_mongo.conf: -------------------------------------------------------------------------------- 1 | ##-------------------------------------------------------------------- 2 | ## MongoDB Auth/ACL Plugin 3 | ##-------------------------------------------------------------------- 4 | 5 | ## MongoDB Topology Type. 6 | ## 7 | ## Value: single | unknown | sharded | rs 8 | auth.mongo.type = single 9 | 10 | ## The set name if type is rs. 11 | ## 12 | ## Value: String 13 | ## auth.mongo.rs_set_name = 14 | 15 | ## MongoDB server list. 16 | ## 17 | ## Value: String 18 | ## 19 | ## Examples: 127.0.0.1:27017,127.0.0.2:27017... 20 | auth.mongo.server = 127.0.0.1:27017 21 | 22 | ## MongoDB pool size 23 | ## 24 | ## Value: Number 25 | auth.mongo.pool = 8 26 | 27 | ## MongoDB login user. 28 | ## 29 | ## Value: String 30 | ## auth.mongo.login = 31 | 32 | ## MongoDB password. 33 | ## 34 | ## Value: String 35 | ## auth.mongo.password = 36 | 37 | ## MongoDB AuthSource 38 | ## 39 | ## Value: String 40 | ## Default: mqtt 41 | ## auth.mongo.auth_source = admin 42 | 43 | ## MongoDB database 44 | ## 45 | ## Value: String 46 | auth.mongo.database = mqtt 47 | 48 | ## MongoDB query timeout 49 | ## 50 | ## Value: Duration 51 | ## auth.mongo.query_timeout = 5s 52 | 53 | ## Whether to enable SSL connection. 54 | ## 55 | ## Value: true | false 56 | ## auth.mongo.ssl = false 57 | 58 | ## SSL keyfile. 59 | ## 60 | ## Value: File 61 | ## auth.mongo.ssl_opts.keyfile = 62 | 63 | ## SSL certfile. 64 | ## 65 | ## Value: File 66 | ## auth.mongo.ssl_opts.certfile = 67 | 68 | ## SSL cacertfile. 69 | ## 70 | ## Value: File 71 | ## auth.mongo.ssl_opts.cacertfile = 72 | 73 | ## MongoDB write mode. 74 | ## 75 | ## Value: unsafe | safe 76 | ## auth.mongo.w_mode = 77 | 78 | ## Mongo read mode. 79 | ## 80 | ## Value: master | slave_ok 81 | ## auth.mongo.r_mode = 82 | 83 | ## MongoDB topology options. 84 | auth.mongo.topology.pool_size = 1 85 | auth.mongo.topology.max_overflow = 0 86 | ## auth.mongo.topology.overflow_ttl = 1000 87 | ## auth.mongo.topology.overflow_check_period = 1000 88 | ## auth.mongo.topology.local_threshold_ms = 1000 89 | ## auth.mongo.topology.connect_timeout_ms = 20000 90 | ## auth.mongo.topology.socket_timeout_ms = 100 91 | ## auth.mongo.topology.server_selection_timeout_ms = 30000 92 | ## auth.mongo.topology.wait_queue_timeout_ms = 1000 93 | ## auth.mongo.topology.heartbeat_frequency_ms = 10000 94 | ## auth.mongo.topology.min_heartbeat_frequency_ms = 1000 95 | 96 | ## ------------------------------------------------- 97 | ## Auth Query 98 | ## ------------------------------------------------- 99 | ## Password hash. 100 | ## 101 | ## Value: plain | md5 | sha | sha256 | bcrypt 102 | auth.mongo.auth_query.password_hash = sha256 103 | 104 | ## sha256 with salt suffix 105 | ## auth.mongo.auth_query.password_hash = sha256,salt 106 | 107 | ## sha256 with salt prefix 108 | ## auth.mongo.auth_query.password_hash = salt,sha256 109 | 110 | ## bcrypt with salt prefix 111 | ## auth.mongo.auth_query.password_hash = salt,bcrypt 112 | 113 | ## pbkdf2 with macfun iterations dklen 114 | ## macfun: md4, md5, ripemd160, sha, sha224, sha256, sha384, sha512 115 | ## auth.mongo.auth_query.password_hash = pbkdf2,sha256,1000,20 116 | 117 | ## Authentication query. 118 | auth.mongo.auth_query.collection = mqtt_user 119 | 120 | ## Password mainly fields 121 | ## 122 | ## Value: password | password,salt 123 | auth.mongo.auth_query.password_field = password 124 | 125 | ## Authentication Selector. 126 | ## 127 | ## Variables: 128 | ## - %u: username 129 | ## - %c: clientid 130 | ## - %C: common name of client TLS cert 131 | ## - %d: subject of client TLS cert 132 | ## 133 | ## auth.mongo.auth_query.selector = {Field}={Placeholder} 134 | auth.mongo.auth_query.selector = username=%u 135 | 136 | ## ------------------------------------------------- 137 | ## Super User Query 138 | ## ------------------------------------------------- 139 | auth.mongo.super_query.collection = mqtt_user 140 | auth.mongo.super_query.super_field = is_superuser 141 | #auth.mongo.super_query.selector = username=%u, clientid=%c 142 | auth.mongo.super_query.selector = username=%u 143 | 144 | ## ACL Selector. 145 | ## 146 | ## Multiple selectors could be combined with '$or' 147 | ## when query acl from mongo. 148 | ## 149 | ## e.g. 150 | ## 151 | ## With following 2 selectors configured: 152 | ## 153 | ## auth.mongo.acl_query.selector.1 = username=%u 154 | ## auth.mongo.acl_query.selector.2 = username=$all 155 | ## 156 | ## And if a client connected using username 'ilyas', 157 | ## then the following mongo command will be used to 158 | ## retrieve acl entries: 159 | ## 160 | ## db.mqtt_acl.find({$or: [{username: "ilyas"}, {username: "$all"}]}); 161 | ## 162 | ## Variables: 163 | ## - %u: username 164 | ## - %c: clientid 165 | ## 166 | ## Examples: 167 | ## 168 | ## auth.mongo.acl_query.selector.1 = username=%u,clientid=%c 169 | ## auth.mongo.acl_query.selector.2 = username=$all 170 | ## auth.mongo.acl_query.selector.3 = clientid=$all 171 | auth.mongo.acl_query.collection = mqtt_acl 172 | auth.mongo.acl_query.selector = username=%u 173 | -------------------------------------------------------------------------------- /include/emqx_auth_mongo.hrl: -------------------------------------------------------------------------------- 1 | 2 | -define(APP, emqx_auth_mongo). 3 | 4 | -define(DEFAULT_SELECTORS, [{<<"username">>, <<"%u">>}]). 5 | 6 | -record(superquery, {collection = <<"mqtt_user">>, 7 | field = <<"is_superuser">>, 8 | selector = {<<"username">>, <<"%u">>}}). 9 | 10 | -record(authquery, {collection = <<"mqtt_user">>, 11 | field = <<"password">>, 12 | hash = sha256, 13 | selector = {<<"username">>, <<"%u">>}}). 14 | 15 | -record(aclquery, {collection = <<"mqtt_acl">>, 16 | selector = {<<"username">>, <<"%u">>}}). 17 | 18 | -record(auth_metrics, { 19 | success = 'client.auth.success', 20 | failure = 'client.auth.failure', 21 | ignore = 'client.auth.ignore' 22 | }). 23 | 24 | -record(acl_metrics, { 25 | allow = 'client.acl.allow', 26 | deny = 'client.acl.deny', 27 | ignore = 'client.acl.ignore' 28 | }). 29 | 30 | -define(METRICS(Type), tl(tuple_to_list(#Type{}))). 31 | -define(METRICS(Type, K), #Type{}#Type.K). 32 | 33 | -define(AUTH_METRICS, ?METRICS(auth_metrics)). 34 | -define(AUTH_METRICS(K), ?METRICS(auth_metrics, K)). 35 | 36 | -define(ACL_METRICS, ?METRICS(acl_metrics)). 37 | -define(ACL_METRICS(K), ?METRICS(acl_metrics, K)). 38 | -------------------------------------------------------------------------------- /priv/emqx_auth_mongo.schema: -------------------------------------------------------------------------------- 1 | %%-*- mode: erlang -*- 2 | %% emqx_auth_mongo config mapping 3 | 4 | {mapping, "auth.mongo.type", "emqx_auth_mongo.server", [ 5 | {default, single}, 6 | {datatype, {enum, [single, unknown, sharded, rs]}} 7 | ]}. 8 | 9 | {mapping, "auth.mongo.rs_set_name", "emqx_auth_mongo.server", [ 10 | {default, "mqtt"}, 11 | {datatype, string} 12 | ]}. 13 | 14 | {mapping, "auth.mongo.server", "emqx_auth_mongo.server", [ 15 | {default, "127.0.0.1:27017"}, 16 | {datatype, string} 17 | ]}. 18 | 19 | {mapping, "auth.mongo.pool", "emqx_auth_mongo.server", [ 20 | {default, 8}, 21 | {datatype, integer} 22 | ]}. 23 | 24 | {mapping, "auth.mongo.login", "emqx_auth_mongo.server", [ 25 | {default, ""}, 26 | {datatype, string} 27 | ]}. 28 | 29 | {mapping, "auth.mongo.password", "emqx_auth_mongo.server", [ 30 | {default, ""}, 31 | {datatype, string} 32 | ]}. 33 | 34 | {mapping, "auth.mongo.database", "emqx_auth_mongo.server", [ 35 | {default, "mqtt"}, 36 | {datatype, string} 37 | ]}. 38 | 39 | {mapping, "auth.mongo.auth_source", "emqx_auth_mongo.server", [ 40 | {default, "mqtt"}, 41 | {datatype, string} 42 | ]}. 43 | 44 | {mapping, "auth.mongo.ssl", "emqx_auth_mongo.server", [ 45 | {default, false}, 46 | {datatype, {enum, [true, false]}} 47 | ]}. 48 | 49 | {mapping, "auth.mongo.ssl_opts.keyfile", "emqx_auth_mongo.server", [ 50 | {datatype, string} 51 | ]}. 52 | 53 | {mapping, "auth.mongo.ssl_opts.certfile", "emqx_auth_mongo.server", [ 54 | {datatype, string} 55 | ]}. 56 | 57 | {mapping, "auth.mongo.ssl_opts.cacertfile", "emqx_auth_mongo.server", [ 58 | {datatype, string} 59 | ]}. 60 | 61 | {mapping, "auth.mongo.w_mode", "emqx_auth_mongo.server", [ 62 | {default, undef}, 63 | {datatype, {enum, [safe, unsafe, undef]}} 64 | ]}. 65 | 66 | {mapping, "auth.mongo.r_mode", "emqx_auth_mongo.server", [ 67 | {default, undef}, 68 | {datatype, {enum, [master, slave_ok, undef]}} 69 | ]}. 70 | 71 | {mapping, "auth.mongo.topology.$name", "emqx_auth_mongo.server", [ 72 | {datatype, integer} 73 | ]}. 74 | 75 | {translation, "emqx_auth_mongo.server", fun(Conf) -> 76 | H = cuttlefish:conf_get("auth.mongo.server", Conf), 77 | Hosts = string:tokens(H, ","), 78 | Type0 = cuttlefish:conf_get("auth.mongo.type", Conf), 79 | Pool = cuttlefish:conf_get("auth.mongo.pool", Conf), 80 | Login = cuttlefish:conf_get("auth.mongo.login", Conf), 81 | Passwd = cuttlefish:conf_get("auth.mongo.password", Conf), 82 | DB = cuttlefish:conf_get("auth.mongo.database", Conf), 83 | AuthSrc = cuttlefish:conf_get("auth.mongo.auth_source", Conf), 84 | R = cuttlefish:conf_get("auth.mongo.w_mode", Conf), 85 | W = cuttlefish:conf_get("auth.mongo.r_mode", Conf), 86 | Login0 = case Login =:= [] of 87 | true -> []; 88 | false -> [{login, list_to_binary(Login)}] 89 | end, 90 | Passwd0 = case Passwd =:= [] of 91 | true -> []; 92 | false -> [{password, list_to_binary(Passwd)}] 93 | end, 94 | W0 = case W =:= undef of 95 | true -> []; 96 | false -> [{w_mode, W}] 97 | end, 98 | R0 = case R =:= undef of 99 | true -> []; 100 | false -> [{r_mode, R}] 101 | end, 102 | Ssl = case cuttlefish:conf_get("auth.mongo.ssl", Conf) of 103 | true -> 104 | Filter = fun(Opts) -> [{K, V} || {K, V} <- Opts, V =/= undefined] end, 105 | SslOpts = fun(Prefix) -> 106 | Filter([{keyfile, cuttlefish:conf_get(Prefix ++ ".keyfile", Conf, undefined)}, 107 | {certfile, cuttlefish:conf_get(Prefix ++ ".certfile", Conf, undefined)}, 108 | {cacertfile, cuttlefish:conf_get(Prefix ++ ".cacertfile", Conf, undefined)}]) 109 | end, 110 | [{ssl, true}, {ssl_opts, SslOpts("auth.mongo.ssl_opts")}]; 111 | false -> 112 | [] 113 | end, 114 | WorkerOptions = [{database, list_to_binary(DB)}, {auth_source, list_to_binary(AuthSrc)}] 115 | ++ Login0 ++ Passwd0 ++ W0 ++ R0 ++ Ssl, 116 | 117 | Vars = cuttlefish_variable:fuzzy_matches(["auth", "mongo", "topology", "$name"], Conf), 118 | Options = lists:map(fun({_, Name}) -> 119 | Name2 = case Name of 120 | "local_threshold_ms" -> "localThresholdMS"; 121 | "connect_timeout_ms" -> "connectTimeoutMS"; 122 | "socket_timeout_ms" -> "socketTimeoutMS"; 123 | "server_selection_timeout_ms" -> "serverSelectionTimeoutMS"; 124 | "wait_queue_timeout_ms" -> "waitQueueTimeoutMS"; 125 | "heartbeat_frequency_ms" -> "heartbeatFrequencyMS"; 126 | "min_heartbeat_frequency_ms" -> "minHeartbeatFrequencyMS"; 127 | _ -> Name 128 | end, 129 | {list_to_atom(Name2), cuttlefish:conf_get("auth.mongo.topology."++Name, Conf)} 130 | end, Vars), 131 | 132 | Type = case Type0 =:= rs of 133 | true -> {Type0, list_to_binary(cuttlefish:conf_get("auth.mongo.rs_set_name", Conf))}; 134 | false -> Type0 135 | end, 136 | [{type, Type}, 137 | {hosts, Hosts}, 138 | {options, Options}, 139 | {worker_options, WorkerOptions}, 140 | {auto_reconnect, 1}, 141 | {pool_size, Pool}] 142 | end}. 143 | 144 | %% The mongodb operation timeout is specified by the value of `cursor_timeout` from application config, 145 | %% or `infinity` if `cursor_timeout` not specified 146 | {mapping, "auth.mongo.query_timeout", "mongodb.cursor_timeout", [ 147 | {datatype, string} 148 | ]}. 149 | 150 | {translation, "mongodb.cursor_timeout", fun(Conf) -> 151 | case cuttlefish:conf_get("auth.mongo.query_timeout", Conf, undefined) of 152 | undefined -> infinity; 153 | Duration -> 154 | case cuttlefish_duration:parse(Duration, ms) of 155 | {error, Reason} -> error(Reason); 156 | Ms when is_integer(Ms) -> Ms 157 | end 158 | end 159 | end}. 160 | 161 | {mapping, "auth.mongo.auth_query.collection", "emqx_auth_mongo.auth_query", [ 162 | {default, "mqtt_user"}, 163 | {datatype, string} 164 | ]}. 165 | 166 | {mapping, "auth.mongo.auth_query.password_field", "emqx_auth_mongo.auth_query", [ 167 | {default, "password"}, 168 | {datatype, string} 169 | ]}. 170 | 171 | {mapping, "auth.mongo.auth_query.password_hash", "emqx_auth_mongo.auth_query", [ 172 | {datatype, string} 173 | ]}. 174 | 175 | {mapping, "auth.mongo.auth_query.selector", "emqx_auth_mongo.auth_query", [ 176 | {default, ""}, 177 | {datatype, string} 178 | ]}. 179 | 180 | {translation, "emqx_auth_mongo.auth_query", fun(Conf) -> 181 | case cuttlefish:conf_get("auth.mongo.auth_query.collection", Conf) of 182 | undefined -> cuttlefish:unset(); 183 | Collection -> 184 | PasswordField = cuttlefish:conf_get("auth.mongo.auth_query.password_field", Conf), 185 | PasswordHash = cuttlefish:conf_get("auth.mongo.auth_query.password_hash", Conf), 186 | SelectorStr = cuttlefish:conf_get("auth.mongo.auth_query.selector", Conf), 187 | SelectorList = 188 | lists:map(fun(Selector) -> 189 | case string:tokens(Selector, "=") of 190 | [Field, Val] -> {list_to_binary(Field), list_to_binary(Val)}; 191 | _ -> {<<"username">>, <<"%u">>} 192 | end 193 | end, string:tokens(SelectorStr, ", ")), 194 | 195 | PasswordFields = [list_to_binary(Field) || Field <- string:tokens(PasswordField, ",")], 196 | HashValue = 197 | case string:tokens(PasswordHash, ",") of 198 | [Hash] -> list_to_atom(Hash); 199 | [Prefix, Suffix] -> {list_to_atom(Prefix), list_to_atom(Suffix)}; 200 | [Hash, MacFun, Iterations, Dklen] -> {list_to_atom(Hash), list_to_atom(MacFun), list_to_integer(Iterations), list_to_integer(Dklen)}; 201 | _ -> plain 202 | end, 203 | [{collection, Collection}, 204 | {password_field, PasswordFields}, 205 | {password_hash, HashValue}, 206 | {selector, SelectorList}] 207 | end 208 | end}. 209 | 210 | {mapping, "auth.mongo.super_query", "emqx_auth_mongo.super_query", [ 211 | {default, off}, 212 | {datatype, flag} 213 | ]}. 214 | 215 | {mapping, "auth.mongo.super_query.collection", "emqx_auth_mongo.super_query", [ 216 | {default, "mqtt_user"}, 217 | {datatype, string} 218 | ]}. 219 | 220 | {mapping, "auth.mongo.super_query.super_field", "emqx_auth_mongo.super_query", [ 221 | {default, "is_superuser"}, 222 | {datatype, string} 223 | ]}. 224 | 225 | {mapping, "auth.mongo.super_query.selector", "emqx_auth_mongo.super_query", [ 226 | {default, ""}, 227 | {datatype, string} 228 | ]}. 229 | 230 | {translation, "emqx_auth_mongo.super_query", fun(Conf) -> 231 | case cuttlefish:conf_get("auth.mongo.super_query.collection", Conf) of 232 | undefined -> cuttlefish:unset(); 233 | Collection -> 234 | SuperField = cuttlefish:conf_get("auth.mongo.super_query.super_field", Conf), 235 | SelectorStr = cuttlefish:conf_get("auth.mongo.super_query.selector", Conf), 236 | SelectorList = 237 | lists:map(fun(Selector) -> 238 | case string:tokens(Selector, "=") of 239 | [Field, Val] -> {list_to_binary(Field), list_to_binary(Val)}; 240 | _ -> {<<"username">>, <<"%u">>} 241 | end 242 | end, string:tokens(SelectorStr, ", ")), 243 | [{collection, Collection}, {super_field, SuperField}, {selector, SelectorList}] 244 | end 245 | end}. 246 | 247 | {mapping, "auth.mongo.acl_query", "emqx_auth_mongo.acl_query", [ 248 | {default, off}, 249 | {datatype, flag} 250 | ]}. 251 | 252 | {mapping, "auth.mongo.acl_query.collection", "emqx_auth_mongo.acl_query", [ 253 | {default, "mqtt_user"}, 254 | {datatype, string} 255 | ]}. 256 | 257 | {mapping, "auth.mongo.acl_query.selector", "emqx_auth_mongo.acl_query", [ 258 | {default, ""}, 259 | {datatype, string} 260 | ]}. 261 | {mapping, "auth.mongo.acl_query.selector.$id", "emqx_auth_mongo.acl_query", [ 262 | {default, ""}, 263 | {datatype, string} 264 | ]}. 265 | 266 | {translation, "emqx_auth_mongo.acl_query", fun(Conf) -> 267 | case cuttlefish:conf_get("auth.mongo.acl_query.collection", Conf) of 268 | undefined -> cuttlefish:unset(); 269 | Collection -> 270 | SelectorStrList = 271 | lists:map( 272 | fun 273 | ({["auth","mongo","acl_query","selector"], ConfEntry}) -> 274 | ConfEntry; 275 | ({["auth","mongo","acl_query","selector", _], ConfEntry}) -> 276 | ConfEntry 277 | end, 278 | cuttlefish_variable:filter_by_prefix("auth.mongo.acl_query.selector", Conf)), 279 | SelectorListList = 280 | lists:map( 281 | fun(SelectorStr) -> 282 | lists:map(fun(Selector) -> 283 | case string:tokens(Selector, "=") of 284 | [Field, Val] -> {list_to_binary(Field), list_to_binary(Val)}; 285 | _ -> {<<"username">>, <<"%u">>} 286 | end 287 | end, string:tokens(SelectorStr, ", ")) 288 | end, 289 | SelectorStrList), 290 | [{collection, Collection}, {selector, SelectorListList}] 291 | end 292 | end}. 293 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {deps, 2 | [{mongodb, {git,"https://github.com/emqx/mongodb-erlang", {tag, "v3.0.7"}}}, 3 | {ecpool, {git,"https://github.com/emqx/ecpool", {tag, "v0.4.2"}}}, 4 | {emqx_passwd, {git, "https://github.com/emqx/emqx-passwd", {tag, "v1.1.1"}}} 5 | ]}. 6 | 7 | {edoc_opts, [{preprocess, true}]}. 8 | {erl_opts, [warn_unused_vars, 9 | warn_shadow_vars, 10 | warn_unused_import, 11 | warn_obsolete_guard, 12 | debug_info, 13 | compressed, 14 | {parse_transform} 15 | ]}. 16 | {overrides, [{add, [{erl_opts, [compressed]}]}]}. 17 | 18 | {xref_checks, [undefined_function_calls, undefined_functions, 19 | locals_not_used, deprecated_function_calls, 20 | warnings_as_errors, deprecated_functions 21 | ]}. 22 | 23 | {cover_enabled, true}. 24 | {cover_opts, [verbose]}. 25 | {cover_export_enabled, true}. 26 | 27 | {profiles, 28 | [{test, 29 | [{deps, 30 | [{emqx_ct_helper, {git, "https://github.com/emqx/emqx-ct-helper", {tag, "1.2.2"}}}, 31 | {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.2.0"}}} 32 | ]}, 33 | {erl_opts, [debug_info]} 34 | ]} 35 | ]}. 36 | -------------------------------------------------------------------------------- /rebar.config.script: -------------------------------------------------------------------------------- 1 | %%-*- mode: erlang -*- 2 | 3 | DEPS = case lists:keyfind(deps, 1, CONFIG) of 4 | {_, Deps} -> Deps; 5 | _ -> [] 6 | end, 7 | 8 | ComparingFun = fun 9 | _Fun([C1|R1], [C2|R2]) when is_list(C1), is_list(C2); 10 | is_integer(C1), is_integer(C2) -> C1 < C2 orelse _Fun(R1, R2); 11 | _Fun([C1|R1], [C2|R2]) when is_integer(C1), is_list(C2) -> _Fun(R1, R2); 12 | _Fun([C1|R1], [C2|R2]) when is_list(C1), is_integer(C2) -> true; 13 | _Fun(_, _) -> false 14 | end, 15 | 16 | SortFun = fun(T1, T2) -> 17 | C = fun(T) -> 18 | [case catch list_to_integer(E) of 19 | I when is_integer(I) -> I; 20 | _ -> E 21 | end || E <- re:split(string:sub_string(T, 2), "[.-]", [{return, list}])] 22 | end, 23 | ComparingFun(C(T1), C(T2)) 24 | end, 25 | 26 | VTags = string:tokens(os:cmd("git tag -l \"v*\" --points-at $(git rev-parse $(git describe --abbrev=0 --tags))"), "\n"), 27 | 28 | Tags = case VTags of 29 | [] -> string:tokens(os:cmd("git tag -l \"e*\" --points-at $(git rev-parse $(git describe --abbrev=0 --tags))"), "\n"); 30 | _ -> VTags 31 | end, 32 | 33 | LatestTag = lists:last(lists:sort(SortFun, Tags)), 34 | 35 | Branch = case os:getenv("GITHUB_RUN_ID") of 36 | false -> os:cmd("git branch | grep -e '^*' | cut -d' ' -f 2") -- "\n"; 37 | _ -> re:replace(os:getenv("GITHUB_REF"), "^refs/heads/|^refs/tags/", "", [global, {return ,list}]) 38 | end, 39 | 40 | GitDescribe = case re:run(Branch, "master|^dev/|^hotfix/", [{capture, none}]) of 41 | match -> {branch, Branch}; 42 | _ -> {tag, LatestTag} 43 | end, 44 | 45 | UrlPrefix = "https://github.com/emqx/", 46 | 47 | EMQX_DEP = {emqx, {git, UrlPrefix ++ "emqx", GitDescribe}}, 48 | EMQX_MGMT_DEP = {emqx_management, {git, UrlPrefix ++ "emqx-management", GitDescribe}}, 49 | 50 | NewDeps = [EMQX_DEP, EMQX_MGMT_DEP | DEPS], 51 | 52 | CONFIG1 = lists:keystore(deps, 1, CONFIG, {deps, NewDeps}), 53 | 54 | CONFIG1. 55 | -------------------------------------------------------------------------------- /src/emqx_acl_mongo.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | 17 | -module(emqx_acl_mongo). 18 | 19 | -include("emqx_auth_mongo.hrl"). 20 | -include_lib("emqx/include/emqx.hrl"). 21 | -include_lib("emqx/include/logger.hrl"). 22 | 23 | %% ACL callbacks 24 | -export([ register_metrics/0 25 | , check_acl/5 26 | , description/0 27 | ]). 28 | -spec(register_metrics() -> ok). 29 | register_metrics() -> 30 | lists:foreach(fun emqx_metrics:ensure/1, ?ACL_METRICS). 31 | 32 | check_acl(#{username := <<$$, _/binary>>}, _PubSub, _Topic, _AclResult, _State) -> 33 | ok; 34 | 35 | check_acl(ClientInfo, PubSub, Topic, _AclResult, Env = #{aclquery := AclQuery}) -> 36 | #aclquery{collection = Coll, selector = SelectorList} = AclQuery, 37 | Pool = maps:get(pool, Env, ?APP), 38 | SelectorMapList = 39 | lists:map(fun(Selector) -> 40 | maps:from_list(emqx_auth_mongo:replvars(Selector, ClientInfo)) 41 | end, SelectorList), 42 | case emqx_auth_mongo:query_multi(Pool, Coll, SelectorMapList) of 43 | [] -> ok; 44 | Rows -> 45 | try match(ClientInfo, Topic, topics(PubSub, Rows)) of 46 | matched -> emqx_metrics:inc(?ACL_METRICS(allow)), 47 | {stop, allow}; 48 | nomatch -> emqx_metrics:inc(?ACL_METRICS(deny)), 49 | {stop, deny} 50 | catch 51 | _Err:Reason-> 52 | ?LOG(error, "[MongoDB] Check mongo ~p ACL failed, got ACL config: ~p, error: :~p", 53 | [PubSub, Rows, Reason]), 54 | emqx_metrics:inc(?ACL_METRICS(ignore)), 55 | ignore 56 | end 57 | end. 58 | 59 | 60 | match(_ClientInfo, _Topic, []) -> 61 | nomatch; 62 | match(ClientInfo, Topic, [TopicFilter|More]) -> 63 | case emqx_topic:match(Topic, feedvar(ClientInfo, TopicFilter)) of 64 | true -> matched; 65 | false -> match(ClientInfo, Topic, More) 66 | end. 67 | 68 | topics(publish, Rows) -> 69 | lists:foldl(fun(Row, Acc) -> 70 | Topics = maps:get(<<"publish">>, Row, []) ++ maps:get(<<"pubsub">>, Row, []), 71 | lists:umerge(Acc, Topics) 72 | end, [], Rows); 73 | 74 | topics(subscribe, Rows) -> 75 | lists:foldl(fun(Row, Acc) -> 76 | Topics = maps:get(<<"subscribe">>, Row, []) ++ maps:get(<<"pubsub">>, Row, []), 77 | lists:umerge(Acc, Topics) 78 | end, [], Rows). 79 | 80 | feedvar(#{clientid := ClientId, username := Username}, Str) -> 81 | lists:foldl(fun({Var, Val}, Acc) -> 82 | feedvar(Acc, Var, Val) 83 | end, Str, [{"%u", Username}, {"%c", ClientId}]). 84 | 85 | feedvar(Str, _Var, undefined) -> 86 | Str; 87 | feedvar(Str, Var, Val) -> 88 | re:replace(Str, Var, Val, [global, {return, binary}]). 89 | 90 | description() -> "ACL with MongoDB". 91 | 92 | -------------------------------------------------------------------------------- /src/emqx_auth_mongo.app.src: -------------------------------------------------------------------------------- 1 | {application, emqx_auth_mongo, 2 | [{description, "EMQX Authentication/ACL with MongoDB"}, 3 | {vsn, "git"}, 4 | {modules, []}, 5 | {registered, [emqx_auth_mongo_sup]}, 6 | {applications, [kernel,stdlib,mongodb,ecpool,emqx_passwd]}, 7 | {mod, {emqx_auth_mongo_app,[]}}, 8 | {env, []}, 9 | {licenses, ["Apache-2.0"]}, 10 | {maintainers, ["EMQX Team "]}, 11 | {links, [{"Homepage", "https://emqx.io/"}, 12 | {"Github", "https://github.com/emqx/emqx-auth-mongo"} 13 | ]} 14 | ]}. 15 | -------------------------------------------------------------------------------- /src/emqx_auth_mongo.app.src.script: -------------------------------------------------------------------------------- 1 | %%-*- mode: erlang -*- 2 | %% .app.src.script 3 | 4 | RemoveLeadingV = 5 | fun(Tag) -> 6 | case re:run(Tag, "^[v|e]?[0-9]\.[0-9]\.([0-9]|(rc|beta|alpha)\.[0-9])", [{capture, none}]) of 7 | nomatch -> 8 | re:replace(Tag, "/", "-", [{return ,list}]); 9 | _ -> 10 | %% if it is a version number prefixed by 'v' or 'e', then remove it 11 | re:replace(Tag, "[v|e]", "", [{return ,list}]) 12 | end 13 | end, 14 | 15 | case os:getenv("EMQX_DEPS_DEFAULT_VSN") of 16 | false -> CONFIG; % env var not defined 17 | [] -> CONFIG; % env var set to empty string 18 | Tag -> 19 | [begin 20 | AppConf0 = lists:keystore(vsn, 1, AppConf, {vsn, RemoveLeadingV(Tag)}), 21 | {application, App, AppConf0} 22 | end || Conf = {application, App, AppConf} <- CONFIG] 23 | end. 24 | 25 | -------------------------------------------------------------------------------- /src/emqx_auth_mongo.appup.src: -------------------------------------------------------------------------------- 1 | %%-*-: erlang -*- 2 | {VSN, 3 | [ 4 | {"4.2.3", [ 5 | {load_module, emqx_auth_mongo, brutal_purge, soft_purge, []} 6 | ]}, 7 | {<<"4.2.[0-2]">>, [ 8 | {load_module, emqx_auth_mongo_app, brutal_purge, soft_purge, []}, 9 | {load_module, emqx_auth_mongo, brutal_purge, soft_purge, []}, 10 | {load_module, emqx_acl_mongo, brutal_purge, soft_purge, [emqx_auth_mongo]} 11 | ]}, 12 | {<<".*">>, []} 13 | ], 14 | [ 15 | {"4.2.3", [ 16 | {load_module, emqx_auth_mongo, brutal_purge, soft_purge, []} 17 | ]}, 18 | {<<"4.2.[0-2]">>, [ 19 | {load_module, emqx_auth_mongo_app, brutal_purge, soft_purge, []}, 20 | {load_module, emqx_auth_mongo, brutal_purge, soft_purge, []}, 21 | {load_module, emqx_acl_mongo, brutal_purge, soft_purge, [emqx_auth_mongo]} 22 | ]}, 23 | {<<".*">>, []} 24 | ] 25 | }. 26 | -------------------------------------------------------------------------------- /src/emqx_auth_mongo.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | 17 | -module(emqx_auth_mongo). 18 | 19 | -behaviour(ecpool_worker). 20 | 21 | -include("emqx_auth_mongo.hrl"). 22 | -include_lib("emqx/include/emqx.hrl"). 23 | -include_lib("emqx/include/logger.hrl"). 24 | -include_lib("emqx/include/types.hrl"). 25 | 26 | -export([ register_metrics/0 27 | , check/3 28 | , description/0 29 | ]). 30 | 31 | -export([ replvar/2 32 | , replvars/2 33 | , connect/1 34 | , query/3 35 | , query_multi/3 36 | ]). 37 | 38 | -spec(register_metrics() -> ok). 39 | register_metrics() -> 40 | lists:foreach(fun emqx_metrics:ensure/1, ?AUTH_METRICS). 41 | 42 | check(ClientInfo = #{password := Password}, AuthResult, 43 | Env = #{authquery := AuthQuery, superquery := SuperQuery}) -> 44 | #authquery{collection = Collection, field = Fields, 45 | hash = HashType, selector = Selector} = AuthQuery, 46 | Pool = maps:get(pool, Env, ?APP), 47 | case query(Pool, Collection, maps:from_list(replvars(Selector, ClientInfo))) of 48 | undefined -> emqx_metrics:inc(?AUTH_METRICS(ignore)); 49 | {error, Reason} -> 50 | ?LOG(error, "[MongoDB] Can't connect to MongoDB server: ~0p", [Reason]), 51 | ok = emqx_metrics:inc(?AUTH_METRICS(failure)), 52 | {stop, AuthResult#{auth_result => not_authorized, anonymous => false}}; 53 | UserMap -> 54 | Result = case [maps:get(Field, UserMap, undefined) || Field <- Fields] of 55 | [undefined] -> {error, password_error}; 56 | [PassHash] -> 57 | check_pass({PassHash, Password}, HashType); 58 | [PassHash, Salt|_] -> 59 | check_pass({PassHash, Salt, Password}, HashType) 60 | end, 61 | case Result of 62 | ok -> 63 | ok = emqx_metrics:inc(?AUTH_METRICS(success)), 64 | {stop, AuthResult#{is_superuser => is_superuser(Pool, SuperQuery, ClientInfo), 65 | anonymous => false, 66 | auth_result => success}}; 67 | {error, Error} -> 68 | ?LOG(error, "[MongoDB] check auth fail: ~p", [Error]), 69 | ok = emqx_metrics:inc(?AUTH_METRICS(failure)), 70 | {stop, AuthResult#{auth_result => Error, anonymous => false}} 71 | end 72 | end. 73 | 74 | check_pass(Password, HashType) -> 75 | case emqx_passwd:check_pass(Password, HashType) of 76 | ok -> ok; 77 | {error, _Reason} -> {error, not_authorized} 78 | end. 79 | 80 | description() -> "Authentication with MongoDB". 81 | 82 | %%-------------------------------------------------------------------- 83 | %% Is Superuser? 84 | %%-------------------------------------------------------------------- 85 | 86 | -spec(is_superuser(string(), maybe(#superquery{}), emqx_types:clientinfo()) -> boolean()). 87 | is_superuser(_Pool, undefined, _ClientInfo) -> 88 | false; 89 | is_superuser(Pool, #superquery{collection = Coll, field = Field, selector = Selector}, ClientInfo) -> 90 | case query(Pool, Coll, maps:from_list(replvars(Selector, ClientInfo))) of 91 | undefined -> false; 92 | {error, Reason} -> 93 | ?LOG(error, "[MongoDB] Can't connect to MongoDB server: ~0p", [Reason]), 94 | false; 95 | Row -> 96 | case maps:get(Field, Row, false) of 97 | true -> true; 98 | _False -> false 99 | end 100 | end. 101 | 102 | replvars(VarList, ClientInfo) -> 103 | lists:map(fun(Var) -> replvar(Var, ClientInfo) end, VarList). 104 | 105 | replvar({Field, <<"%u">>}, #{username := Username}) -> 106 | {Field, Username}; 107 | replvar({Field, <<"%c">>}, #{clientid := ClientId}) -> 108 | {Field, ClientId}; 109 | replvar({Field, <<"%C">>}, #{cn := CN}) -> 110 | {Field, CN}; 111 | replvar({Field, <<"%d">>}, #{dn := DN}) -> 112 | {Field, DN}; 113 | replvar(Selector, _ClientInfo) -> 114 | Selector. 115 | 116 | %%-------------------------------------------------------------------- 117 | %% MongoDB Connect/Query 118 | %%-------------------------------------------------------------------- 119 | 120 | connect(Opts) -> 121 | Type = proplists:get_value(type, Opts, single), 122 | Hosts = proplists:get_value(hosts, Opts, []), 123 | Options = proplists:get_value(options, Opts, []), 124 | WorkerOptions = proplists:get_value(worker_options, Opts, []), 125 | mongo_api:connect(Type, Hosts, Options, WorkerOptions). 126 | 127 | query(Pool, Collection, Selector) -> 128 | ecpool:with_client(Pool, fun(Conn) -> mongo_api:find_one(Conn, Collection, Selector, #{}) end). 129 | 130 | query_multi(Pool, Collection, SelectorList) -> 131 | lists:reverse(lists:flatten(lists:foldl(fun(Selector, Acc1) -> 132 | Batch = ecpool:with_client(Pool, fun(Conn) -> 133 | case mongo_api:find(Conn, Collection, Selector, #{}) of 134 | [] -> []; 135 | {ok, Cursor} -> 136 | mc_cursor:foldl(fun(O, Acc2) -> [O|Acc2] end, [], Cursor, 1000) 137 | end 138 | end), 139 | [Batch|Acc1] 140 | end, [], SelectorList))). 141 | -------------------------------------------------------------------------------- /src/emqx_auth_mongo_app.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | 17 | -module(emqx_auth_mongo_app). 18 | 19 | -behaviour(application). 20 | 21 | -emqx_plugin(auth). 22 | 23 | -include("emqx_auth_mongo.hrl"). 24 | 25 | -import(proplists, [get_value/3]). 26 | 27 | %% Application callbacks 28 | -export([ start/2 29 | , prep_stop/1 30 | , stop/1 31 | ]). 32 | 33 | %%-------------------------------------------------------------------- 34 | %% Application callbacks 35 | %%-------------------------------------------------------------------- 36 | 37 | start(_StartType, _StartArgs) -> 38 | {ok, Sup} = emqx_auth_mongo_sup:start_link(), 39 | with_env(auth_query, fun reg_authmod/1), 40 | with_env(acl_query, fun reg_aclmod/1), 41 | {ok, Sup}. 42 | 43 | prep_stop(State) -> 44 | ok = emqx:unhook('client.authenticate', fun emqx_auth_mongo:check/3), 45 | ok = emqx:unhook('client.check_acl', fun emqx_acl_mongo:check_acl/5), 46 | State. 47 | 48 | stop(_State) -> 49 | ok. 50 | 51 | reg_authmod(AuthQuery) -> 52 | emqx_auth_mongo:register_metrics(), 53 | SuperQuery = r(super_query, application:get_env(?APP, super_query, undefined)), 54 | ok = emqx:hook('client.authenticate', fun emqx_auth_mongo:check/3, 55 | [#{authquery => AuthQuery, superquery => SuperQuery, pool => ?APP}]). 56 | 57 | reg_aclmod(AclQuery) -> 58 | emqx_acl_mongo:register_metrics(), 59 | ok = emqx:hook('client.check_acl', fun emqx_acl_mongo:check_acl/5, [#{aclquery => AclQuery, pool => ?APP}]). 60 | 61 | %%-------------------------------------------------------------------- 62 | %% Internal functions 63 | %%-------------------------------------------------------------------- 64 | 65 | with_env(Name, Fun) -> 66 | case application:get_env(?APP, Name) of 67 | undefined -> ok; 68 | {ok, Config} -> Fun(r(Name, Config)) 69 | end. 70 | 71 | r(super_query, undefined) -> 72 | undefined; 73 | r(super_query, Config) -> 74 | #superquery{collection = list_to_binary(get_value(collection, Config, "mqtt_user")), 75 | field = list_to_binary(get_value(super_field, Config, "is_superuser")), 76 | selector = get_value(selector, Config, ?DEFAULT_SELECTORS)}; 77 | 78 | r(auth_query, Config) -> 79 | #authquery{collection = list_to_binary(get_value(collection, Config, "mqtt_user")), 80 | field = get_value(password_field, Config, [<<"password">>]), 81 | hash = get_value(password_hash, Config, sha256), 82 | selector = get_value(selector, Config, ?DEFAULT_SELECTORS)}; 83 | 84 | r(acl_query, Config) -> 85 | #aclquery{collection = list_to_binary(get_value(collection, Config, "mqtt_acl")), 86 | selector = get_value(selector, Config, [?DEFAULT_SELECTORS])}. 87 | 88 | -------------------------------------------------------------------------------- /src/emqx_auth_mongo_sup.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | 17 | -module(emqx_auth_mongo_sup). 18 | 19 | -behaviour(supervisor). 20 | 21 | -include("emqx_auth_mongo.hrl"). 22 | 23 | -export([start_link/0]). 24 | 25 | -export([init/1]). 26 | 27 | start_link() -> 28 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 29 | 30 | init([]) -> 31 | {ok, PoolEnv} = application:get_env(?APP, server), 32 | PoolSpec = ecpool:pool_spec(?APP, ?APP, ?APP, PoolEnv), 33 | {ok, {{one_for_all, 10, 100}, [PoolSpec]}}. 34 | 35 | -------------------------------------------------------------------------------- /test/emqx_auth_mongo_SUITE.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | 17 | -module(emqx_auth_mongo_SUITE). 18 | 19 | -compile(export_all). 20 | -compile(nowarn_export_all). 21 | 22 | -include_lib("emqx/include/emqx.hrl"). 23 | -include_lib("common_test/include/ct.hrl"). 24 | -include_lib("eunit/include/eunit.hrl"). 25 | 26 | -define(APP, emqx_auth_mongo). 27 | 28 | -define(POOL(App), ecpool_worker:client(gproc_pool:pick_worker({ecpool, App}))). 29 | 30 | -define(MONGO_CL_ACL, <<"mqtt_acl">>). 31 | -define(MONGO_CL_USER, <<"mqtt_user">>). 32 | 33 | -define(INIT_ACL, [{<<"username">>, <<"testuser">>, <<"clientid">>, <<"null">>, <<"subscribe">>, [<<"#">>]}, 34 | {<<"username">>, <<"dashboard">>, <<"clientid">>, <<"null">>, <<"pubsub">>, [<<"$SYS/#">>]}, 35 | {<<"username">>, <<"user3">>, <<"clientid">>, <<"null">>, <<"publish">>, [<<"a/b/c">>]}]). 36 | 37 | -define(INIT_AUTH, [{<<"username">>, <<"plain">>, <<"password">>, <<"plain">>, <<"salt">>, <<"salt">>, <<"is_superuser">>, true}, 38 | {<<"username">>, <<"md5">>, <<"password">>, <<"1bc29b36f623ba82aaf6724fd3b16718">>, <<"salt">>, <<"salt">>, <<"is_superuser">>, false}, 39 | {<<"username">>, <<"sha">>, <<"password">>, <<"d8f4590320e1343a915b6394170650a8f35d6926">>, <<"salt">>, <<"salt">>, <<"is_superuser">>, false}, 40 | {<<"username">>, <<"sha256">>, <<"password">>, <<"5d5b09f6dcb2d53a5fffc60c4ac0d55fabdf556069d6631545f42aa6e3500f2e">>, <<"salt">>, <<"salt">>, <<"is_superuser">>, false}, 41 | {<<"username">>, <<"pbkdf2_password">>, <<"password">>, <<"cdedb5281bb2f801565a1122b2563515">>, <<"salt">>, <<"ATHENA.MIT.EDUraeburn">>, <<"is_superuser">>, false}, 42 | {<<"username">>, <<"bcrypt_foo">>, <<"password">>, <<"$2a$12$sSS8Eg.ovVzaHzi1nUHYK.HbUIOdlQI0iS22Q5rd5z.JVVYH6sfm6">>, <<"salt">>, <<"$2a$12$sSS8Eg.ovVzaHzi1nUHYK.">>, <<"is_superuser">>, false} 43 | ]). 44 | 45 | %%-------------------------------------------------------------------- 46 | %% Setups 47 | %%-------------------------------------------------------------------- 48 | 49 | all() -> 50 | emqx_ct:all(?MODULE). 51 | 52 | init_per_suite(Cfg) -> 53 | emqx_ct_helpers:start_apps([emqx_auth_mongo], fun set_special_confs/1), 54 | emqx_modules:load_module(emqx_mod_acl_internal, false), 55 | init_mongo_data(), 56 | Cfg. 57 | 58 | end_per_suite(_Cfg) -> 59 | deinit_mongo_data(), 60 | emqx_ct_helpers:stop_apps([emqx_auth_mongo]). 61 | 62 | set_special_confs(emqx) -> 63 | application:set_env(emqx, acl_nomatch, deny), 64 | application:set_env(emqx, acl_file, 65 | emqx_ct_helpers:deps_path(emqx, "test/emqx_SUITE_data/acl.conf")), 66 | application:set_env(emqx, allow_anonymous, false), 67 | application:set_env(emqx, enable_acl_cache, false), 68 | application:set_env(emqx, plugins_loaded_file, 69 | emqx_ct_helpers:deps_path(emqx, "test/emqx_SUITE_data/loaded_plugins")); 70 | set_special_confs(_App) -> 71 | ok. 72 | 73 | init_mongo_data() -> 74 | %% Users 75 | {ok, Connection} = ?POOL(?APP), 76 | mongo_api:delete(Connection, ?MONGO_CL_USER, {}), 77 | ?assertMatch({{true, _}, _}, mongo_api:insert(Connection, ?MONGO_CL_USER, ?INIT_AUTH)), 78 | %% ACLs 79 | mongo_api:delete(Connection, ?MONGO_CL_ACL, {}), 80 | ?assertMatch({{true, _}, _}, mongo_api:insert(Connection, ?MONGO_CL_ACL, ?INIT_ACL)). 81 | 82 | deinit_mongo_data() -> 83 | {ok, Connection} = ?POOL(?APP), 84 | mongo_api:delete(Connection, ?MONGO_CL_USER, {}), 85 | mongo_api:delete(Connection, ?MONGO_CL_ACL, {}). 86 | 87 | %%-------------------------------------------------------------------- 88 | %% Test cases 89 | %%-------------------------------------------------------------------- 90 | 91 | t_check_auth(_) -> 92 | Plain = #{zone => external, clientid => <<"client1">>, username => <<"plain">>}, 93 | Plain1 = #{zone => external, clientid => <<"client1">>, username => <<"plain2">>}, 94 | Md5 = #{zone => external, clientid => <<"md5">>, username => <<"md5">>}, 95 | Sha = #{zone => external, clientid => <<"sha">>, username => <<"sha">>}, 96 | Sha256 = #{zone => external, clientid => <<"sha256">>, username => <<"sha256">>}, 97 | Pbkdf2 = #{zone => external, clientid => <<"pbkdf2_password">>, username => <<"pbkdf2_password">>}, 98 | Bcrypt = #{zone => external, clientid => <<"bcrypt_foo">>, username => <<"bcrypt_foo">>}, 99 | User1 = #{zone => external, clientid => <<"bcrypt_foo">>, username => <<"user">>}, 100 | reload({auth_query, [{password_hash, plain}]}), 101 | %% With exactly username/password, connection success 102 | {ok, #{is_superuser := true}} = emqx_access_control:authenticate(Plain#{password => <<"plain">>}), 103 | %% With exactly username and wrong password, connection fail 104 | {error, _} = emqx_access_control:authenticate(Plain#{password => <<"error_pwd">>}), 105 | %% With wrong username and wrong password, emqx_auth_mongo auth fail, then allow anonymous authentication 106 | {error, _} = emqx_access_control:authenticate(Plain1#{password => <<"error_pwd">>}), 107 | %% With wrong username and exactly password, emqx_auth_mongo auth fail, then allow anonymous authentication 108 | {error, _} = emqx_access_control:authenticate(Plain1#{password => <<"plain">>}), 109 | reload({auth_query, [{password_hash, md5}]}), 110 | {ok, #{is_superuser := false}} = emqx_access_control:authenticate(Md5#{password => <<"md5">>}), 111 | reload({auth_query, [{password_hash, sha}]}), 112 | {ok, #{is_superuser := false}} = emqx_access_control:authenticate(Sha#{password => <<"sha">>}), 113 | reload({auth_query, [{password_hash, sha256}]}), 114 | {ok, #{is_superuser := false}} = emqx_access_control:authenticate(Sha256#{password => <<"sha256">>}), 115 | %%pbkdf2 sha 116 | reload({auth_query, [{password_hash, {pbkdf2, sha, 1, 16}}, {password_field, [<<"password">>, <<"salt">>]}]}), 117 | {ok, #{is_superuser := false}} = emqx_access_control:authenticate(Pbkdf2#{password => <<"password">>}), 118 | reload({auth_query, [{password_hash, {salt, bcrypt}}]}), 119 | {ok, #{is_superuser := false}} = emqx_access_control:authenticate(Bcrypt#{password => <<"foo">>}), 120 | {error, _} = emqx_access_control:authenticate(User1#{password => <<"foo">>}). 121 | 122 | t_check_acl(_) -> 123 | {ok, Connection} = ?POOL(?APP), 124 | User1 = #{zone => external, clientid => <<"client1">>, username => <<"testuser">>}, 125 | User2 = #{zone => external, clientid => <<"client2">>, username => <<"dashboard">>}, 126 | User3 = #{zone => external, clientid => <<"client2">>, username => <<"user3">>}, 127 | User4 = #{zone => external, clientid => <<"$$client2">>, username => <<"$$user3">>}, 128 | 3 = mongo_api:count(Connection, ?MONGO_CL_ACL, {}, 17), 129 | %% ct log output 130 | allow = emqx_access_control:check_acl(User1, subscribe, <<"users/testuser/1">>), 131 | deny = emqx_access_control:check_acl(User1, subscribe, <<"$SYS/testuser/1">>), 132 | deny = emqx_access_control:check_acl(User2, subscribe, <<"a/b/c">>), 133 | allow = emqx_access_control:check_acl(User2, subscribe, <<"$SYS/testuser/1">>), 134 | allow = emqx_access_control:check_acl(User3, publish, <<"a/b/c">>), 135 | deny = emqx_access_control:check_acl(User3, publish, <<"c">>), 136 | allow = emqx_access_control:check_acl(User4, publish, <<"a/b/c">>). 137 | 138 | t_acl_super(_) -> 139 | reload({auth_query, [{password_hash, plain}, {password_field, [<<"password">>]}]}), 140 | {ok, C} = emqtt:start_link([{clientid, <<"simpleClient">>}, 141 | {username, <<"plain">>}, 142 | {password, <<"plain">>}]), 143 | {ok, _} = emqtt:connect(C), 144 | timer:sleep(10), 145 | emqtt:subscribe(C, <<"TopicA">>, qos2), 146 | timer:sleep(1000), 147 | emqtt:publish(C, <<"TopicA">>, <<"Payload">>, qos2), 148 | timer:sleep(1000), 149 | receive 150 | {publish, #{payload := Payload}} -> 151 | ?assertEqual(<<"Payload">>, Payload) 152 | after 153 | 1000 -> 154 | ct:fail({receive_timeout, <<"Payload">>}), 155 | ok 156 | end, 157 | emqtt:disconnect(C). 158 | 159 | %%-------------------------------------------------------------------- 160 | %% Utils 161 | %%-------------------------------------------------------------------- 162 | 163 | reload({Par, Vals}) when is_list(Vals) -> 164 | application:stop(?APP), 165 | {ok, TupleVals} = application:get_env(?APP, Par), 166 | NewVals = 167 | lists:filtermap(fun({K, V}) -> 168 | case lists:keymember(K, 1, Vals) of 169 | false ->{true, {K, V}}; 170 | _ -> false 171 | end 172 | end, TupleVals), 173 | application:set_env(?APP, Par, lists:append(NewVals, Vals)), 174 | application:start(?APP). 175 | -------------------------------------------------------------------------------- /test/emqx_auth_mongo_SUITE_data/ca-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA0kGUBi9NDp65jgdxKfizIfuSr2wpwb44yM9SuP4oUQSULOA2 3 | 4iFpLR/c5FAYHU81y9Vx91dQjdZfffaBZuv2zVvteXUkol8Nez7boKbo2E41MTew 4 | 8edtNKZAQVvnaHAC2NCZxjchCzUCDEoUUcl+cIERZ8R48FBqK5iTVcMRIx1akwus 5 | +dhBqP0ykA5TGOWZkJrLM9aUXSPQha9+wXlOpkvu0Ur2nkX8PPJnifWao9UShSar 6 | ll1IqPZNCSlZMwcFYcQNBCpdvITUUYlHvMRQV64bUpOxUGDuJkQL3dLKBlNuBRlJ 7 | BcjBAKw7rFnwwHZcMmQ9tan/dZzpzwjo/T0XjwIDAQABAoIBAQCSHvUqnzDkWjcG 8 | l/Fzg92qXlYBCCC0/ugj1sHcwvVt6Mq5rVE3MpUPwTcYjPlVVTlD4aEEjm/zQuq2 9 | ddxUlOS+r4aIhHrjRT/vSS4FpjnoKeIZxGR6maVxk6DQS3i1QjMYT1CvSpzyVvKH 10 | a+xXMrtmoKxh+085ZAmFJtIuJhUA2yEa4zggCxWnvz8ecLClUPfVDPhdLBHc3KmL 11 | CRpHEC6L/wanvDPRdkkzfKyaJuIJlTDaCg63AY5sDkTW2I57iI/nJ3haSeidfQKz 12 | 39EfbnM1A/YprIakafjAu3frBIsjBVcxwGihZmL/YriTHjOggJF841kT5zFkkv2L 13 | /530Wk6xAoGBAOqZLZ4DIi/zLndEOz1mRbUfjc7GQUdYplBnBwJ22VdS0P4TOXnd 14 | UbJth2MA92NM7ocTYVFl4TVIZY/Y+Prxk7KQdHWzR7JPpKfx9OEVgtSqV0vF9eGI 15 | rKp79Y1T4Mvc3UcQCXX6TP7nHLihEzpS8odm2LW4txrOiLsn4Fq/IWrLAoGBAOVv 16 | 6U4tm3lImotUupKLZPKEBYwruo9qRysoug9FiorP4TjaBVOfltiiHbAQD6aGfVtN 17 | SZpZZtrs17wL7Xl4db5asgMcZd+8Hkfo5siR7AuGW9FZloOjDcXb5wCh9EvjJ74J 18 | Cjw7RqyVymq9t7IP6wnVwj5Ck48YhlOZCz/mzlnNAoGAWq7NYFgLvgc9feLFF23S 19 | IjpJQZWHJEITP98jaYNxbfzYRm49+GphqxwFinKULjFNvq7yHlnIXSVYBOu1CqOZ 20 | GRwXuGuNmlKI7lZr9xmukfAqgGLMMdr4C4qRF4lFyufcLRz42z7exmWlx4ST/yaT 21 | E13hBRWayeTuG5JFei6Jh1MCgYEAqmX4LyC+JFBgvvQZcLboLRkSCa18bADxhENG 22 | FAuAvmFvksqRRC71WETmqZj0Fqgxt7pp3KFjO1rFSprNLvbg85PmO1s+6fCLyLpX 23 | lESTu2d5D71qhK93jigooxalGitFm+SY3mzjq0/AOpBWOn+J/w7rqVPGxXLgaHv0 24 | l+vx+00CgYBOvo9/ImjwYii2jFl+sHEoCzlvpITi2temRlT2j6ulSjCLJgjwEFw9 25 | 8e+vvfQumQOsutakUVyURrkMGNDiNlIv8kv5YLCCkrwN22E6Ghyi69MJUvHQXkc/ 26 | QZhjn/luyfpB5f/BeHFS2bkkxAXo+cfG45ApY3Qfz6/7o+H+vDa6/A== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/emqx_auth_mongo_SUITE_data/ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDAzCCAeugAwIBAgIBATANBgkqhkiG9w0BAQsFADA8MTowOAYDVQQDDDFNeVNR 3 | TF9TZXJ2ZXJfOC4wLjE5X0F1dG9fR2VuZXJhdGVkX0NBX0NlcnRpZmljYXRlMB4X 4 | DTIwMDYxMTAzMzg0NloXDTMwMDYwOTAzMzg0NlowPDE6MDgGA1UEAwwxTXlTUUxf 5 | U2VydmVyXzguMC4xOV9BdXRvX0dlbmVyYXRlZF9DQV9DZXJ0aWZpY2F0ZTCCASIw 6 | DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANJBlAYvTQ6euY4HcSn4syH7kq9s 7 | KcG+OMjPUrj+KFEElCzgNuIhaS0f3ORQGB1PNcvVcfdXUI3WX332gWbr9s1b7Xl1 8 | JKJfDXs+26Cm6NhONTE3sPHnbTSmQEFb52hwAtjQmcY3IQs1AgxKFFHJfnCBEWfE 9 | ePBQaiuYk1XDESMdWpMLrPnYQaj9MpAOUxjlmZCayzPWlF0j0IWvfsF5TqZL7tFK 10 | 9p5F/DzyZ4n1mqPVEoUmq5ZdSKj2TQkpWTMHBWHEDQQqXbyE1FGJR7zEUFeuG1KT 11 | sVBg7iZEC93SygZTbgUZSQXIwQCsO6xZ8MB2XDJkPbWp/3Wc6c8I6P09F48CAwEA 12 | AaMQMA4wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEADKz6bIpP5anp 13 | GgLB0jkclRWuMlS4qqIt4itSsMXPJ/ezpHwECixmgW2TIQl6S1woRkUeMxhT2/Ay 14 | Sn/7aKxuzRagyE5NEGOvrOuAP5RO2ZdNJ/X3/Rh533fK1sOTEEbSsWUvW6iSkZef 15 | rsfZBVP32xBhRWkKRdLeLB4W99ADMa0IrTmZPCXHSSE2V4e1o6zWLXcOZeH1Qh8N 16 | SkelBweR+8r1Fbvy1r3s7eH7DCbYoGEDVLQGOLvzHKBisQHmoDnnF5E9g1eeNRdg 17 | o+vhOKfYCOzeNREJIqS42PHcGhdNRk90ycigPmfUJclz1mDHoMjKR2S5oosTpr65 18 | tNPx3CL7GA== 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /test/emqx_auth_mongo_SUITE_data/client-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDBDCCAeygAwIBAgIBAzANBgkqhkiG9w0BAQsFADA8MTowOAYDVQQDDDFNeVNR 3 | TF9TZXJ2ZXJfOC4wLjE5X0F1dG9fR2VuZXJhdGVkX0NBX0NlcnRpZmljYXRlMB4X 4 | DTIwMDYxMTAzMzg0N1oXDTMwMDYwOTAzMzg0N1owQDE+MDwGA1UEAww1TXlTUUxf 5 | U2VydmVyXzguMC4xOV9BdXRvX0dlbmVyYXRlZF9DbGllbnRfQ2VydGlmaWNhdGUw 6 | ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVYSWpOvCTupz82fc85Opv 7 | EQ7rkB8X2oOMyBCpkyHKBIr1ZQgRDWBp9UVOASq3GnSElm6+T3Kb1QbOffa8GIlw 8 | sjAueKdq5L2eSkmPIEQ7eoO5kEW+4V866hE1LeL/PmHg2lGP0iqZiJYtElhHNQO8 9 | 3y9I7cm3xWMAA3SSWikVtpJRn3qIp2QSrH+tK+/HHbE5QwtPxdir4ULSCSOaM5Yh 10 | Wi5Oto88TZqe1v7SXC864JVvO4LuS7TuSreCdWZyPXTJFBFeCEWSAxonKZrqHbBe 11 | CwKML6/0NuzjaQ51c2tzmVI6xpHj3nnu4cSRx6Jf9WBm+35vm0wk4pohX3ptdzeV 12 | AgMBAAGjDTALMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEBAByQ5zSNeFUH 13 | Aw7JlpZHtHaSEeiiyBHke20ziQ07BK1yi/ms2HAWwQkpZv149sjNuIRH8pkTmkZn 14 | g8PDzSefjLbC9AsWpWV0XNV22T/cdobqLqMBDDZ2+5bsV+jTrOigWd9/AHVZ93PP 15 | IJN8HJn6rtvo2l1bh/CdsX14uVSdofXnuWGabNTydqtMvmCerZsdf6qKqLL+PYwm 16 | RDpgWiRUY7KPBSSlKm/9lJzA+bOe4dHeJzxWFVCJcbpoiTFs1je1V8kKQaHtuW39 17 | ifX6LTKUMlwEECCbDKM8Yq2tm8NjkjCcnFDtKg8zKGPUu+jrFMN5otiC3wnKcP7r 18 | O9EkaPcgYH8= 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /test/emqx_auth_mongo_SUITE_data/client-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEA1WElqTrwk7qc/Nn3POTqbxEO65AfF9qDjMgQqZMhygSK9WUI 3 | EQ1gafVFTgEqtxp0hJZuvk9ym9UGzn32vBiJcLIwLninauS9nkpJjyBEO3qDuZBF 4 | vuFfOuoRNS3i/z5h4NpRj9IqmYiWLRJYRzUDvN8vSO3Jt8VjAAN0klopFbaSUZ96 5 | iKdkEqx/rSvvxx2xOUMLT8XYq+FC0gkjmjOWIVouTraPPE2antb+0lwvOuCVbzuC 6 | 7ku07kq3gnVmcj10yRQRXghFkgMaJyma6h2wXgsCjC+v9Dbs42kOdXNrc5lSOsaR 7 | 49557uHEkceiX/VgZvt+b5tMJOKaIV96bXc3lQIDAQABAoIBAF7yjXmSOn7h6P0y 8 | WCuGiTLG2mbDiLJqj2LTm2Z5i+2Cu/qZ7E76Ls63TxF4v3MemH5vGfQhEhR5ZD/6 9 | GRJ1sKKvB3WGRqjwA9gtojHH39S/nWGy6vYW/vMOOH37XyjIr3EIdIaUtFQBTSHd 10 | Kd71niYrAbVn6fyWHolhADwnVmTMOl5OOAhCdEF4GN3b5aIhIu8BJ7EUzTtHBJIj 11 | CAEfjZFjDs1y1cIgGFJkuIQxMfCpq5recU2qwip7YO6fk//WEjOPu7kSf5IEswL8 12 | jg1dea9rGBV6KaD2xsgsC6Ll6Sb4BbsrHMfflG3K2Lk3RdVqqTFp1Fn1PTLQE/1S 13 | S/SZPYECgYEA9qYcHKHd0+Q5Ty5wgpxKGa4UCWkpwvfvyv4bh8qlmxueB+l2AIdo 14 | ZvkM8gTPagPQ3WypAyC2b9iQu70uOJo1NizTtKnpjDdN1YpDjISJuS/P0x73gZwy 15 | gmoM5AzMtN4D6IbxXtXnPaYICvwLKU80ouEN5ZPM4/ODLUu6gsp0v2UCgYEA3Xgi 16 | zMC4JF0vEKEaK0H6QstaoXUmw/lToZGH3TEojBIkb/2LrHUclygtONh9kJSFb89/ 17 | jbmRRLAOrx3HZKCNGUmF4H9k5OQyAIv6OGBinvLGqcbqnyNlI+Le8zxySYwKMlEj 18 | EMrBCLmSyi0CGFrbZ3mlj/oCET/ql9rNvcK+DHECgYAEx5dH3sMjtgp+RFId1dWB 19 | xePRgt4yTwewkVgLO5wV82UOljGZNQaK6Eyd7AXw8f38LHzh+KJQbIvxd2sL4cEi 20 | OaAoohpKg0/Y0YMZl//rPMf0OWdmdZZs/I0fZjgZUSwWN3c59T8z7KG/RL8an9RP 21 | S7kvN7wCttdV61/D5RR6GQKBgDxCe/WKWpBKaovzydMLWLTj7/0Oi0W3iXHkzzr4 22 | LTgvl4qBSofaNbVLUUKuZTv5rXUG2IYPf99YqCYtzBstNDc1MiAriaBeFtzfOW4t 23 | i6gEFtoLLbuvPc3N5Sv5vn8Ug5G9UfU3td5R4AbyyCcoUZqOFuZd+EIJSiOXfXOs 24 | kVmBAoGBAIU9aPAqhU5LX902oq8KsrpdySONqv5mtoStvl3wo95WIqXNEsFY60wO 25 | q02jKQmJJ2MqhkJm2EoF2Mq8+40EZ5sz8LdgeQ/M0yQ9lAhPi4rftwhpe55Ma9dk 26 | SE9X1c/DMCBEaIjJqVXdy0/EeArwpb8sHkguVVAZUWxzD+phm1gs 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/emqx_auth_mongo_SUITE_data/mongodb.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDBDCCAeygAwIBAgIBAjANBgkqhkiG9w0BAQsFADA8MTowOAYDVQQDDDFNeVNR 3 | TF9TZXJ2ZXJfOC4wLjE5X0F1dG9fR2VuZXJhdGVkX0NBX0NlcnRpZmljYXRlMB4X 4 | DTIwMDYxMTAzMzg0NloXDTMwMDYwOTAzMzg0NlowQDE+MDwGA1UEAww1TXlTUUxf 5 | U2VydmVyXzguMC4xOV9BdXRvX0dlbmVyYXRlZF9TZXJ2ZXJfQ2VydGlmaWNhdGUw 6 | ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCcEnEm5hqP1EbEJycOz8Ua 7 | NWp29QdpFUzTWhkKGhVXk+0msmNTw4NBAFB42moY44OU8wvDideOlJNhPRWveD8z 8 | G2lxzJA91p0UK4et8ia9MmeuCGhdC9jxJ8X69WNlUiPyy0hI/ZsqRq9Z0C2eW0iL 9 | JPXsy4X8Xpw3SFwoXf5pR9RFY5Pb2tuyxqmSestu2VXT/NQjJg4CVDR3mFcHPXZB 10 | 4elRzH0WshExEGkgy0bg20MJeRc2Qdb5Xx+EakbmwroDWaCn3NSGqQ7jv6Vw0doy 11 | TGvS6h6RHBxnyqRfRgKGlCoOMG9/5+rFJC00QpCUG2vHXHWGoWlMlJ3foN7rj5v9 12 | AgMBAAGjDTALMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEBAJ5zt2rj4Ag6 13 | zpN59AWC1Fur8g8l41ksHkSpKPp+PtyO/ngvbMqBpfmK1e7JCKZv/68QXfMyWWAI 14 | hwalqZkXXWHKjuz3wE7dE25PXFXtGJtcZAaj10xt98fzdqt8lQSwh2kbfNwZIz1F 15 | sgAStgE7+ZTcqTgvNB76Os1UK0to+/P0VBWktaVFdyub4Nc2SdPVnZNvrRBXBwOD 16 | 3V8ViwywDOFoE7DvCvwx/SVsvoC0Z4j3AMMovO6oHicP7uU83qsQgm1Qru3YeoLR 17 | +DoVi7IPHbWvN7MqFYn3YjNlByO2geblY7MR0BlqbFlmFrqLsUfjsh2ys7/U/knC 18 | dN/klu446fI= 19 | -----END CERTIFICATE----- 20 | -----BEGIN RSA PRIVATE KEY----- 21 | MIIEowIBAAKCAQEAnBJxJuYaj9RGxCcnDs/FGjVqdvUHaRVM01oZChoVV5PtJrJj 22 | U8ODQQBQeNpqGOODlPMLw4nXjpSTYT0Vr3g/MxtpccyQPdadFCuHrfImvTJnrgho 23 | XQvY8SfF+vVjZVIj8stISP2bKkavWdAtnltIiyT17MuF/F6cN0hcKF3+aUfURWOT 24 | 29rbssapknrLbtlV0/zUIyYOAlQ0d5hXBz12QeHpUcx9FrIRMRBpIMtG4NtDCXkX 25 | NkHW+V8fhGpG5sK6A1mgp9zUhqkO47+lcNHaMkxr0uoekRwcZ8qkX0YChpQqDjBv 26 | f+fqxSQtNEKQlBtrx1x1hqFpTJSd36De64+b/QIDAQABAoIBAFiah66Dt9SruLkn 27 | WR8piUaFyLlcBib8Nq9OWSTJBhDAJERxxb4KIvvGB+l0ZgNXNp5bFPSfzsZdRwZP 28 | PX5uj8Kd71Dxx3mz211WESMJdEC42u+MSmN4lGLkJ5t/sDwXU91E1vbJM0ve8THV 29 | 4/Ag9qA4DX2vVZOeyqT/6YHpSsPNZplqzrbAiwrfHwkctHfgqwOf3QLfhmVQgfCS 30 | VwidBldEUv2whSIiIxh4Rv5St4kA68IBCbJxdpOpyuQBkk6CkxZ7VN9FqOuSd4Pk 31 | Wm7iWyBMZsCmELZh5XAXld4BEt87C5R4CvbPBDZxAv3THk1DNNvpy3PFQfwARRFb 32 | SAToYMECgYEAyL7U8yxpzHDYWd3oCx6vTi9p9N/z0FfAkWrRF6dm4UcSklNiT1Aq 33 | EOnTA+SaW8tV3E64gCWcY23gNP8so/ZseWj6L+peHwtchaP9+KB7yGw2A+05+lOx 34 | VetLTjAOmfpiUXFe5w1q4C1RGhLjZjjzW+GvwdAuchQgUEFaomrV+PUCgYEAxwfH 35 | cmVGFbAktcjU4HSRjKSfawCrut+3YUOLybyku3Q/hP9amG8qkVTFe95CTLjLe2D0 36 | ccaTTpofFEJ32COeck0g0Ujn/qQ+KXRoauOYs4FB1DtqMpqB78wufWEUpDpbd9/h 37 | J+gJdC/IADd4tJW9zA92g8IA7ZtFmqDtiSpQ0ekCgYAQGkaorvJZpN+l7cf0RGTZ 38 | h7IfI2vCVZer0n6tQA9fmLzjoe6r4AlPzAHSOR8sp9XeUy43kUzHKQQoHCPvjw/K 39 | eWJAP7OHF/k2+x2fOPhU7mEy1W+mJdp+wt4Kio5RSaVjVQ3AyPG+w8PSrJszEvRq 40 | dWMMz+851WV2KpfjmWBKlQKBgQC++4j4DZQV5aMkSKV1CIZOBf3vaIJhXKEUFQPD 41 | PmB4fBEjpwCg+zNGp6iktt65zi17o8qMjrb1mtCt2SY04eD932LZUHNFlwcLMmes 42 | Ad+aiDLJ24WJL1f16eDGcOyktlblDZB5gZ/ovJzXEGOkLXglosTfo77OQculmDy2 43 | /UL2WQKBgGeKasmGNfiYAcWio+KXgFkHXWtAXB9B91B1OFnCa40wx+qnl71MIWQH 44 | PQ/CZFNWOfGiNEJIZjrHsfNJoeXkhq48oKcT0AVCDYyLV0VxDO4ejT95mGW6njNd 45 | JpvmhwwAjOvuWVr0tn4iXlSK8irjlJHmwcRjLTJq97vE9fsA2MjI 46 | -----END RSA PRIVATE KEY----- 47 | -------------------------------------------------------------------------------- /test/emqx_auth_mongo_SUITE_data/private_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA1zVmMhPqpSPMmYkKh5wwlRD5XuS8YWJKEM6tjFx61VK8qxHE 3 | YngkC2KnL5EuKAjQZIF3tJskwt0hAat047CCCZxrkNEpbVvSnvnk+A/8bg/Ww1n3 4 | qxzfifhsWfpUKlDnwrtH+ftt+5rZeEkf37XAPy7ZjzecAF9SDV6WSiPeAxUX2+hN 5 | dId42Pf45woo4LFGUlQeagCFkD/R0dpNIMGwcnkKCUikiBqr2ijSIgvRtBfZ9fBG 6 | jFGER2uE/Eay4AgcQsHue8skRwDCng8OnqtPnBtTytmqTy9V/BRgsVKUoksm6wsx 7 | kUYwgHeaq7UCvlCm25SZ7yRyd4k8t0BKDf2h+wIDAQABAoIBAEQcrHmRACTADdNS 8 | IjkFYALt2l8EOfMAbryfDSJtapr1kqz59JPNvmq0EIHnixo0n/APYdmReLML1ZR3 9 | tYkSpjVwgkLVUC1CcIjMQoGYXaZf8PLnGJHZk45RR8m6hsTV0mQ5bfBaeVa2jbma 10 | OzJMjcnxg/3l9cPQZ2G/3AUfEPccMxOXp1KRz3mUQcGnKJGtDbN/kfmntcwYoxaE 11 | Zg4RoeKAoMpK1SSHAiJKe7TnztINJ7uygR9XSzNd6auY8A3vomSIjpYO7XL+lh7L 12 | izm4Ir3Gb/eCYBvWgQyQa2KCJgK/sQyEs3a09ngofSEUhQJQYhgZDwUj+fDDOGqj 13 | hCZOA8ECgYEA+ZWuHdcUQ3ygYhLds2QcogUlIsx7C8n/Gk/FUrqqXJrTkuO0Eqqa 14 | B47lCITvmn2zm0ODfSFIARgKEUEDLS/biZYv7SUTrFqBLcet+aGI7Dpv91CgB75R 15 | tNzcIf8VxoiP0jPqdbh9mLbbxGi5Uc4p9TVXRljC4hkswaouebWee0sCgYEA3L2E 16 | YB3kiHrhPI9LHS5Px9C1w+NOu5wP5snxrDGEgaFCvL6zgY6PflacppgnmTXl8D1x 17 | im0IDKSw5dP3FFonSVXReq3CXDql7UnhfTCiLDahV7bLxTH42FofcBpDN3ERdOal 18 | 58RwQh6VrLkzQRVoObo+hbGlFiwwSAfQC509FhECgYBsRSBpVXo25IN2yBRg09cP 19 | +gdoFyhxrsj5kw1YnB13WrrZh+oABv4WtUhp77E5ZbpaamlKCPwBbXpAjeFg4tfr 20 | 0bksuN7V79UGFQ9FsWuCfr8/nDwv38H2IbFlFhFONMOfPmJBey0Q6JJhm8R41mSh 21 | OOiJXcv85UrjIH5U0hLUDQKBgQDVLOU5WcUJlPoOXSgiT0ZW5xWSzuOLRUUKEf6l 22 | 19BqzAzCcLy0orOrRAPW01xylt2v6/bJw1Ahva7k1ZZo/kOwjANYoZPxM+ZoSZBN 23 | MXl8j2mzZuJVV1RFxItV3NcLJNPB/Lk+IbRz9kt/2f9InF7iWR3mSU/wIM6j0X+2 24 | p6yFsQKBgQCM/ldWb511lA+SNkqXB2P6WXAgAM/7+jwsNHX2ia2Ikufm4SUEKMSv 25 | mti/nZkHDHsrHU4wb/2cOAywMELzv9EHzdcoenjBQP65OAc/1qWJs+LnBcCXfqKk 26 | aHjEZW6+brkHdRGLLY3YAHlt/AUL+RsKPJfN72i/FSpmu+52G36eeQ== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/emqx_auth_mongo_SUITE_data/public_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1zVmMhPqpSPMmYkKh5ww 3 | lRD5XuS8YWJKEM6tjFx61VK8qxHEYngkC2KnL5EuKAjQZIF3tJskwt0hAat047CC 4 | CZxrkNEpbVvSnvnk+A/8bg/Ww1n3qxzfifhsWfpUKlDnwrtH+ftt+5rZeEkf37XA 5 | Py7ZjzecAF9SDV6WSiPeAxUX2+hNdId42Pf45woo4LFGUlQeagCFkD/R0dpNIMGw 6 | cnkKCUikiBqr2ijSIgvRtBfZ9fBGjFGER2uE/Eay4AgcQsHue8skRwDCng8OnqtP 7 | nBtTytmqTy9V/BRgsVKUoksm6wsxkUYwgHeaq7UCvlCm25SZ7yRyd4k8t0BKDf2h 8 | +wIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /test/emqx_auth_mongo_SUITE_data/server-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDBDCCAeygAwIBAgIBAjANBgkqhkiG9w0BAQsFADA8MTowOAYDVQQDDDFNeVNR 3 | TF9TZXJ2ZXJfOC4wLjE5X0F1dG9fR2VuZXJhdGVkX0NBX0NlcnRpZmljYXRlMB4X 4 | DTIwMDYxMTAzMzg0NloXDTMwMDYwOTAzMzg0NlowQDE+MDwGA1UEAww1TXlTUUxf 5 | U2VydmVyXzguMC4xOV9BdXRvX0dlbmVyYXRlZF9TZXJ2ZXJfQ2VydGlmaWNhdGUw 6 | ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCcEnEm5hqP1EbEJycOz8Ua 7 | NWp29QdpFUzTWhkKGhVXk+0msmNTw4NBAFB42moY44OU8wvDideOlJNhPRWveD8z 8 | G2lxzJA91p0UK4et8ia9MmeuCGhdC9jxJ8X69WNlUiPyy0hI/ZsqRq9Z0C2eW0iL 9 | JPXsy4X8Xpw3SFwoXf5pR9RFY5Pb2tuyxqmSestu2VXT/NQjJg4CVDR3mFcHPXZB 10 | 4elRzH0WshExEGkgy0bg20MJeRc2Qdb5Xx+EakbmwroDWaCn3NSGqQ7jv6Vw0doy 11 | TGvS6h6RHBxnyqRfRgKGlCoOMG9/5+rFJC00QpCUG2vHXHWGoWlMlJ3foN7rj5v9 12 | AgMBAAGjDTALMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEBAJ5zt2rj4Ag6 13 | zpN59AWC1Fur8g8l41ksHkSpKPp+PtyO/ngvbMqBpfmK1e7JCKZv/68QXfMyWWAI 14 | hwalqZkXXWHKjuz3wE7dE25PXFXtGJtcZAaj10xt98fzdqt8lQSwh2kbfNwZIz1F 15 | sgAStgE7+ZTcqTgvNB76Os1UK0to+/P0VBWktaVFdyub4Nc2SdPVnZNvrRBXBwOD 16 | 3V8ViwywDOFoE7DvCvwx/SVsvoC0Z4j3AMMovO6oHicP7uU83qsQgm1Qru3YeoLR 17 | +DoVi7IPHbWvN7MqFYn3YjNlByO2geblY7MR0BlqbFlmFrqLsUfjsh2ys7/U/knC 18 | dN/klu446fI= 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /test/emqx_auth_mongo_SUITE_data/server-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAnBJxJuYaj9RGxCcnDs/FGjVqdvUHaRVM01oZChoVV5PtJrJj 3 | U8ODQQBQeNpqGOODlPMLw4nXjpSTYT0Vr3g/MxtpccyQPdadFCuHrfImvTJnrgho 4 | XQvY8SfF+vVjZVIj8stISP2bKkavWdAtnltIiyT17MuF/F6cN0hcKF3+aUfURWOT 5 | 29rbssapknrLbtlV0/zUIyYOAlQ0d5hXBz12QeHpUcx9FrIRMRBpIMtG4NtDCXkX 6 | NkHW+V8fhGpG5sK6A1mgp9zUhqkO47+lcNHaMkxr0uoekRwcZ8qkX0YChpQqDjBv 7 | f+fqxSQtNEKQlBtrx1x1hqFpTJSd36De64+b/QIDAQABAoIBAFiah66Dt9SruLkn 8 | WR8piUaFyLlcBib8Nq9OWSTJBhDAJERxxb4KIvvGB+l0ZgNXNp5bFPSfzsZdRwZP 9 | PX5uj8Kd71Dxx3mz211WESMJdEC42u+MSmN4lGLkJ5t/sDwXU91E1vbJM0ve8THV 10 | 4/Ag9qA4DX2vVZOeyqT/6YHpSsPNZplqzrbAiwrfHwkctHfgqwOf3QLfhmVQgfCS 11 | VwidBldEUv2whSIiIxh4Rv5St4kA68IBCbJxdpOpyuQBkk6CkxZ7VN9FqOuSd4Pk 12 | Wm7iWyBMZsCmELZh5XAXld4BEt87C5R4CvbPBDZxAv3THk1DNNvpy3PFQfwARRFb 13 | SAToYMECgYEAyL7U8yxpzHDYWd3oCx6vTi9p9N/z0FfAkWrRF6dm4UcSklNiT1Aq 14 | EOnTA+SaW8tV3E64gCWcY23gNP8so/ZseWj6L+peHwtchaP9+KB7yGw2A+05+lOx 15 | VetLTjAOmfpiUXFe5w1q4C1RGhLjZjjzW+GvwdAuchQgUEFaomrV+PUCgYEAxwfH 16 | cmVGFbAktcjU4HSRjKSfawCrut+3YUOLybyku3Q/hP9amG8qkVTFe95CTLjLe2D0 17 | ccaTTpofFEJ32COeck0g0Ujn/qQ+KXRoauOYs4FB1DtqMpqB78wufWEUpDpbd9/h 18 | J+gJdC/IADd4tJW9zA92g8IA7ZtFmqDtiSpQ0ekCgYAQGkaorvJZpN+l7cf0RGTZ 19 | h7IfI2vCVZer0n6tQA9fmLzjoe6r4AlPzAHSOR8sp9XeUy43kUzHKQQoHCPvjw/K 20 | eWJAP7OHF/k2+x2fOPhU7mEy1W+mJdp+wt4Kio5RSaVjVQ3AyPG+w8PSrJszEvRq 21 | dWMMz+851WV2KpfjmWBKlQKBgQC++4j4DZQV5aMkSKV1CIZOBf3vaIJhXKEUFQPD 22 | PmB4fBEjpwCg+zNGp6iktt65zi17o8qMjrb1mtCt2SY04eD932LZUHNFlwcLMmes 23 | Ad+aiDLJ24WJL1f16eDGcOyktlblDZB5gZ/ovJzXEGOkLXglosTfo77OQculmDy2 24 | /UL2WQKBgGeKasmGNfiYAcWio+KXgFkHXWtAXB9B91B1OFnCa40wx+qnl71MIWQH 25 | PQ/CZFNWOfGiNEJIZjrHsfNJoeXkhq48oKcT0AVCDYyLV0VxDO4ejT95mGW6njNd 26 | JpvmhwwAjOvuWVr0tn4iXlSK8irjlJHmwcRjLTJq97vE9fsA2MjI 27 | -----END RSA PRIVATE KEY----- 28 | --------------------------------------------------------------------------------