Browse Source

Migrate plugins from tag 4.2.2

terry-xiaoyu 5 years ago
parent
commit
0cd1c57a54
100 changed files with 7022 additions and 0 deletions
  1. 117 0
      apps/emqx_auth_clientid/README.md
  2. 3 0
      apps/emqx_auth_clientid/TODO
  3. 13 0
      apps/emqx_auth_clientid/include/emqx_auth_clientid.hrl
  4. 26 0
      apps/emqx_auth_clientid/priv/emqx_auth_clientid.schema
  5. 1 0
      apps/emqx_auth_clientid/rebar.config
  6. 14 0
      apps/emqx_auth_clientid/src/emqx_auth_clientid.app.src
  7. 24 0
      apps/emqx_auth_clientid/src/emqx_auth_clientid.app.src.script
  8. 170 0
      apps/emqx_auth_clientid/src/emqx_auth_clientid.erl
  9. 127 0
      apps/emqx_auth_clientid/src/emqx_auth_clientid_api.erl
  10. 53 0
      apps/emqx_auth_clientid/src/emqx_auth_clientid_app.erl
  11. 191 0
      apps/emqx_auth_clientid/test/emqx_auth_clientid_SUITE.erl
  12. 100 0
      apps/emqx_auth_http/README.md
  13. 25 0
      apps/emqx_auth_http/include/emqx_auth_http.hrl
  14. 165 0
      apps/emqx_auth_http/priv/emqx_auth_http.schema
  15. 1 0
      apps/emqx_auth_http/rebar.config
  16. 92 0
      apps/emqx_auth_http/src/emqx_acl_http.erl
  17. 14 0
      apps/emqx_auth_http/src/emqx_auth_http.app.src
  18. 24 0
      apps/emqx_auth_http/src/emqx_auth_http.app.src.script
  19. 116 0
      apps/emqx_auth_http/src/emqx_auth_http.erl
  20. 103 0
      apps/emqx_auth_http/src/emqx_auth_http_app.erl
  21. 101 0
      apps/emqx_auth_http/src/emqx_auth_http_cli.erl
  22. 167 0
      apps/emqx_auth_http/test/emqx_auth_http_SUITE.erl
  23. 152 0
      apps/emqx_auth_http/test/http_auth_server.erl
  24. 90 0
      apps/emqx_auth_jwt/README.md
  25. 2 0
      apps/emqx_auth_jwt/TODO.md
  26. 3 0
      apps/emqx_auth_jwt/doc/hmac-vs-ecdsa-for-jwt.txt
  27. 48 0
      apps/emqx_auth_jwt/priv/emqx_auth_jwt.schema
  28. 3 0
      apps/emqx_auth_jwt/rebar.config
  29. 14 0
      apps/emqx_auth_jwt/src/emqx_auth_jwt.app.src
  30. 24 0
      apps/emqx_auth_jwt/src/emqx_auth_jwt.app.src.script
  31. 146 0
      apps/emqx_auth_jwt/src/emqx_auth_jwt.erl
  32. 69 0
      apps/emqx_auth_jwt/src/emqx_auth_jwt_app.erl
  33. 137 0
      apps/emqx_auth_jwt/test/emqx_auth_jwt_SUITE.erl
  34. 26 0
      apps/emqx_auth_ldap/.ci/docker-compose.yml
  35. 26 0
      apps/emqx_auth_ldap/.ci/emqx-ldap/Dockerfile
  36. 16 0
      apps/emqx_auth_ldap/.ci/emqx-ldap/slapd.conf
  37. 96 0
      apps/emqx_auth_ldap/README.md
  38. 135 0
      apps/emqx_auth_ldap/emqx.io.ldif
  39. 46 0
      apps/emqx_auth_ldap/emqx.schema
  40. 23 0
      apps/emqx_auth_ldap/include/emqx_auth_ldap.hrl
  41. 176 0
      apps/emqx_auth_ldap/priv/emqx_auth_ldap.schema
  42. 3 0
      apps/emqx_auth_ldap/rebar.config
  43. 98 0
      apps/emqx_auth_ldap/src/emqx_acl_ldap.erl
  44. 14 0
      apps/emqx_auth_ldap/src/emqx_auth_ldap.app.src
  45. 24 0
      apps/emqx_auth_ldap/src/emqx_auth_ldap.app.src.script
  46. 210 0
      apps/emqx_auth_ldap/src/emqx_auth_ldap.erl
  47. 78 0
      apps/emqx_auth_ldap/src/emqx_auth_ldap_app.erl
  48. 150 0
      apps/emqx_auth_ldap/src/emqx_auth_ldap_cli.erl
  49. 35 0
      apps/emqx_auth_ldap/src/emqx_auth_ldap_sup.erl
  50. 20 0
      apps/emqx_auth_ldap/test/certs/cacert.pem
  51. 19 0
      apps/emqx_auth_ldap/test/certs/cert.pem
  52. 19 0
      apps/emqx_auth_ldap/test/certs/client-cert.pem
  53. 27 0
      apps/emqx_auth_ldap/test/certs/client-key.pem
  54. 27 0
      apps/emqx_auth_ldap/test/certs/key.pem
  55. 153 0
      apps/emqx_auth_ldap/test/emqx_auth_ldap_SUITE.erl
  56. 114 0
      apps/emqx_auth_ldap/test/emqx_auth_ldap_bind_as_user_SUITE.erl
  57. 2 0
      apps/emqx_auth_mnesia/README.md
  58. 35 0
      apps/emqx_auth_mnesia/include/emqx_auth_mnesia.hrl
  59. 35 0
      apps/emqx_auth_mnesia/priv/emqx_auth_mnesia.schema
  60. 1 0
      apps/emqx_auth_mnesia/rebar.config
  61. 83 0
      apps/emqx_auth_mnesia/src/emqx_acl_mnesia.erl
  62. 148 0
      apps/emqx_auth_mnesia/src/emqx_acl_mnesia_api.erl
  63. 14 0
      apps/emqx_auth_mnesia/src/emqx_auth_mnesia.app.src
  64. 24 0
      apps/emqx_auth_mnesia/src/emqx_auth_mnesia.app.src.script
  65. 76 0
      apps/emqx_auth_mnesia/src/emqx_auth_mnesia.erl
  66. 201 0
      apps/emqx_auth_mnesia/src/emqx_auth_mnesia_api.erl
  67. 70 0
      apps/emqx_auth_mnesia/src/emqx_auth_mnesia_app.erl
  68. 193 0
      apps/emqx_auth_mnesia/src/emqx_auth_mnesia_cli.erl
  69. 36 0
      apps/emqx_auth_mnesia/src/emqx_auth_mnesia_sup.erl
  70. 279 0
      apps/emqx_auth_mnesia/test/emqx_acl_mnesia_SUITE.erl
  71. 259 0
      apps/emqx_auth_mnesia/test/emqx_auth_mnesia_SUITE.erl
  72. 31 0
      apps/emqx_auth_mongo/CHANGES
  73. 192 0
      apps/emqx_auth_mongo/README.md
  74. 31 0
      apps/emqx_auth_mongo/docker-compose-ssl.yml
  75. 27 0
      apps/emqx_auth_mongo/docker-compose.yml
  76. 37 0
      apps/emqx_auth_mongo/include/emqx_auth_mongo.hrl
  77. 292 0
      apps/emqx_auth_mongo/priv/emqx_auth_mongo.schema
  78. 2 0
      apps/emqx_auth_mongo/rebar.config
  79. 91 0
      apps/emqx_auth_mongo/src/emqx_acl_mongo.erl
  80. 14 0
      apps/emqx_auth_mongo/src/emqx_auth_mongo.app.src
  81. 24 0
      apps/emqx_auth_mongo/src/emqx_auth_mongo.app.src.script
  82. 134 0
      apps/emqx_auth_mongo/src/emqx_auth_mongo.erl
  83. 87 0
      apps/emqx_auth_mongo/src/emqx_auth_mongo_app.erl
  84. 34 0
      apps/emqx_auth_mongo/src/emqx_auth_mongo_sup.erl
  85. 174 0
      apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE.erl
  86. 27 0
      apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/ca-key.pem
  87. 19 0
      apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/ca.pem
  88. 19 0
      apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/client-cert.pem
  89. 27 0
      apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/client-key.pem
  90. 46 0
      apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/mongodb.pem
  91. 27 0
      apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/private_key.pem
  92. 9 0
      apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/public_key.pem
  93. 19 0
      apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/server-cert.pem
  94. 27 0
      apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/server-key.pem
  95. 167 0
      apps/emqx_auth_mysql/README.md
  96. 41 0
      apps/emqx_auth_mysql/docker-compose-ssl.yml
  97. 34 0
      apps/emqx_auth_mysql/docker-compose.yml
  98. 23 0
      apps/emqx_auth_mysql/include/emqx_auth_mysql.hrl
  99. 41 0
      apps/emqx_auth_mysql/mqtt.sql
  100. 0 0
      apps/emqx_auth_mysql/priv/emqx_auth_mysql.schema

+ 117 - 0
apps/emqx_auth_clientid/README.md

@@ -0,0 +1,117 @@
+emqx_auth_clientid
+==================
+
+Authentication with ClientId and Password
+
+Build
+-----
+
+```
+make && make tests
+```
+
+Configuration
+-------------
+
+etc/emqx_auth_clientid.conf:
+
+```
+## Password hash.
+##
+## Value: plain | md5 | sha | sha256
+auth.client.password_hash = sha256
+```
+
+[REST API](https://developer.emqx.io/docs/emq/v3/en/rest.html)
+------------
+
+List all clientids:
+
+```
+# Request
+GET api/v4/auth_clientid
+
+# Response
+{
+    "code": 0,
+    "data": ["clientid1"]
+}
+```
+
+Add clientid:
+
+```
+# Request
+POST api/v4/auth_clientid
+{
+    "clientid": "a_client_id",
+    "password": "password"
+}
+
+# Response
+{
+    "code": 0
+}
+```
+
+Update password for a clientid:
+
+```
+# Request
+PUT api/v4/auth_clientid/$CLIENTID
+
+{
+    "password": "password"
+}
+
+# Response
+{
+    "code": 0
+}
+```
+
+Lookup a clientid info:
+
+```
+# Request
+GET api/v4/auth_clientid/$CLIENTID
+
+# Response
+{
+    "code": 0,
+    "data": {
+        "clientid": "a_client_id",
+        "password": "hash_password" 
+    }
+}
+```
+
+Delete a clientid:
+
+```
+# Request
+DELETE api/v4/auth_clientid/$CLIENTID
+
+# Response
+{
+    "code": 0
+}
+```
+
+Load the Plugin
+---------------
+
+```
+./bin/emqx_ctl plugins load emqx_auth_clientid
+```
+
+License
+-------
+
+Apache License Version 2.0
+
+Author
+------
+
+EMQ X Team.
+

+ 3 - 0
apps/emqx_auth_clientid/TODO

@@ -0,0 +1,3 @@
+1. Hash password
+2. Add Test cases
+3. Hot Reloader

+ 13 - 0
apps/emqx_auth_clientid/include/emqx_auth_clientid.hrl

@@ -0,0 +1,13 @@
+-define(APP, emqx_auth_clientid).
+
+-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)).

+ 26 - 0
apps/emqx_auth_clientid/priv/emqx_auth_clientid.schema

@@ -0,0 +1,26 @@
+%%-*- mode: erlang -*-
+%% emqx_auth_clientid config mapping
+
+{mapping, "auth.client.password_hash", "emqx_auth_clientid.password_hash", [
+  {default, sha256},
+  {datatype, {enum, [plain, md5, sha, sha256]}}
+]}.
+
+{mapping, "auth.client.$id.clientid", "emqx_auth_clientid.client_list", [
+  {datatype, string}
+]}.
+
+{mapping, "auth.client.$id.password", "emqx_auth_clientid.client_list", [
+  {datatype, string}
+]}.
+
+{translation, "emqx_auth_clientid.client_list", fun(Conf) ->
+  ClientList = cuttlefish_variable:filter_by_prefix("auth.client", Conf),
+  lists:foldl(
+       fun({["auth", "client", Id, "clientid"], ClientId}, AccIn) ->
+        [{ClientId, cuttlefish:conf_get("auth.client." ++ Id ++ ".password", Conf)} | AccIn];
+       (_, AccIn) ->
+        AccIn
+       end, [], ClientList)
+end}.
+

+ 1 - 0
apps/emqx_auth_clientid/rebar.config

@@ -0,0 +1 @@
+{deps, []}.

+ 14 - 0
apps/emqx_auth_clientid/src/emqx_auth_clientid.app.src

@@ -0,0 +1,14 @@
+{application, emqx_auth_clientid,
+ [{description, "EMQ X Authentication with ClientId/Password"},
+  {vsn, "git"},
+  {modules, []},
+  {registered, [emqx_auth_clientid_sup]},
+  {applications, [kernel,stdlib,minirest,emqx_passwd,emqx_libs]},
+  {mod, {emqx_auth_clientid_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-clientid"}
+          ]}
+ ]}.

+ 24 - 0
apps/emqx_auth_clientid/src/emqx_auth_clientid.app.src.script

@@ -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.
+

+ 170 - 0
apps/emqx_auth_clientid/src/emqx_auth_clientid.erl

@@ -0,0 +1,170 @@
+%%--------------------------------------------------------------------
+%% 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_clientid).
+
+-include("emqx_auth_clientid.hrl").
+
+-include_lib("emqx_libs/include/emqx.hrl").
+
+%% CLI callbacks
+-export([cli/1]).
+
+%% APIs
+-export([ add_clientid/2
+        , update_password/2
+        , lookup_clientid/1
+        , remove_clientid/1
+        , all_clientids/0
+        ]).
+
+-export([unwrap_salt/1]).
+
+%% Auth callbacks
+-export([ init/1
+        , register_metrics/0
+        , check/3
+        , description/0
+        ]).
+
+-define(TAB, ?MODULE).
+
+-record(?TAB, {clientid, password}).
+
+%%--------------------------------------------------------------------
+%% CLI
+%%--------------------------------------------------------------------
+
+cli(["list"]) ->
+    ClientIds = mnesia:dirty_all_keys(?TAB),
+    [emqx_ctl:print("~s~n", [ClientId]) || ClientId <- ClientIds];
+
+cli(["add", ClientId, Password]) ->
+    Ok = add_clientid(iolist_to_binary(ClientId), iolist_to_binary(Password)),
+    emqx_ctl:print("~p~n", [Ok]);
+
+cli(["update", ClientId, NewPassword]) ->
+    Ok = update_password(iolist_to_binary(ClientId), iolist_to_binary(NewPassword)),
+    emqx_ctl:print("~p~n", [Ok]);
+
+cli(["del", ClientId]) ->
+    emqx_ctl:print("~p~n", [remove_clientid(iolist_to_binary(ClientId))]);
+
+cli(_) ->
+    emqx_ctl:usage([{"clientid list", "List ClientId"},
+                    {"clientid add <ClientId> <Password>", "Add ClientId"},
+                    {"clientid update <Clientid> <NewPassword>", "Update Clientid"},
+                    {"clientid del <ClientId>", "Delete ClientId"}]).
+
+%%--------------------------------------------------------------------
+%% API
+%%--------------------------------------------------------------------
+%% @doc Add clientid with password
+-spec(add_clientid(binary(), binary()) -> {atomic, ok} | {aborted, any()}).
+add_clientid(ClientId, Password) ->
+    Client = #?TAB{clientid = ClientId, password = encrypted_data(Password)},
+    ret(mnesia:transaction(fun do_add_clientid/1, [Client])).
+
+do_add_clientid(Client = #?TAB{clientid = ClientId}) ->
+    case mnesia:read(?TAB, ClientId) of
+        [] -> mnesia:write(Client);
+        [_|_] -> mnesia:abort(exitsted)
+    end.
+
+%% @doc Update clientid with newpassword
+-spec(update_password(binary(), binary()) -> {atomic, ok} | {aborted, any()}).
+update_password(ClientId, NewPassword) ->
+    Client = #?TAB{clientid = ClientId, password = encrypted_data(NewPassword)},
+    ret(mnesia:transaction(fun do_update_password/1, [Client])).
+
+do_update_password(Client = #?TAB{clientid = ClientId}) ->
+    case mnesia:read(?TAB, ClientId) of
+        [_|_] -> mnesia:write(Client);
+        [] -> mnesia:abort(noexitsted)
+    end.
+
+%% @doc Lookup clientid
+-spec(lookup_clientid(binary()) -> list(#?TAB{})).
+lookup_clientid(ClientId) ->
+    mnesia:dirty_read(?TAB, ClientId).
+
+%% @doc Lookup all clientids
+-spec(all_clientids() -> list(binary())).
+all_clientids() ->
+    mnesia:dirty_all_keys(?TAB).
+
+%% @doc Remove clientid
+-spec(remove_clientid(binary()) -> {atomic, ok} | {aborted, term()}).
+remove_clientid(ClientId) ->
+    ret(mnesia:transaction(fun mnesia:delete/1, [{?TAB, ClientId}])).
+
+unwrap_salt(<<_Salt:4/binary, HashPasswd/binary>>) ->
+    HashPasswd.
+
+%% @private
+ret({atomic, ok})     -> ok;
+ret({aborted, Error}) -> {error, Error}.
+
+%%--------------------------------------------------------------------
+%% Auth callbacks
+%%--------------------------------------------------------------------
+
+init(DefaultIds) ->
+    ok = ekka_mnesia:create_table(?TAB, [
+            {disc_copies, [node()]},
+            {attributes, record_info(fields, ?TAB)},
+            {storage_properties, [{ets, [{read_concurrency, true}]}]}]),
+    lists:foreach(fun add_default_clientid/1, DefaultIds),
+    ok = ekka_mnesia:copy_table(?TAB, disc_copies).
+
+%% @private
+add_default_clientid({ClientId, Password}) ->
+    add_clientid(iolist_to_binary(ClientId), iolist_to_binary(Password)).
+
+register_metrics() ->
+    [emqx_metrics:ensure(MetricName) || MetricName <- ?AUTH_METRICS].
+
+check(#{clientid := ClientId, password := Password}, AuthResult, #{hash_type := HashType}) ->
+    case mnesia:dirty_read(?TAB, ClientId) of
+        [] -> emqx_metrics:inc(?AUTH_METRICS(ignore));
+        [#?TAB{password = <<Salt:4/binary, Hash/binary>>}] ->
+            case Hash =:= hash(Password, Salt, HashType) of
+                true ->
+                    emqx_metrics:inc(?AUTH_METRICS(success)),
+                    {stop, AuthResult#{auth_result => success, anonymous => false}};
+                false ->
+                    emqx_metrics:inc(?AUTH_METRICS(failure)),
+                    {stop, AuthResult#{auth_result => not_authorized, anonymous => false}}
+            end
+    end.
+
+description() ->
+    "ClientId Authentication Module".
+
+encrypted_data(Password) ->
+    HashType = application:get_env(emqx_auth_clientid, password_hash, sha256),
+    SaltBin = salt(),
+    <<SaltBin/binary, (hash(Password, SaltBin, HashType))/binary>>.
+
+hash(undefined, SaltBin, HashType) ->
+    hash(<<>>, SaltBin, HashType);
+hash(Password, SaltBin, HashType) ->
+    emqx_passwd:hash(HashType, <<SaltBin/binary, Password/binary>>).
+
+salt() ->
+    rand:seed(exsplus, erlang:timestamp()),
+    Salt = rand:uniform(16#ffffffff), <<Salt:32>>.
+

+ 127 - 0
apps/emqx_auth_clientid/src/emqx_auth_clientid_api.erl

@@ -0,0 +1,127 @@
+%%--------------------------------------------------------------------
+%% 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_clientid_api).
+
+-include("emqx_auth_clientid.hrl").
+
+-export([ list/2
+        , lookup/2
+        , add/2
+        , update/2
+        , delete/2
+        ]).
+
+-import(proplists, [get_value/2]).
+-import(minirest,  [return/0, return/1]).
+
+-rest_api(#{name   => list_clientid,
+            method => 'GET',
+            path   => "/auth_clientid",
+            func   => list,
+            descr  => "List available clientid in the cluster"
+           }).
+
+-rest_api(#{name   => lookup_clientid,
+            method => 'GET',
+            path   => "/auth_clientid/:bin:clientid",
+            func   => lookup,
+            descr  => "Lookup clientid in the cluster"
+           }).
+
+-rest_api(#{name   => add_clientid,
+            method => 'POST',
+            path   => "/auth_clientid",
+            func   => add,
+            descr  => "Add clientid in the cluster"
+           }).
+
+-rest_api(#{name   => update_clientid,
+            method => 'PUT',
+            path   => "/auth_clientid/:bin:clientid",
+            func   => update,
+            descr  => "Update clientid in the cluster"
+           }).
+
+-rest_api(#{name   => delete_clientid,
+            method => 'DELETE',
+            path   => "/auth_clientid/:bin:clientid",
+            func   => delete,
+            descr  => "Delete clientid in the cluster"
+           }).
+
+list(_Bindings, _Params) ->
+    return({ok, emqx_auth_clientid:all_clientids()}).
+
+lookup(#{clientid := ClientId}, _Params) ->
+    case emqx_auth_clientid:lookup_clientid(ClientId) of
+        [] -> return({error, not_found});
+        Auth -> return({ok, format(Auth)})
+    end.
+
+add(_Bindings, Params) ->
+    ClientId = get_value(<<"clientid">>, Params),
+    Password = get_value(<<"password">>, Params),
+    case validate([clientid, password], [ClientId, Password]) of
+        ok ->
+            case emqx_auth_clientid:add_clientid(ClientId, Password) of
+                ok -> return();
+                Error -> return(Error)
+            end;
+        Error -> return(Error)
+    end.
+
+update(#{clientid := ClientId}, Params) ->
+    Password = get_value(<<"password">>, Params),
+    case validate([password], [Password]) of
+        ok ->
+            case emqx_auth_clientid:update_password(ClientId, Password) of
+                ok -> return();
+                Error -> return(Error)
+            end;
+        Error -> return(Error)
+    end.
+
+delete(#{clientid := ClientId}, _) ->
+    case emqx_auth_clientid:remove_clientid(ClientId) of
+        ok -> return();
+        Error -> return(Error)
+    end.
+
+%%------------------------------------------------------------------------------
+%% Interval Funcs
+%%------------------------------------------------------------------------------
+
+format([{?APP, ClientId, Password}]) ->
+    #{clientid => ClientId,
+      password => emqx_auth_clientid:unwrap_salt(Password)}.
+
+validate([], []) ->
+    ok;
+validate([K|Keys], [V|Values]) ->
+    case validation(K, V) of
+        false -> {error, K};
+        true  -> validate(Keys, Values)
+    end.
+
+validation(clientid, V) when is_binary(V)
+                        andalso byte_size(V) > 0 ->
+    true;
+validation(password, V) when is_binary(V)
+                        andalso byte_size(V) > 0 ->
+    true;
+validation(_, _) ->
+    false.

+ 53 - 0
apps/emqx_auth_clientid/src/emqx_auth_clientid_app.erl

@@ -0,0 +1,53 @@
+%%--------------------------------------------------------------------
+%% 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_clientid_app).
+
+-include("emqx_auth_clientid.hrl").
+
+-behaviour(application).
+
+-emqx_plugin(auth).
+
+-export([ start/2
+        , stop/1
+        ]).
+
+-behaviour(supervisor).
+
+-export([init/1]).
+
+start(_Type, _Args) ->
+    emqx_ctl:register_command(clientid, {?APP, cli}, []),
+    emqx_auth_clientid:register_metrics(),
+    HashType = application:get_env(?APP, password_hash, sha256),
+    Params = #{hash_type => HashType},
+    emqx:hook('client.authenticate', fun emqx_auth_clientid:check/3, [Params]),
+    DefaultIds = application:get_env(?APP, client_list, []),
+    ok = emqx_auth_clientid:init(DefaultIds),
+    supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+stop(_State) ->
+    emqx:unhook('client.authenticate', fun emqx_auth_clientid:check/3),
+    emqx_ctl:unregister_command(clientid).
+
+%%--------------------------------------------------------------------
+%% Dummy supervisor
+%%--------------------------------------------------------------------
+
+init([]) ->
+    {ok, { {one_for_all, 1, 10}, []} }.
+

+ 191 - 0
apps/emqx_auth_clientid/test/emqx_auth_clientid_SUITE.erl

@@ -0,0 +1,191 @@
+%% 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_clientid_SUITE).
+
+-compile(nowarn_export_all).
+-compile(export_all).
+
+-include_lib("emqx_libs/include/emqx.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").
+
+-define(CLIENTID,  <<"client_id_for_ct">>).
+-define(PASSWORD,  <<"password">>).
+-define(NPASSWORD, <<"password1">>).
+-define(USER,      #{clientid => ?CLIENTID, zone => external}).
+
+all() ->
+    emqx_ct:all(?MODULE).
+
+init_per_suite(Config) ->
+    emqx_ct_helpers:start_apps([emqx_auth_clientid, emqx_management], fun set_special_configs/1),
+    create_default_app(),
+    Config.
+
+end_per_suite(_Config) ->
+    emqx_ct_helpers:stop_apps([emqx_auth_clientid, emqx_management]).
+
+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_managing(_) ->
+    clean_all_clientids(),
+
+    ok = emqx_auth_clientid:add_clientid(?CLIENTID, ?PASSWORD),
+    ?assertNotEqual(emqx_auth_clientid:lookup_clientid(?CLIENTID), []),
+
+    {ok, #{auth_result := success,
+           anonymous := false}} = emqx_access_control:authenticate(?USER#{password => ?PASSWORD}),
+
+    {error, _} = emqx_access_control:authenticate(?USER#{password => ?NPASSWORD}),
+
+    emqx_auth_clientid:update_password(?CLIENTID, ?NPASSWORD),
+    {ok, #{auth_result := success,
+           anonymous := false}} = emqx_access_control:authenticate(?USER#{password => ?NPASSWORD}),
+
+    ok = emqx_auth_clientid:remove_clientid(?CLIENTID),
+    {ok, #{auth_result := success,
+           anonymous := true}} = emqx_access_control:authenticate(?USER#{password => ?PASSWORD}).
+
+t_cli(_) ->
+    clean_all_clientids(),
+
+    HashType = application:get_env(emqx_auth_clientid, password_hash, sha256),
+
+    emqx_auth_clientid:cli(["add", ?CLIENTID, ?PASSWORD]),
+    [{_, ?CLIENTID, <<Salt:4/binary, Hash/binary>>}] = emqx_auth_clientid:lookup_clientid(?CLIENTID),
+    ?assertEqual(Hash, emqx_passwd:hash(HashType, <<Salt/binary, ?PASSWORD/binary>>)),
+
+    emqx_auth_clientid:cli(["update", ?CLIENTID, ?NPASSWORD]),
+    [{_, ?CLIENTID, <<Salt1:4/binary, Hash1/binary>>}] = emqx_auth_clientid:lookup_clientid(?CLIENTID),
+    ?assertEqual(Hash1, emqx_passwd:hash(HashType, <<Salt1/binary, ?NPASSWORD/binary>>)),
+
+    emqx_auth_clientid:cli(["del", ?CLIENTID]),
+    ?assertEqual([], emqx_auth_clientid:lookup_clientid(?CLIENTID)),
+
+    emqx_auth_clientid:cli(["add", "user1", "pass1"]),
+    emqx_auth_clientid:cli(["add", "user2", "pass2"]),
+    ?assertEqual(2, length(emqx_auth_clientid:cli(["list"]))),
+
+    emqx_auth_clientid:cli(usage).
+
+t_rest_api(_Config) ->
+    clean_all_clientids(),
+
+    HashType = application:get_env(emqx_auth_clientid, password_hash, sha256),
+
+    {ok, Result} = request_http_rest_list(),
+    [] = get_http_data(Result),
+
+    {ok, _} = request_http_rest_add(?CLIENTID, ?PASSWORD),
+    {ok, Result1} = request_http_rest_lookup(?CLIENTID),
+    #{<<"password">> := Hash} = get_http_data(Result1),
+    [{_, ?CLIENTID, <<Salt:4/binary, Hash/binary>>}] = emqx_auth_clientid:lookup_clientid(?CLIENTID),
+    ?assertEqual(Hash, emqx_passwd:hash(HashType, <<Salt/binary, ?PASSWORD/binary>>)),
+
+    {ok, _} = request_http_rest_update(?CLIENTID, ?NPASSWORD),
+    [{_, ?CLIENTID, <<Salt1:4/binary, Hash1/binary>>}] = emqx_auth_clientid:lookup_clientid(?CLIENTID),
+    ?assertEqual(Hash1, emqx_passwd:hash(HashType, <<Salt1/binary, ?NPASSWORD/binary>>)),
+
+    {ok, _} = request_http_rest_delete(?CLIENTID),
+    ?assertEqual([], emqx_auth_clientid:lookup_clientid(?CLIENTID)).
+
+t_conf_not_override_existed(_) ->
+    clean_all_clientids(),
+
+    application:stop(emqx_auth_clientid),
+    application:set_env(emqx_auth_clientid, client_list, [{?CLIENTID, ?PASSWORD}]),
+    application:ensure_all_started(emqx_auth_clientid),
+
+    {ok, _} = emqx_access_control:authenticate(?USER#{password => ?PASSWORD}),
+    emqx_auth_clientid:cli(["update", ?CLIENTID, ?NPASSWORD]),
+
+    {error, _} = emqx_access_control:authenticate(?USER#{password => ?PASSWORD}),
+    {ok, _} = emqx_access_control:authenticate(?USER#{password => ?NPASSWORD}),
+
+    application:stop(emqx_auth_clientid),
+    application:ensure_all_started(emqx_auth_clientid),
+    {ok, _} = emqx_access_control:authenticate(?USER#{password => ?NPASSWORD}),
+
+    ct:pal("~p", [ets:tab2list(emqx_auth_clientid)]),
+    {ok, _} = request_http_rest_update(?CLIENTID, ?PASSWORD),
+    application:stop(emqx_auth_clientid),
+    application:ensure_all_started(emqx_auth_clientid),
+    ct:pal("~p", [ets:tab2list(emqx_auth_clientid)]),
+    {ok, _} = emqx_access_control:authenticate(?USER#{password => ?PASSWORD}).
+
+%%--------------------------------------------------------------------
+%% Helpers
+%%--------------------------------------------------------------------
+
+clean_all_clientids() ->
+    [mnesia:dirty_delete({emqx_auth_clientid, Id})
+     || Id <- mnesia:dirty_all_keys(emqx_auth_clientid)].
+
+%%--------------------------------------------------------------------
+%% REST API Requests
+
+request_http_rest_list() ->
+    request_api(get, uri(), default_auth_header()).
+
+request_http_rest_lookup(ClientId) ->
+    request_api(get, uri([ClientId]), default_auth_header()).
+
+request_http_rest_add(ClientId, Password) ->
+    Params = #{<<"clientid">> => ClientId, <<"password">> => Password},
+    request_api(post, uri(), [], default_auth_header(), Params).
+
+request_http_rest_update(ClientId, Password) ->
+    Params = #{<<"password">> => Password},
+    request_api(put, uri([ClientId]), [], default_auth_header(), Params).
+
+request_http_rest_delete(ClientId) ->
+    request_api(delete, uri([ClientId]), default_auth_header()).
+
+%% @private
+uri() -> uri([]).
+uri(Parts) when is_list(Parts) ->
+    NParts = [b2l(E) || E <- Parts],
+    %% http://127.0.0.1:8080/api/v4/auth_clientid/<P1>/<P2>/<Pn>
+    ?HOST ++ filename:join([?BASE_PATH, ?API_VERSION, "auth_clientid"| NParts]).
+
+%% @private
+b2l(B) when is_binary(B) ->
+    binary_to_list(B);
+b2l(L) when is_list(L) ->
+    L.
+

+ 100 - 0
apps/emqx_auth_http/README.md

@@ -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.
+

+ 25 - 0
apps/emqx_auth_http/include/emqx_auth_http.hrl

@@ -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)).

+ 165 - 0
apps/emqx_auth_http/priv/emqx_auth_http.schema

@@ -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}.

+ 1 - 0
apps/emqx_auth_http/rebar.config

@@ -0,0 +1 @@
+{deps, []}.

+ 92 - 0
apps/emqx_auth_http/src/emqx_acl_http.erl

@@ -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_libs/include/emqx.hrl").
+-include_lib("emqx_libs/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.
+

+ 14 - 0
apps/emqx_auth_http/src/emqx_auth_http.app.src

@@ -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,emqx_libs]},
+  {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"}
+          ]}
+ ]}.

+ 24 - 0
apps/emqx_auth_http/src/emqx_auth_http.app.src.script

@@ -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.
+

+ 116 - 0
apps/emqx_auth_http/src/emqx_auth_http.erl

@@ -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_libs/include/emqx.hrl").
+-include_lib("emqx_libs/include/logger.hrl").
+-include_lib("emqx_libs/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.

+ 103 - 0
apps/emqx_auth_http/src/emqx_auth_http_app.erl

@@ -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.

+ 101 - 0
apps/emqx_auth_http/src/emqx_auth_http_cli.erl

@@ -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).
+

+ 167 - 0
apps/emqx_auth_http/test/emqx_auth_http_SUITE.erl

@@ -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_libs/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'))).
+

+ 152 - 0
apps/emqx_auth_http/test/http_auth_server.erl

@@ -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.

+ 90 - 0
apps/emqx_auth_jwt/README.md

@@ -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.

+ 2 - 0
apps/emqx_auth_jwt/TODO.md

@@ -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/)
+

+ 3 - 0
apps/emqx_auth_jwt/doc/hmac-vs-ecdsa-for-jwt.txt

@@ -0,0 +1,3 @@
+
+https://crypto.stackexchange.com/questions/30657/hmac-vs-ecdsa-for-jwt
+

+ 48 - 0
apps/emqx_auth_jwt/priv/emqx_auth_jwt.schema

@@ -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}.

+ 3 - 0
apps/emqx_auth_jwt/rebar.config

@@ -0,0 +1,3 @@
+{deps,
+ [{jwerl, {git, "https://github.com/emqx/jwerl.git", {branch, "1.1.1"}}}
+ ]}.

+ 14 - 0
apps/emqx_auth_jwt/src/emqx_auth_jwt.app.src

@@ -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,emqx_libs]},
+  {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"}
+          ]}
+ ]}.

+ 24 - 0
apps/emqx_auth_jwt/src/emqx_auth_jwt.app.src.script

@@ -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.
+

+ 146 - 0
apps/emqx_auth_jwt/src/emqx_auth_jwt.erl

@@ -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_libs/include/emqx.hrl").
+-include_lib("emqx_libs/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).
+

+ 69 - 0
apps/emqx_auth_jwt/src/emqx_auth_jwt_app.erl

@@ -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).
+

+ 137 - 0
apps/emqx_auth_jwt/test/emqx_auth_jwt_SUITE.erl

@@ -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_libs/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).
+

+ 26 - 0
apps/emqx_auth_ldap/.ci/docker-compose.yml

@@ -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

+ 26 - 0
apps/emqx_auth_ldap/.ci/emqx-ldap/Dockerfile

@@ -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 []

+ 16 - 0
apps/emqx_auth_ldap/.ci/emqx-ldap/slapd.conf

@@ -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

+ 96 - 0
apps/emqx_auth_ldap/README.md

@@ -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.
+

+ 135 - 0
apps/emqx_auth_ldap/emqx.io.ldif

@@ -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=
+

+ 46 - 0
apps/emqx_auth_ldap/emqx.schema

@@ -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 ) )

+ 23 - 0
apps/emqx_auth_ldap/include/emqx_auth_ldap.hrl

@@ -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)).

+ 176 - 0
apps/emqx_auth_ldap/priv/emqx_auth_ldap.schema

@@ -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}
+]}.

+ 3 - 0
apps/emqx_auth_ldap/rebar.config

@@ -0,0 +1,3 @@
+{deps,
+ [{eldap2, {git, "https://github.com/emqx/eldap2", {tag, "v0.2.2"}}}
+ ]}.

+ 98 - 0
apps/emqx_auth_ldap/src/emqx_acl_ldap.erl

@@ -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_libs/include/emqx.hrl").
+-include_lib("eldap/include/eldap.hrl").
+-include_lib("emqx_libs/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".
+

+ 14 - 0
apps/emqx_auth_ldap/src/emqx_auth_ldap.app.src

@@ -0,0 +1,14 @@
+{application, emqx_auth_ldap,
+ [{description, "EMQ X Authentication/ACL with LDAP"},
+  {vsn, "git"},
+  {modules, []},
+  {registered, [emqx_auth_ldap_sup,emqx_libs]},
+  {applications, [kernel,stdlib,eldap2,ecpool,emqx_passwd,emqx_libs]},
+  {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"}
+          ]}
+ ]}.

+ 24 - 0
apps/emqx_auth_ldap/src/emqx_auth_ldap.app.src.script

@@ -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.
+

+ 210 - 0
apps/emqx_auth_ldap/src/emqx_auth_ldap.erl

@@ -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_libs/include/emqx.hrl").
+-include_lib("eldap/include/eldap.hrl").
+-include_lib("emqx_libs/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).

+ 78 - 0
apps/emqx_auth_ldap/src/emqx_auth_ldap_app.erl

@@ -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}.

+ 150 - 0
apps/emqx_auth_ldap/src/emqx_auth_ldap_cli.erl

@@ -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_libs/include/emqx.hrl").
+-include_lib("emqx_libs/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}}.
+

+ 35 - 0
apps/emqx_auth_ldap/src/emqx_auth_ldap_sup.erl

@@ -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]}}.
+

+ 20 - 0
apps/emqx_auth_ldap/test/certs/cacert.pem

@@ -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-----

+ 19 - 0
apps/emqx_auth_ldap/test/certs/cert.pem

@@ -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-----

+ 19 - 0
apps/emqx_auth_ldap/test/certs/client-cert.pem

@@ -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-----

+ 27 - 0
apps/emqx_auth_ldap/test/certs/client-key.pem

@@ -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-----

+ 27 - 0
apps/emqx_auth_ldap/test/certs/key.pem

@@ -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-----

+ 153 - 0
apps/emqx_auth_ldap/test/emqx_auth_ldap_SUITE.erl

@@ -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_libs/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").
+

+ 114 - 0
apps/emqx_auth_ldap/test/emqx_auth_ldap_bind_as_user_SUITE.erl

@@ -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_libs/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.
+

+ 2 - 0
apps/emqx_auth_mnesia/README.md

@@ -0,0 +1,2 @@
+emqx_auth_mnesia
+===============

+ 35 - 0
apps/emqx_auth_mnesia/include/emqx_auth_mnesia.hrl

@@ -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)).

+ 35 - 0
apps/emqx_auth_mnesia/priv/emqx_auth_mnesia.schema

@@ -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}.

+ 1 - 0
apps/emqx_auth_mnesia/rebar.config

@@ -0,0 +1 @@
+{deps, []}.

+ 83 - 0
apps/emqx_auth_mnesia/src/emqx_acl_mnesia.erl

@@ -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.

+ 148 - 0
apps/emqx_auth_mnesia/src/emqx_acl_mnesia_api.erl

@@ -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.
+

+ 14 - 0
apps/emqx_auth_mnesia/src/emqx_auth_mnesia.app.src

@@ -0,0 +1,14 @@
+{application, emqx_auth_mnesia,
+ [{description, "EMQ X Authentication with Mnesia"},
+  {vsn, "git"},
+  {modules, []},
+  {registered, []},
+  {applications, [kernel,stdlib,mnesia,emqx_libs]},
+  {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"}
+          ]}
+ ]}.

+ 24 - 0
apps/emqx_auth_mnesia/src/emqx_auth_mnesia.app.src.script

@@ -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.
+

+ 76 - 0
apps/emqx_auth_mnesia/src/emqx_auth_mnesia.erl

@@ -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_libs/include/emqx.hrl").
+-include_lib("emqx_libs/include/logger.hrl").
+-include_lib("emqx_libs/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.

+ 201 - 0
apps/emqx_auth_mnesia/src/emqx_auth_mnesia_api.erl

@@ -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.
+

+ 70 - 0
apps/emqx_auth_mnesia/src/emqx_auth_mnesia_app.erl

@@ -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]).
+

+ 193 - 0
apps/emqx_auth_mnesia/src/emqx_auth_mnesia_cli.erl

@@ -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_libs/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"}]).

+ 36 - 0
apps/emqx_auth_mnesia/src/emqx_auth_mnesia_sup.erl

@@ -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}, []}}.

+ 279 - 0
apps/emqx_auth_mnesia/test/emqx_acl_mnesia_SUITE.erl

@@ -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).

+ 259 - 0
apps/emqx_auth_mnesia/test/emqx_auth_mnesia_SUITE.erl

@@ -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.

+ 31 - 0
apps/emqx_auth_mongo/CHANGES

@@ -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
+

+ 192 - 0
apps/emqx_auth_mongo/README.md

@@ -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.
+

+ 31 - 0
apps/emqx_auth_mongo/docker-compose-ssl.yml

@@ -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

+ 27 - 0
apps/emqx_auth_mongo/docker-compose.yml

@@ -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

+ 37 - 0
apps/emqx_auth_mongo/include/emqx_auth_mongo.hrl

@@ -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)).

+ 292 - 0
apps/emqx_auth_mongo/priv/emqx_auth_mongo.schema

@@ -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}.

+ 2 - 0
apps/emqx_auth_mongo/rebar.config

@@ -0,0 +1,2 @@
+{deps,
+ [{mongodb, {git,"https://github.com/emqx/mongodb-erlang", {tag, "v3.0.7"}}}]}.

+ 91 - 0
apps/emqx_auth_mongo/src/emqx_acl_mongo.erl

@@ -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".
+

+ 14 - 0
apps/emqx_auth_mongo/src/emqx_auth_mongo.app.src

@@ -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,emqx_libs]},
+  {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"}
+          ]}
+ ]}.

+ 24 - 0
apps/emqx_auth_mongo/src/emqx_auth_mongo.app.src.script

@@ -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.
+

+ 134 - 0
apps/emqx_auth_mongo/src/emqx_auth_mongo.erl

@@ -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))).

+ 87 - 0
apps/emqx_auth_mongo/src/emqx_auth_mongo_app.erl

@@ -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])}.
+

+ 34 - 0
apps/emqx_auth_mongo/src/emqx_auth_mongo_sup.erl

@@ -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]}}.
+

+ 174 - 0
apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE.erl

@@ -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_libs/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).

+ 27 - 0
apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/ca-key.pem

@@ -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-----

+ 19 - 0
apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/ca.pem

@@ -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-----

+ 19 - 0
apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/client-cert.pem

@@ -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-----

+ 27 - 0
apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/client-key.pem

@@ -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-----

+ 46 - 0
apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/mongodb.pem

@@ -0,0 +1,46 @@
+-----BEGIN CERTIFICATE-----
+MIIDBDCCAeygAwIBAgIBAjANBgkqhkiG9w0BAQsFADA8MTowOAYDVQQDDDFNeVNR
+TF9TZXJ2ZXJfOC4wLjE5X0F1dG9fR2VuZXJhdGVkX0NBX0NlcnRpZmljYXRlMB4X
+DTIwMDYxMTAzMzg0NloXDTMwMDYwOTAzMzg0NlowQDE+MDwGA1UEAww1TXlTUUxf
+U2VydmVyXzguMC4xOV9BdXRvX0dlbmVyYXRlZF9TZXJ2ZXJfQ2VydGlmaWNhdGUw
+ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCcEnEm5hqP1EbEJycOz8Ua
+NWp29QdpFUzTWhkKGhVXk+0msmNTw4NBAFB42moY44OU8wvDideOlJNhPRWveD8z
+G2lxzJA91p0UK4et8ia9MmeuCGhdC9jxJ8X69WNlUiPyy0hI/ZsqRq9Z0C2eW0iL
+JPXsy4X8Xpw3SFwoXf5pR9RFY5Pb2tuyxqmSestu2VXT/NQjJg4CVDR3mFcHPXZB
+4elRzH0WshExEGkgy0bg20MJeRc2Qdb5Xx+EakbmwroDWaCn3NSGqQ7jv6Vw0doy
+TGvS6h6RHBxnyqRfRgKGlCoOMG9/5+rFJC00QpCUG2vHXHWGoWlMlJ3foN7rj5v9
+AgMBAAGjDTALMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEBAJ5zt2rj4Ag6
+zpN59AWC1Fur8g8l41ksHkSpKPp+PtyO/ngvbMqBpfmK1e7JCKZv/68QXfMyWWAI
+hwalqZkXXWHKjuz3wE7dE25PXFXtGJtcZAaj10xt98fzdqt8lQSwh2kbfNwZIz1F
+sgAStgE7+ZTcqTgvNB76Os1UK0to+/P0VBWktaVFdyub4Nc2SdPVnZNvrRBXBwOD
+3V8ViwywDOFoE7DvCvwx/SVsvoC0Z4j3AMMovO6oHicP7uU83qsQgm1Qru3YeoLR
++DoVi7IPHbWvN7MqFYn3YjNlByO2geblY7MR0BlqbFlmFrqLsUfjsh2ys7/U/knC
+dN/klu446fI=
+-----END CERTIFICATE-----
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAnBJxJuYaj9RGxCcnDs/FGjVqdvUHaRVM01oZChoVV5PtJrJj
+U8ODQQBQeNpqGOODlPMLw4nXjpSTYT0Vr3g/MxtpccyQPdadFCuHrfImvTJnrgho
+XQvY8SfF+vVjZVIj8stISP2bKkavWdAtnltIiyT17MuF/F6cN0hcKF3+aUfURWOT
+29rbssapknrLbtlV0/zUIyYOAlQ0d5hXBz12QeHpUcx9FrIRMRBpIMtG4NtDCXkX
+NkHW+V8fhGpG5sK6A1mgp9zUhqkO47+lcNHaMkxr0uoekRwcZ8qkX0YChpQqDjBv
+f+fqxSQtNEKQlBtrx1x1hqFpTJSd36De64+b/QIDAQABAoIBAFiah66Dt9SruLkn
+WR8piUaFyLlcBib8Nq9OWSTJBhDAJERxxb4KIvvGB+l0ZgNXNp5bFPSfzsZdRwZP
+PX5uj8Kd71Dxx3mz211WESMJdEC42u+MSmN4lGLkJ5t/sDwXU91E1vbJM0ve8THV
+4/Ag9qA4DX2vVZOeyqT/6YHpSsPNZplqzrbAiwrfHwkctHfgqwOf3QLfhmVQgfCS
+VwidBldEUv2whSIiIxh4Rv5St4kA68IBCbJxdpOpyuQBkk6CkxZ7VN9FqOuSd4Pk
+Wm7iWyBMZsCmELZh5XAXld4BEt87C5R4CvbPBDZxAv3THk1DNNvpy3PFQfwARRFb
+SAToYMECgYEAyL7U8yxpzHDYWd3oCx6vTi9p9N/z0FfAkWrRF6dm4UcSklNiT1Aq
+EOnTA+SaW8tV3E64gCWcY23gNP8so/ZseWj6L+peHwtchaP9+KB7yGw2A+05+lOx
+VetLTjAOmfpiUXFe5w1q4C1RGhLjZjjzW+GvwdAuchQgUEFaomrV+PUCgYEAxwfH
+cmVGFbAktcjU4HSRjKSfawCrut+3YUOLybyku3Q/hP9amG8qkVTFe95CTLjLe2D0
+ccaTTpofFEJ32COeck0g0Ujn/qQ+KXRoauOYs4FB1DtqMpqB78wufWEUpDpbd9/h
+J+gJdC/IADd4tJW9zA92g8IA7ZtFmqDtiSpQ0ekCgYAQGkaorvJZpN+l7cf0RGTZ
+h7IfI2vCVZer0n6tQA9fmLzjoe6r4AlPzAHSOR8sp9XeUy43kUzHKQQoHCPvjw/K
+eWJAP7OHF/k2+x2fOPhU7mEy1W+mJdp+wt4Kio5RSaVjVQ3AyPG+w8PSrJszEvRq
+dWMMz+851WV2KpfjmWBKlQKBgQC++4j4DZQV5aMkSKV1CIZOBf3vaIJhXKEUFQPD
+PmB4fBEjpwCg+zNGp6iktt65zi17o8qMjrb1mtCt2SY04eD932LZUHNFlwcLMmes
+Ad+aiDLJ24WJL1f16eDGcOyktlblDZB5gZ/ovJzXEGOkLXglosTfo77OQculmDy2
+/UL2WQKBgGeKasmGNfiYAcWio+KXgFkHXWtAXB9B91B1OFnCa40wx+qnl71MIWQH
+PQ/CZFNWOfGiNEJIZjrHsfNJoeXkhq48oKcT0AVCDYyLV0VxDO4ejT95mGW6njNd
+JpvmhwwAjOvuWVr0tn4iXlSK8irjlJHmwcRjLTJq97vE9fsA2MjI
+-----END RSA PRIVATE KEY-----

+ 27 - 0
apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/private_key.pem

@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEA1zVmMhPqpSPMmYkKh5wwlRD5XuS8YWJKEM6tjFx61VK8qxHE
+YngkC2KnL5EuKAjQZIF3tJskwt0hAat047CCCZxrkNEpbVvSnvnk+A/8bg/Ww1n3
+qxzfifhsWfpUKlDnwrtH+ftt+5rZeEkf37XAPy7ZjzecAF9SDV6WSiPeAxUX2+hN
+dId42Pf45woo4LFGUlQeagCFkD/R0dpNIMGwcnkKCUikiBqr2ijSIgvRtBfZ9fBG
+jFGER2uE/Eay4AgcQsHue8skRwDCng8OnqtPnBtTytmqTy9V/BRgsVKUoksm6wsx
+kUYwgHeaq7UCvlCm25SZ7yRyd4k8t0BKDf2h+wIDAQABAoIBAEQcrHmRACTADdNS
+IjkFYALt2l8EOfMAbryfDSJtapr1kqz59JPNvmq0EIHnixo0n/APYdmReLML1ZR3
+tYkSpjVwgkLVUC1CcIjMQoGYXaZf8PLnGJHZk45RR8m6hsTV0mQ5bfBaeVa2jbma
+OzJMjcnxg/3l9cPQZ2G/3AUfEPccMxOXp1KRz3mUQcGnKJGtDbN/kfmntcwYoxaE
+Zg4RoeKAoMpK1SSHAiJKe7TnztINJ7uygR9XSzNd6auY8A3vomSIjpYO7XL+lh7L
+izm4Ir3Gb/eCYBvWgQyQa2KCJgK/sQyEs3a09ngofSEUhQJQYhgZDwUj+fDDOGqj
+hCZOA8ECgYEA+ZWuHdcUQ3ygYhLds2QcogUlIsx7C8n/Gk/FUrqqXJrTkuO0Eqqa
+B47lCITvmn2zm0ODfSFIARgKEUEDLS/biZYv7SUTrFqBLcet+aGI7Dpv91CgB75R
+tNzcIf8VxoiP0jPqdbh9mLbbxGi5Uc4p9TVXRljC4hkswaouebWee0sCgYEA3L2E
+YB3kiHrhPI9LHS5Px9C1w+NOu5wP5snxrDGEgaFCvL6zgY6PflacppgnmTXl8D1x
+im0IDKSw5dP3FFonSVXReq3CXDql7UnhfTCiLDahV7bLxTH42FofcBpDN3ERdOal
+58RwQh6VrLkzQRVoObo+hbGlFiwwSAfQC509FhECgYBsRSBpVXo25IN2yBRg09cP
++gdoFyhxrsj5kw1YnB13WrrZh+oABv4WtUhp77E5ZbpaamlKCPwBbXpAjeFg4tfr
+0bksuN7V79UGFQ9FsWuCfr8/nDwv38H2IbFlFhFONMOfPmJBey0Q6JJhm8R41mSh
+OOiJXcv85UrjIH5U0hLUDQKBgQDVLOU5WcUJlPoOXSgiT0ZW5xWSzuOLRUUKEf6l
+19BqzAzCcLy0orOrRAPW01xylt2v6/bJw1Ahva7k1ZZo/kOwjANYoZPxM+ZoSZBN
+MXl8j2mzZuJVV1RFxItV3NcLJNPB/Lk+IbRz9kt/2f9InF7iWR3mSU/wIM6j0X+2
+p6yFsQKBgQCM/ldWb511lA+SNkqXB2P6WXAgAM/7+jwsNHX2ia2Ikufm4SUEKMSv
+mti/nZkHDHsrHU4wb/2cOAywMELzv9EHzdcoenjBQP65OAc/1qWJs+LnBcCXfqKk
+aHjEZW6+brkHdRGLLY3YAHlt/AUL+RsKPJfN72i/FSpmu+52G36eeQ==
+-----END RSA PRIVATE KEY-----

+ 9 - 0
apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/public_key.pem

@@ -0,0 +1,9 @@
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1zVmMhPqpSPMmYkKh5ww
+lRD5XuS8YWJKEM6tjFx61VK8qxHEYngkC2KnL5EuKAjQZIF3tJskwt0hAat047CC
+CZxrkNEpbVvSnvnk+A/8bg/Ww1n3qxzfifhsWfpUKlDnwrtH+ftt+5rZeEkf37XA
+Py7ZjzecAF9SDV6WSiPeAxUX2+hNdId42Pf45woo4LFGUlQeagCFkD/R0dpNIMGw
+cnkKCUikiBqr2ijSIgvRtBfZ9fBGjFGER2uE/Eay4AgcQsHue8skRwDCng8OnqtP
+nBtTytmqTy9V/BRgsVKUoksm6wsxkUYwgHeaq7UCvlCm25SZ7yRyd4k8t0BKDf2h
++wIDAQAB
+-----END PUBLIC KEY-----

+ 19 - 0
apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/server-cert.pem

@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDBDCCAeygAwIBAgIBAjANBgkqhkiG9w0BAQsFADA8MTowOAYDVQQDDDFNeVNR
+TF9TZXJ2ZXJfOC4wLjE5X0F1dG9fR2VuZXJhdGVkX0NBX0NlcnRpZmljYXRlMB4X
+DTIwMDYxMTAzMzg0NloXDTMwMDYwOTAzMzg0NlowQDE+MDwGA1UEAww1TXlTUUxf
+U2VydmVyXzguMC4xOV9BdXRvX0dlbmVyYXRlZF9TZXJ2ZXJfQ2VydGlmaWNhdGUw
+ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCcEnEm5hqP1EbEJycOz8Ua
+NWp29QdpFUzTWhkKGhVXk+0msmNTw4NBAFB42moY44OU8wvDideOlJNhPRWveD8z
+G2lxzJA91p0UK4et8ia9MmeuCGhdC9jxJ8X69WNlUiPyy0hI/ZsqRq9Z0C2eW0iL
+JPXsy4X8Xpw3SFwoXf5pR9RFY5Pb2tuyxqmSestu2VXT/NQjJg4CVDR3mFcHPXZB
+4elRzH0WshExEGkgy0bg20MJeRc2Qdb5Xx+EakbmwroDWaCn3NSGqQ7jv6Vw0doy
+TGvS6h6RHBxnyqRfRgKGlCoOMG9/5+rFJC00QpCUG2vHXHWGoWlMlJ3foN7rj5v9
+AgMBAAGjDTALMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEBAJ5zt2rj4Ag6
+zpN59AWC1Fur8g8l41ksHkSpKPp+PtyO/ngvbMqBpfmK1e7JCKZv/68QXfMyWWAI
+hwalqZkXXWHKjuz3wE7dE25PXFXtGJtcZAaj10xt98fzdqt8lQSwh2kbfNwZIz1F
+sgAStgE7+ZTcqTgvNB76Os1UK0to+/P0VBWktaVFdyub4Nc2SdPVnZNvrRBXBwOD
+3V8ViwywDOFoE7DvCvwx/SVsvoC0Z4j3AMMovO6oHicP7uU83qsQgm1Qru3YeoLR
++DoVi7IPHbWvN7MqFYn3YjNlByO2geblY7MR0BlqbFlmFrqLsUfjsh2ys7/U/knC
+dN/klu446fI=
+-----END CERTIFICATE-----

+ 27 - 0
apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/server-key.pem

@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAnBJxJuYaj9RGxCcnDs/FGjVqdvUHaRVM01oZChoVV5PtJrJj
+U8ODQQBQeNpqGOODlPMLw4nXjpSTYT0Vr3g/MxtpccyQPdadFCuHrfImvTJnrgho
+XQvY8SfF+vVjZVIj8stISP2bKkavWdAtnltIiyT17MuF/F6cN0hcKF3+aUfURWOT
+29rbssapknrLbtlV0/zUIyYOAlQ0d5hXBz12QeHpUcx9FrIRMRBpIMtG4NtDCXkX
+NkHW+V8fhGpG5sK6A1mgp9zUhqkO47+lcNHaMkxr0uoekRwcZ8qkX0YChpQqDjBv
+f+fqxSQtNEKQlBtrx1x1hqFpTJSd36De64+b/QIDAQABAoIBAFiah66Dt9SruLkn
+WR8piUaFyLlcBib8Nq9OWSTJBhDAJERxxb4KIvvGB+l0ZgNXNp5bFPSfzsZdRwZP
+PX5uj8Kd71Dxx3mz211WESMJdEC42u+MSmN4lGLkJ5t/sDwXU91E1vbJM0ve8THV
+4/Ag9qA4DX2vVZOeyqT/6YHpSsPNZplqzrbAiwrfHwkctHfgqwOf3QLfhmVQgfCS
+VwidBldEUv2whSIiIxh4Rv5St4kA68IBCbJxdpOpyuQBkk6CkxZ7VN9FqOuSd4Pk
+Wm7iWyBMZsCmELZh5XAXld4BEt87C5R4CvbPBDZxAv3THk1DNNvpy3PFQfwARRFb
+SAToYMECgYEAyL7U8yxpzHDYWd3oCx6vTi9p9N/z0FfAkWrRF6dm4UcSklNiT1Aq
+EOnTA+SaW8tV3E64gCWcY23gNP8so/ZseWj6L+peHwtchaP9+KB7yGw2A+05+lOx
+VetLTjAOmfpiUXFe5w1q4C1RGhLjZjjzW+GvwdAuchQgUEFaomrV+PUCgYEAxwfH
+cmVGFbAktcjU4HSRjKSfawCrut+3YUOLybyku3Q/hP9amG8qkVTFe95CTLjLe2D0
+ccaTTpofFEJ32COeck0g0Ujn/qQ+KXRoauOYs4FB1DtqMpqB78wufWEUpDpbd9/h
+J+gJdC/IADd4tJW9zA92g8IA7ZtFmqDtiSpQ0ekCgYAQGkaorvJZpN+l7cf0RGTZ
+h7IfI2vCVZer0n6tQA9fmLzjoe6r4AlPzAHSOR8sp9XeUy43kUzHKQQoHCPvjw/K
+eWJAP7OHF/k2+x2fOPhU7mEy1W+mJdp+wt4Kio5RSaVjVQ3AyPG+w8PSrJszEvRq
+dWMMz+851WV2KpfjmWBKlQKBgQC++4j4DZQV5aMkSKV1CIZOBf3vaIJhXKEUFQPD
+PmB4fBEjpwCg+zNGp6iktt65zi17o8qMjrb1mtCt2SY04eD932LZUHNFlwcLMmes
+Ad+aiDLJ24WJL1f16eDGcOyktlblDZB5gZ/ovJzXEGOkLXglosTfo77OQculmDy2
+/UL2WQKBgGeKasmGNfiYAcWio+KXgFkHXWtAXB9B91B1OFnCa40wx+qnl71MIWQH
+PQ/CZFNWOfGiNEJIZjrHsfNJoeXkhq48oKcT0AVCDYyLV0VxDO4ejT95mGW6njNd
+JpvmhwwAjOvuWVr0tn4iXlSK8irjlJHmwcRjLTJq97vE9fsA2MjI
+-----END RSA PRIVATE KEY-----

+ 167 - 0
apps/emqx_auth_mysql/README.md

@@ -0,0 +1,167 @@
+emqx_auth_mysql
+===============
+
+Authentication, ACL with MySQL Database.
+
+Notice: changed mysql driver to [mysql-otp](https://github.com/mysql-otp/mysql-otp).
+
+Features
+---------
+
+- Full *Authentication*, *Superuser*, *ACL* support
+- IPv4, IPv6 and TLS support
+- Connection pool by [ecpool](https://github.com/emqx/ecpool)
+- Completely cover MySQL 5.7, MySQL 8 in our tests
+
+Build Plugin
+-------------
+
+make && make tests
+
+Configure Plugin
+----------------
+
+File: etc/emqx_auth_mysql.conf
+
+```
+## MySQL server address.
+##
+## Value: Port | IP:Port
+##
+## Examples: 3306, 127.0.0.1:3306, localhost:3306
+auth.mysql.server = 127.0.0.1:3306
+
+## MySQL pool size.
+##
+## Value: Number
+auth.mysql.pool = 8
+
+## MySQL username.
+##
+## Value: String
+## auth.mysql.username =
+
+## MySQL Password.
+##
+## Value: String
+## auth.mysql.password =
+
+## MySQL database.
+##
+## Value: String
+auth.mysql.database = mqtt
+
+## Variables: %u = username, %c = clientid
+
+## Authentication query.
+##
+## Note that column names should be 'password' and 'salt' (if used).
+## In case column names differ in your DB - please use aliases,
+## e.g. "my_column_name as password".
+##
+## Value: SQL
+##
+## Variables:
+##  - %u: username
+##  - %c: clientid
+##  - %C: common name of client TLS cert
+##  - %d: subject of client TLS cert
+##
+auth.mysql.auth_query = select password from mqtt_user where username = '%u' limit 1
+## auth.mysql.auth_query = select password_hash as password from mqtt_user where username = '%u' limit 1
+
+## Password hash.
+##
+## Value: plain | md5 | sha | sha256 | bcrypt
+auth.mysql.password_hash = sha256
+
+## sha256 with salt prefix
+## auth.mysql.password_hash = salt,sha256
+
+## bcrypt with salt only prefix
+## auth.mysql.password_hash = salt,bcrypt
+
+## sha256 with salt suffix
+## auth.mysql.password_hash = sha256,salt
+
+## pbkdf2 with macfun iterations dklen
+## macfun: md4, md5, ripemd160, sha, sha224, sha256, sha384, sha512
+## auth.mysql.password_hash = pbkdf2,sha256,1000,20
+
+## Superuser query.
+##
+## Value: SQL
+##
+## Variables:
+##  - %u: username
+##  - %c: clientid
+##  - %C: common name of client TLS cert
+##  - %d: subject of client TLS cert
+auth.mysql.super_query = select is_superuser from mqtt_user where username = '%u' limit 1
+
+## ACL query.
+##
+## Value: SQL
+##
+## Variables:
+##  - %a: ipaddr
+##  - %u: username
+##  - %c: clientid
+## Note: You can add the 'ORDER BY' statement to control the rules match order
+auth.mysql.acl_query = select allow, ipaddr, username, clientid, access, topic from mqtt_acl where ipaddr = '%a' or username = '%u' or username = '$all' or clientid = '%c'
+
+```
+
+Import mqtt.sql
+---------------
+
+Import mqtt.sql into your database.
+
+Load Plugin
+-----------
+
+./bin/emqx_ctl plugins load emqx_auth_mysql
+
+Auth Table
+----------
+
+Notice: This is a demo table. You could authenticate with any user table.
+
+```sql
+CREATE TABLE `mqtt_user` (
+  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+  `username` varchar(100) DEFAULT NULL,
+  `password` varchar(100) DEFAULT NULL,
+  `salt` varchar(35) DEFAULT NULL,
+  `is_superuser` tinyint(1) DEFAULT 0,
+  `created` datetime DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `mqtt_username` (`username`)
+) ENGINE=MyISAM DEFAULT CHARSET=utf8;
+```
+
+ACL Table
+----------
+
+```sql
+CREATE TABLE `mqtt_acl` (
+  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+  `allow` int(1) DEFAULT NULL COMMENT '0: deny, 1: allow',
+  `ipaddr` varchar(60) DEFAULT NULL COMMENT 'IpAddress',
+  `username` varchar(100) DEFAULT NULL COMMENT 'Username',
+  `clientid` varchar(100) DEFAULT NULL COMMENT 'ClientId',
+  `access` int(2) NOT NULL COMMENT '1: subscribe, 2: publish, 3: pubsub',
+  `topic` varchar(100) NOT NULL DEFAULT '' COMMENT 'Topic Filter',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+```
+
+License
+-------
+
+Apache License Version 2.0
+
+Author
+------
+
+EMQ X Team.

+ 41 - 0
apps/emqx_auth_mysql/docker-compose-ssl.yml

@@ -0,0 +1,41 @@
+version: '3'
+
+services:
+  erlang:
+    image: erlang:22.3
+    volumes:
+      - ./:/emqx_auth_mysql
+    networks:
+      - emqx_bridge
+    depends_on:
+      - mysql_server
+    tty: true
+
+  mysql_server:
+    image: mysql:${MYSQL_TAG}
+    restart: always
+    environment:
+      MYSQL_ROOT_PASSWORD: public
+      MYSQL_DATABASE: mqtt
+    volumes:
+      - ./test/emqx_auth_mysql_SUITE_data/ca.pem:/etc/certs/ca-cert.pem
+      - ./test/emqx_auth_mysql_SUITE_data/server-cert.pem:/etc/certs/server-cert.pem
+      - ./test/emqx_auth_mysql_SUITE_data/server-key.pem:/etc/certs/server-key.pem
+    networks:
+      - emqx_bridge
+    command:
+      --bind-address "::"
+      --default-authentication-plugin=mysql_native_password
+      --character-set-server=utf8mb4
+      --collation-server=utf8mb4_general_ci
+      --explicit_defaults_for_timestamp=true
+      --lower_case_table_names=1
+      --max_allowed_packet=128M
+      --skip-symbolic-links
+      --ssl-ca=/etc/certs/ca.pem
+      --ssl-cert=/etc/certs/server-cert.pem
+      --ssl-key=/etc/certs/server-key.pem
+
+networks:
+  emqx_bridge:
+    driver: bridge

+ 34 - 0
apps/emqx_auth_mysql/docker-compose.yml

@@ -0,0 +1,34 @@
+version: '3'
+
+services:
+  erlang:
+    image: erlang:22.3
+    volumes:
+      - ./:/emqx_auth_mysql
+    networks:
+      - emqx_bridge
+    depends_on:
+      - mysql_server
+    tty: true
+
+  mysql_server:
+    image: mysql:${MYSQL_TAG}
+    restart: always
+    environment:
+      MYSQL_ROOT_PASSWORD: public
+      MYSQL_DATABASE: mqtt
+    networks:
+      - emqx_bridge
+    command:
+      --bind-address "::"
+      --default-authentication-plugin=mysql_native_password
+      --character-set-server=utf8mb4
+      --collation-server=utf8mb4_general_ci
+      --explicit_defaults_for_timestamp=true
+      --lower_case_table_names=1
+      --max_allowed_packet=128M
+      --skip-symbolic-links
+
+networks:
+  emqx_bridge:
+    driver: bridge

+ 23 - 0
apps/emqx_auth_mysql/include/emqx_auth_mysql.hrl

@@ -0,0 +1,23 @@
+
+-define(APP, emqx_auth_mysql).
+
+-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)).

+ 41 - 0
apps/emqx_auth_mysql/mqtt.sql

@@ -0,0 +1,41 @@
+
+DROP TABLE IF EXISTS `mqtt_acl`;
+
+CREATE TABLE `mqtt_acl` (
+  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+  `allow` int(1) DEFAULT NULL COMMENT '0: deny, 1: allow',
+  `ipaddr` varchar(60) DEFAULT NULL COMMENT 'IpAddress',
+  `username` varchar(100) DEFAULT NULL COMMENT 'Username',
+  `clientid` varchar(100) DEFAULT NULL COMMENT 'ClientId',
+  `access` int(2) NOT NULL COMMENT '1: subscribe, 2: publish, 3: pubsub',
+  `topic` varchar(100) NOT NULL DEFAULT '' COMMENT 'Topic Filter',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+LOCK TABLES `mqtt_acl` WRITE;
+
+INSERT INTO `mqtt_acl` (`id`, `allow`, `ipaddr`, `username`, `clientid`, `access`, `topic`)
+VALUES
+	(1,1,NULL,'$all',NULL,2,'#'),
+	(2,0,NULL,'$all',NULL,1,'$SYS/#'),
+	(3,0,NULL,'$all',NULL,1,'eq #'),
+	(4,1,'127.0.0.1',NULL,NULL,2,'$SYS/#'),
+	(5,1,'127.0.0.1',NULL,NULL,2,'#'),
+	(6,1,NULL,'dashboard',NULL,1,'$SYS/#');
+
+UNLOCK TABLES;
+
+
+DROP TABLE IF EXISTS `mqtt_user`;
+
+CREATE TABLE `mqtt_user` (
+  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+  `username` varchar(100) DEFAULT NULL,
+  `password` varchar(100) DEFAULT NULL,
+  `salt` varchar(35) DEFAULT NULL,
+  `is_superuser` tinyint(1) DEFAULT 0,
+  `created` datetime DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `mqtt_username` (`username`)
+) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
+

+ 0 - 0
apps/emqx_auth_mysql/priv/emqx_auth_mysql.schema


Some files were not shown because too many files changed in this diff